Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@ The extension exposes several settings under `cloudsmith-vsc.*`:
| `cloudsmith-vsc.showLicenseIndicators` | Show license risk classification on packages. Default: `true`. |
| `cloudsmith-vsc.showDockerDigestCommand` | Show an additional "Pull by digest" option for Docker install commands. Default: `false`. |
| `cloudsmith-vsc.experimentalSSOBrowser` | Enable experimental browser-based SSO authentication. Default: `false`. |
| `cloudsmith-vsc.useLegacyWebApp` | Use the legacy `cloudsmith.io` webapp for platform links. Default: `false`. |
| `cloudsmith-vsc.autoScanOnOpen` | Automatically scan project dependencies against Cloudsmith when a workspace is opened. Default: `false`. |
| `cloudsmith-vsc.dependencyScanWorkspace` | Cloudsmith workspace slug to use for dependency health scanning. |
| `cloudsmith-vsc.dependencyScanRepo` | Cloudsmith repository slug to use for dependency health scanning. |
Expand Down
29 changes: 10 additions & 19 deletions extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -755,21 +755,9 @@ async function activate(context) {
const version = unwrapValue(item.version);
const identifier = unwrapValue(item.slug_perm);

//need to replace '/' in name as UI URL replaces these with _
const pkg = name.replaceAll("/", "_");

const config = vscode.workspace.getConfiguration("cloudsmith-vsc");
const useLegacyApp = await config.get("useLegacyWebApp");


if (identifier) {
if (useLegacyApp) {
const url = `https://cloudsmith.io/~${workspace}/repos/${repo}/packages/detail/${format}/${pkg}/${version}`;
vscode.env.openExternal(vscode.Uri.parse(url));
} else {
const url = `https://app.cloudsmith.com/${workspace}/${repo}/${format}/${pkg}/${version}/${identifier}`;
vscode.env.openExternal(vscode.Uri.parse(url));
}
const url = buildPackageUrl(workspace, repo, format, name, version, identifier);
if (url) {
vscode.env.openExternal(vscode.Uri.parse(url));
} else {
vscode.window.showWarningMessage("Run this command from a package context menu.");
}
Expand All @@ -786,10 +774,12 @@ async function activate(context) {
const name = typeof item === "string" ? item : item.name;

if (name) {
// Encode special characters for URL
const encodedName = name.replaceAll("/", "%2F").replaceAll(":", "%3A");
const url = `https://app.cloudsmith.com/${workspace}/${repo}?page=1&query=name:${encodedName}&sort=name`;
vscode.env.openExternal(vscode.Uri.parse(url));
const url = buildPackageGroupUrl(workspace, repo, name);
if (url) {
vscode.env.openExternal(vscode.Uri.parse(url));
return;
}
vscode.window.showWarningMessage("Please use this command from the package context menu.");
} else {
vscode.window.showWarningMessage("Run this command from a package context menu.");
}
Expand Down Expand Up @@ -1288,6 +1278,7 @@ async function activate(context) {
} else {
vscode.window.showInformationMessage("Could not open this package in Cloudsmith.");
}
vscode.env.openExternal(vscode.Uri.parse(packageUrl));
} else if (action.id === "inspect") {
const inspectItem = {
name: name,
Expand Down
1 change: 0 additions & 1 deletion models/dependencyHealthNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class DependencyHealthNode {
this.slug_perm_raw = cloudsmithMatch.slug_perm;
this.version = { id: "Version", value: cloudsmithMatch.version };
this.status_str = { id: "Status", value: cloudsmithMatch.status_str };
this.self_webapp_url = cloudsmithMatch.self_webapp_url || null;
this.checksum_sha256 = cloudsmithMatch.checksum_sha256 || null;
this.version_digest = cloudsmithMatch.version_digest || null;
this.tags_raw = cloudsmithMatch.tags || {};
Expand Down
30 changes: 30 additions & 0 deletions test/webAppUrls.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const assert = require("assert");
const {
WEB_APP_BASE_URL,
buildPackageGroupUrl,
buildPackageUrl,
buildRepositoryUrl,
} = require("../util/webAppUrls");

suite("Web app URL helpers", () => {
test("buildRepositoryUrl always uses the app domain", () => {
assert.strictEqual(
buildRepositoryUrl("my-org", "my-repo"),
`${WEB_APP_BASE_URL}/my-org/my-repo`
);
});

test("buildPackageUrl always uses the app domain and package slug path", () => {
assert.strictEqual(
buildPackageUrl("my-org", "my-repo", "npm", "@scope/pkg", "1.0.0", "pkg-id"),
`${WEB_APP_BASE_URL}/my-org/my-repo/npm/@scope_pkg/1.0.0/pkg-id`
);
});

test("buildPackageGroupUrl always uses the app domain and repo search path", () => {
assert.strictEqual(
buildPackageGroupUrl("my-org", "my-repo", "group/name:latest"),
`${WEB_APP_BASE_URL}/my-org/my-repo?page=1&query=name:group%2Fname%3Alatest&sort=name`
);
});
});
18 changes: 12 additions & 6 deletions util/diagnosticsPublisher.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

const vscode = require("vscode");
const { ManifestParser } = require("./manifestParser");
const { buildRepositoryUrl } = require("./webAppUrls");

class DiagnosticsPublisher {
constructor() {
Expand Down Expand Up @@ -61,12 +62,17 @@ class DiagnosticsPublisher {

// Add related info if there's a fix version available
if (dep.cloudsmithMatch && dep.cloudsmithMatch.num_vulnerabilities > 0) {
diagnostic.code = {
value: `${dep.cloudsmithMatch.num_vulnerabilities} vulnerabilities`,
target: vscode.Uri.parse(
`https://app.cloudsmith.com/${dep.cloudsmithMatch.namespace}/${dep.cloudsmithMatch.repository}`
),
};
const repositoryUrl = buildRepositoryUrl(
dep.cloudsmithMatch.namespace,
dep.cloudsmithMatch.repository
);
const vulnerabilityCode = `${dep.cloudsmithMatch.num_vulnerabilities} vulnerabilities`;
diagnostic.code = repositoryUrl
? {
value: vulnerabilityCode,
target: vscode.Uri.parse(repositoryUrl),
}
: vulnerabilityCode;
}

diagnostics.push(diagnostic);
Expand Down
5 changes: 4 additions & 1 deletion util/installCommandBuilder.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Install command builder - generates format-native install commands
// with Cloudsmith registry URLs pre-filled.

const { WEB_APP_BASE_URL, buildRepositoryUrl } = require("./webAppUrls");

const VERIFICATION_BANNER = "# Verify package details before running";

class InstallCommandBuilder {
Expand Down Expand Up @@ -194,9 +196,10 @@ class InstallCommandBuilder {

const entry = commands[format];
if (!entry) {
const repositoryUrl = buildRepositoryUrl(workspace, repo) || WEB_APP_BASE_URL;
return {
command: `# Verify package details before running\n# No install command template for format: ${format}`,
note: `Visit https://app.cloudsmith.com/${workspace}/${repo} for setup instructions.`,
note: `Visit ${repositoryUrl} for setup instructions.`,
};
}
return entry;
Expand Down
41 changes: 41 additions & 0 deletions util/webAppUrls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2026 Cloudsmith Ltd. All rights reserved.
const WEB_APP_BASE_URL = "https://app.cloudsmith.com";

Comment thread
DevonL marked this conversation as resolved.
function encodePathSegment(value) {
return encodeURIComponent(String(value));
}

function buildRepositoryUrl(workspace, repo) {
if (!workspace || !repo) {
return null;
}

return `${WEB_APP_BASE_URL}/${encodePathSegment(workspace)}/${encodePathSegment(repo)}`;
}

function buildPackageUrl(workspace, repo, format, name, version, identifier) {
if (!workspace || !repo || !format || !name || !version || !identifier) {
return null;
}

const packageName = String(name).replaceAll("/", "_");
const encodedPackageName = encodePathSegment(packageName).replaceAll("%40", "@");
return `${WEB_APP_BASE_URL}/${encodePathSegment(workspace)}/${encodePathSegment(repo)}/${encodePathSegment(format)}/${encodedPackageName}/${encodePathSegment(version)}/${encodePathSegment(identifier)}`;
}
Comment thread
DevonL marked this conversation as resolved.

function buildPackageGroupUrl(workspace, repo, name) {
const repositoryUrl = buildRepositoryUrl(workspace, repo);
if (!repositoryUrl || !name) {
return null;
}

const query = encodePathSegment(name);
return `${repositoryUrl}?page=1&query=name:${query}&sort=name`;
}

module.exports = {
WEB_APP_BASE_URL,
buildRepositoryUrl,
buildPackageUrl,
buildPackageGroupUrl,
};
11 changes: 6 additions & 5 deletions views/quarantineExplainProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
const crypto = require("crypto");
const vscode = require("vscode");
const { CloudsmithAPI } = require("../util/cloudsmithAPI");
const { buildPackageUrl } = require("../util/webAppUrls");

class QuarantineExplainProvider {
constructor(context) {
Expand Down Expand Up @@ -43,7 +44,7 @@ class QuarantineExplainProvider {
}

const statusReason = item.status_reason || null;
const selfUrl = item.self_webapp_url || null;
const packageUrl = buildPackageUrl(workspace, repo, format, name, version, slugPerm);

if (!workspace || !slugPerm) {
vscode.window.showWarningMessage("Could not determine package details for quarantine explanation.");
Expand Down Expand Up @@ -82,7 +83,7 @@ class QuarantineExplainProvider {
// Render the full panel
panel.webview.html = this._getHtmlContent(
nonce, name, version, format, workspace, repo, slugPerm,
statusReason, selfUrl, policyTrace
statusReason, packageUrl, policyTrace
);

// Handle messages from the WebView
Expand All @@ -91,8 +92,8 @@ class QuarantineExplainProvider {
vscode.commands.executeCommand("cloudsmith-vsc.findSafeVersion", item);
} else if (message.command === "showVulnerabilities") {
vscode.commands.executeCommand("cloudsmith-vsc.showVulnerabilities", item);
} else if (message.command === "openInCloudsmith" && selfUrl) {
await vscode.env.openExternal(vscode.Uri.parse(selfUrl));
} else if (message.command === "openInCloudsmith" && packageUrl) {
await vscode.env.openExternal(vscode.Uri.parse(packageUrl));
} else if (message.command === "copyReport") {
const report = this._buildPlainTextReport(name, version, statusReason, policyTrace);
await vscode.env.clipboard.writeText(report);
Expand Down Expand Up @@ -175,7 +176,7 @@ class QuarantineExplainProvider {
</html>`;
}

_getHtmlContent(nonce, name, version, format, workspace, repo, slugPerm, statusReason, selfUrl, policyTrace) {
_getHtmlContent(nonce, name, version, format, workspace, repo, slugPerm, statusReason, packageUrl, policyTrace) {
// Determine if this is vulnerability-related by checking for CVE references in decision logs
const hasCVEs = policyTrace.decisionLogs.some(entry =>
(entry.reason && /CVE-/i.test(entry.reason)) ||
Expand Down
Loading