@westbayberry/dg 2.3.3 → 2.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +94 -217
- package/dist/agents/copilot-cli.js +7 -2
- package/dist/agents/persistence.js +83 -8
- package/dist/agents/registry.js +5 -2
- package/dist/agents/routing.js +31 -14
- package/dist/api/analyze.js +138 -41
- package/dist/audit/deep.js +2 -1
- package/dist/audit/detectors.js +23 -2
- package/dist/audit/rules.js +4 -1
- package/dist/audit-ui/export.js +5 -4
- package/dist/auth/device-login.js +33 -11
- package/dist/auth/login-app.js +17 -12
- package/dist/auth/store.js +65 -9
- package/dist/bin/dg.js +1 -1
- package/dist/commands/audit.js +12 -20
- package/dist/commands/{guard-commit.js → commit-guard.js} +12 -12
- package/dist/commands/cooldown.js +2 -1
- package/dist/commands/decisions.js +8 -7
- package/dist/commands/help.js +11 -3
- package/dist/commands/licenses.js +17 -8
- package/dist/commands/login.js +58 -21
- package/dist/commands/router.js +12 -2
- package/dist/commands/sbom.js +20 -5
- package/dist/commands/scan.js +5 -4
- package/dist/commands/service.js +1 -1
- package/dist/commands/setup.js +12 -12
- package/dist/commands/status.js +4 -4
- package/dist/commands/uninstall.js +1 -1
- package/dist/commands/update.js +20 -0
- package/dist/commands/verify.js +4 -4
- package/dist/config/settings.js +81 -12
- package/dist/export-ui/ExportDialog.js +1 -1
- package/dist/gate/cooldown-request.js +22 -0
- package/dist/gate/egress-guard.js +185 -0
- package/dist/gate/host-match.js +48 -0
- package/dist/gate/verdict-core.js +101 -0
- package/dist/gate/verdict-fetch.js +214 -0
- package/dist/launcher/agent-check.js +1096 -42
- package/dist/launcher/agent-hook-exec.js +8 -1
- package/dist/launcher/agent-hook-io.js +5 -3
- package/dist/launcher/classify.js +110 -26
- package/dist/launcher/env.js +84 -18
- package/dist/launcher/install-preflight.js +26 -3
- package/dist/launcher/live-install.js +4 -2
- package/dist/launcher/manifest-screen.js +88 -7
- package/dist/launcher/output-redaction.js +153 -15
- package/dist/launcher/preflight-prompt.js +1 -1
- package/dist/launcher/resolve-real-binary.js +8 -3
- package/dist/launcher/run.js +237 -40
- package/dist/origin/artifact-cache.js +55 -0
- package/dist/origin/cargo-config.js +12 -0
- package/dist/origin/gate-mode.js +7 -0
- package/dist/origin/platform-paths.js +14 -0
- package/dist/origin/precedence.js +345 -0
- package/dist/origin/server.js +611 -0
- package/dist/origin/spike-worker.js +35 -0
- package/dist/origin/worker.js +116 -0
- package/dist/presentation/package-page.js +21 -1
- package/dist/presentation/safe-version.js +11 -0
- package/dist/presentation/sarif.js +109 -0
- package/dist/project/dgfile.js +54 -17
- package/dist/project/override-trust.js +0 -0
- package/dist/proxy/enforcement.js +27 -7
- package/dist/proxy/metadata-map.js +33 -7
- package/dist/proxy/preverified.js +0 -3
- package/dist/proxy/server.js +104 -446
- package/dist/proxy/upstream-proxy.js +23 -4
- package/dist/proxy/worker.js +3 -2
- package/dist/publish-set/collect.js +6 -0
- package/dist/runtime/fatal.js +2 -1
- package/dist/runtime/first-run.js +5 -1
- package/dist/runtime/node-version.js +51 -0
- package/dist/runtime/nudges.js +27 -1
- package/dist/sbom/cyclonedx.js +120 -33
- package/dist/sbom/graph.js +236 -0
- package/dist/sbom/spdx.js +53 -0
- package/dist/sbom-ui/SbomApp.js +5 -3
- package/dist/sbom-ui/inventory.js +8 -10
- package/dist/scan/analyze-worker.js +3 -1
- package/dist/scan/collect.js +38 -8
- package/dist/scan/command.js +77 -27
- package/dist/scan/discovery.js +18 -7
- package/dist/scan/installed.js +202 -0
- package/dist/scan/manifest-coverage.js +128 -0
- package/dist/scan/render.js +125 -40
- package/dist/scan/scanner-report.js +92 -22
- package/dist/scan/staged.js +68 -15
- package/dist/scan/types.js +7 -1
- package/dist/scan-ui/LegacyApp.js +3 -3
- package/dist/scan-ui/components/InteractiveResultsView.js +17 -14
- package/dist/scan-ui/hooks/useScan.js +37 -11
- package/dist/scan-ui/launch.js +2 -2
- package/dist/scan-ui/logo.js +6 -2
- package/dist/scan-ui/shims.js +6 -6
- package/dist/scripts/gate.js +53 -35
- package/dist/security/csv.js +9 -0
- package/dist/security/sanitize.js +11 -2
- package/dist/service/state.js +120 -37
- package/dist/service/trust-refresh.js +12 -1
- package/dist/service/trust-store.js +36 -5
- package/dist/service/worker.js +5 -7
- package/dist/setup/git-hook.js +2 -2
- package/dist/setup/install-method.js +36 -0
- package/dist/setup/plan.js +119 -57
- package/dist/setup/uninstall-standalone.js +7 -2
- package/dist/setup-ui/gate.js +1 -8
- package/dist/setup-ui/offer.js +14 -7
- package/dist/setup-ui/selector.js +1 -1
- package/dist/setup-ui/wizard.js +73 -78
- package/dist/standalone/uninstall.mjs +1114 -386
- package/dist/state/cleanup-registry.js +14 -3
- package/dist/state/cooldown-held.js +36 -13
- package/dist/state/locks.js +239 -34
- package/dist/state/sessions.js +2 -2
- package/dist/util/sh-escape.js +6 -0
- package/dist/util/tty-prompt.js +13 -4
- package/dist/verify/local.js +60 -10
- package/dist/verify/package-check.js +35 -49
- package/dist/verify/preflight.js +59 -33
- package/dist/verify/render.js +11 -12
- package/npm-shrinkwrap.json +714 -213
- package/package.json +3 -2
package/dist/auth/store.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { loadUserConfig, parseUrl, saveUserConfig, withUserConfigLock } from "../config/settings.js";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { assertVendorApiBaseUrl, loadUserConfig, parseUrl, saveUserConfig, withUserConfigLock } from "../config/settings.js";
|
|
5
5
|
import { resolveDgPaths } from "../state/index.js";
|
|
6
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
6
7
|
import { envAuthToken } from "./env-token.js";
|
|
7
8
|
export class AuthError extends Error {
|
|
8
9
|
constructor(message) {
|
|
@@ -13,31 +14,62 @@ export class AuthError extends Error {
|
|
|
13
14
|
export function authPath(paths) {
|
|
14
15
|
return join(paths.configDir, "auth.json");
|
|
15
16
|
}
|
|
17
|
+
// The bearer token is written 0600, so any group/other access bit means the file
|
|
18
|
+
// was loosened externally and the token must be treated as potentially exposed.
|
|
19
|
+
// Restore 0600 to close the leak; if it cannot be secured, refuse to trust it.
|
|
20
|
+
function enforceAuthFilePerms(path) {
|
|
21
|
+
let mode;
|
|
22
|
+
try {
|
|
23
|
+
mode = statSync(path).mode & 0o777;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if ((mode & 0o077) === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
chmodSync(path, 0o600);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
throw new AuthError(`auth state at ${path} is group- or world-accessible and could not be secured; fix its permissions (chmod 600) or run 'dg login' again`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
16
38
|
export function readAuthState(env = process.env) {
|
|
17
39
|
const paths = resolveDgPaths(env);
|
|
18
40
|
const path = authPath(paths);
|
|
19
41
|
if (!existsSync(path)) {
|
|
20
42
|
return undefined;
|
|
21
43
|
}
|
|
44
|
+
enforceAuthFilePerms(path);
|
|
22
45
|
try {
|
|
23
46
|
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
24
47
|
if (parsed.version !== 1 || !parsed.token || !parsed.apiBaseUrl || parsed.loggedInAt === undefined) {
|
|
25
48
|
throw new AuthError("unsupported auth state");
|
|
26
49
|
}
|
|
50
|
+
let allowNonVendor = false;
|
|
51
|
+
try {
|
|
52
|
+
allowNonVendor = loadUserConfig(env).policy.allowNonVendorApiHost;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
allowNonVendor = false;
|
|
56
|
+
}
|
|
27
57
|
let apiBaseUrl;
|
|
28
58
|
try {
|
|
29
|
-
apiBaseUrl = parseUrl(parsed.apiBaseUrl);
|
|
59
|
+
apiBaseUrl = assertVendorApiBaseUrl(parseUrl(parsed.apiBaseUrl), allowNonVendor);
|
|
30
60
|
}
|
|
31
61
|
catch {
|
|
32
|
-
throw new AuthError("auth state has an invalid api base URL; run 'dg login' again");
|
|
62
|
+
throw new AuthError("auth state has an invalid or non-vendor api base URL; run 'dg login' again");
|
|
33
63
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
64
|
+
// email / tier / name / tokenPreview are read from the auth file and rendered
|
|
65
|
+
// by `dg status`; a tampered file must not inject ANSI/control sequences.
|
|
66
|
+
const email = typeof parsed.email === "string" && parsed.email.length > 0 ? sanitizeLine(parsed.email) : undefined;
|
|
67
|
+
const tier = typeof parsed.tier === "string" && parsed.tier.length > 0 ? sanitizeLine(parsed.tier) : undefined;
|
|
68
|
+
const name = typeof parsed.name === "string" && parsed.name.length > 0 ? sanitizeLine(parsed.name) : undefined;
|
|
37
69
|
return {
|
|
38
70
|
version: 1,
|
|
39
71
|
token: parsed.token,
|
|
40
|
-
tokenPreview: parsed.tokenPreview ?? redactToken(parsed.token),
|
|
72
|
+
tokenPreview: sanitizeLine(parsed.tokenPreview ?? redactToken(parsed.token)),
|
|
41
73
|
apiBaseUrl,
|
|
42
74
|
orgId: parsed.orgId ?? "",
|
|
43
75
|
loggedInAt: parsed.loggedInAt,
|
|
@@ -104,6 +136,7 @@ export function writeAuthState(options, env = process.env) {
|
|
|
104
136
|
}
|
|
105
137
|
export function clearAuthState(env = process.env) {
|
|
106
138
|
const path = authPath(resolveDgPaths(env));
|
|
139
|
+
sweepOrphanAuthTemps(path);
|
|
107
140
|
if (!existsSync(path)) {
|
|
108
141
|
return false;
|
|
109
142
|
}
|
|
@@ -153,6 +186,29 @@ export function redactToken(token) {
|
|
|
153
186
|
}
|
|
154
187
|
return `${trimmed.slice(0, 4)}...${trimmed.slice(-4)}`;
|
|
155
188
|
}
|
|
189
|
+
// A SIGKILL between the temp write and the rename in writeJsonAtomic orphans a
|
|
190
|
+
// temp file that still holds the full token. Sweep them on logout (and before the
|
|
191
|
+
// canonical file is removed) so a credential is not left behind on disk.
|
|
192
|
+
function sweepOrphanAuthTemps(path) {
|
|
193
|
+
const dir = dirname(path);
|
|
194
|
+
const base = basename(path);
|
|
195
|
+
const pattern = new RegExp(`^${base.replace(/[.]/g, "\\.")}\\.\\d+\\.[0-9a-f-]{36}\\.tmp$`);
|
|
196
|
+
try {
|
|
197
|
+
for (const entry of readdirSync(dir)) {
|
|
198
|
+
if (pattern.test(entry)) {
|
|
199
|
+
try {
|
|
200
|
+
unlinkSync(join(dir, entry));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// a concurrent writer's temp, or already gone — skip
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// configDir missing — nothing to sweep
|
|
210
|
+
}
|
|
211
|
+
}
|
|
156
212
|
function writeJsonAtomic(path, value) {
|
|
157
213
|
mkdirSync(dirname(path), {
|
|
158
214
|
recursive: true,
|
package/dist/bin/dg.js
CHANGED
|
@@ -8,7 +8,7 @@ process.on("uncaughtException", (error) => {
|
|
|
8
8
|
}
|
|
9
9
|
exitOnFatal(error);
|
|
10
10
|
});
|
|
11
|
-
process.on("unhandledRejection", exitOnFatal);
|
|
11
|
+
process.on("unhandledRejection", (reason) => exitOnFatal(reason));
|
|
12
12
|
process.stdout.on("error", (error) => {
|
|
13
13
|
if (error.code === "EPIPE") {
|
|
14
14
|
process.exit(process.exitCode ?? 0);
|
package/dist/commands/audit.js
CHANGED
|
@@ -4,8 +4,9 @@ import { writeReportAtomic } from "../util/report-writer.js";
|
|
|
4
4
|
import { EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
|
|
5
5
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
6
6
|
import { createTheme } from "../presentation/theme.js";
|
|
7
|
-
import { actionForFindings, detectFindings, findingLocation } from "../audit/detectors.js";
|
|
7
|
+
import { actionForFindings, detectFindings, findingEvidenceSafe, findingLocation, findingTitleSafe } from "../audit/detectors.js";
|
|
8
8
|
import { buildAuditFile } from "../publish-set/collect.js";
|
|
9
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
9
10
|
import { npmPublishSet } from "../publish-set/npm.js";
|
|
10
11
|
import { pypiPublishSet } from "../publish-set/pypi.js";
|
|
11
12
|
import { deepDecision, runDeepUpload, consentGiven, deepSummary, teamPolicyBlocksUpload } from "../audit/deep.js";
|
|
@@ -33,11 +34,11 @@ export const auditCommand = {
|
|
|
33
34
|
"Audits exactly what you're about to publish — the resolved publish set of one package, never the whole repo.",
|
|
34
35
|
"Basic checks run 100% locally and never upload anything. If you're on a paid plan (and your org allows it), it also runs a deep behavioral scan of your package on the scanner; raw bytes are never retained. Exit codes: 0 clean (warn counts as clean under the default --fail-on block), 1 warn with --fail-on warn, 2 block, 3 deep audit required but unavailable (--require-deep), 4 analysis incomplete, 64 usage error."
|
|
35
36
|
],
|
|
36
|
-
handler: (context) =>
|
|
37
|
+
handler: async (context) => (await maybeAudit(["audit", ...context.args])).result
|
|
37
38
|
};
|
|
38
39
|
function privatePackageResult(scope) {
|
|
39
40
|
const theme = createTheme(resolvePresentation().color);
|
|
40
|
-
const name = typeof scope.packageJson?.name === "string" ? scope.packageJson.name : basename(scope.root);
|
|
41
|
+
const name = sanitizeLine(typeof scope.packageJson?.name === "string" ? scope.packageJson.name : basename(scope.root));
|
|
41
42
|
return {
|
|
42
43
|
exitCode: 0,
|
|
43
44
|
stdout: `${theme.paint("pass", "✓")} ${name} is private — npm refuses to publish it, so there is nothing to audit ${theme.paint("muted", "(remove \"private\": true to audit it for publish)")}\n`,
|
|
@@ -97,17 +98,6 @@ function finalize(gathered, deep) {
|
|
|
97
98
|
function gatherError(gathered) {
|
|
98
99
|
return gathered.usage ? usageError(gathered.error) : { exitCode: EXIT_USAGE_VERDICT, stdout: "", stderr: `dg audit: ${gathered.error}.\n` };
|
|
99
100
|
}
|
|
100
|
-
function runAuditCommand(args) {
|
|
101
|
-
const gathered = gather(args);
|
|
102
|
-
if ("error" in gathered) {
|
|
103
|
-
return gatherError(gathered);
|
|
104
|
-
}
|
|
105
|
-
if ("privatePackage" in gathered) {
|
|
106
|
-
return privatePackageResult(gathered.privatePackage);
|
|
107
|
-
}
|
|
108
|
-
const decision = deepDecision(gathered.scope, gathered.parsed.local);
|
|
109
|
-
return finalize(gathered, { ran: false, reason: decision.reason });
|
|
110
|
-
}
|
|
111
101
|
export async function maybeAudit(args) {
|
|
112
102
|
if (args[0] !== "audit") {
|
|
113
103
|
return { handled: false, result: { exitCode: 0, stdout: "", stderr: "" } };
|
|
@@ -234,15 +224,17 @@ function resolveScope(target) {
|
|
|
234
224
|
const packageJson = safeReadJson(resolve(root, "package.json"));
|
|
235
225
|
const name = packageJson && typeof packageJson.name === "string" ? packageJson.name : basename(root);
|
|
236
226
|
const version = packageJson && typeof packageJson.version === "string" ? packageJson.version : "(unknown)";
|
|
237
|
-
|
|
227
|
+
// name/version are free-text from a (possibly hostile) package.json rendered
|
|
228
|
+
// into the header and report; strip control bytes so they cannot inject ANSI.
|
|
229
|
+
return { root, ecosystem: "npm", packageJson, artifact: sanitizeLine(`${name}@${version}`) };
|
|
238
230
|
}
|
|
239
231
|
if (existsSync(resolve(root, "pyproject.toml")) || existsSync(resolve(root, "setup.py")) || existsSync(resolve(root, "setup.cfg"))) {
|
|
240
|
-
return { root, ecosystem: "pypi", packageJson: null, artifact: basename(root) };
|
|
232
|
+
return { root, ecosystem: "pypi", packageJson: null, artifact: sanitizeLine(basename(root)) };
|
|
241
233
|
}
|
|
242
234
|
if (existsSync(resolve(root, "Cargo.toml"))) {
|
|
243
|
-
return { root, ecosystem: "cargo", packageJson: null, artifact: basename(root) };
|
|
235
|
+
return { root, ecosystem: "cargo", packageJson: null, artifact: sanitizeLine(basename(root)) };
|
|
244
236
|
}
|
|
245
|
-
return { root, ecosystem: "unknown", packageJson: null, artifact: basename(root) };
|
|
237
|
+
return { root, ecosystem: "unknown", packageJson: null, artifact: sanitizeLine(basename(root)) };
|
|
246
238
|
}
|
|
247
239
|
function findPackageRoot(start) {
|
|
248
240
|
let current = start;
|
|
@@ -292,9 +284,9 @@ function renderReport(report, theme) {
|
|
|
292
284
|
? theme.paint("warn", WARN_GLYPH)
|
|
293
285
|
: theme.paint("muted", "·");
|
|
294
286
|
lines.push(` ${tag} ${accent(findingLocation(finding))}`);
|
|
295
|
-
lines.push(` ${finding
|
|
287
|
+
lines.push(` ${findingTitleSafe(finding)}`);
|
|
296
288
|
if (finding.evidence && finding.evidence !== `path: ${finding.location}` && finding.evidence !== finding.location) {
|
|
297
|
-
lines.push(` ${muted(finding
|
|
289
|
+
lines.push(` ${muted(findingEvidenceSafe(finding))}`);
|
|
298
290
|
}
|
|
299
291
|
lines.push(` ${accent("→")} ${finding.recommendation}`);
|
|
300
292
|
lines.push("");
|
|
@@ -3,17 +3,17 @@ import { resolvePresentation } from "../presentation/mode.js";
|
|
|
3
3
|
import { createTheme } from "../presentation/theme.js";
|
|
4
4
|
import { promptYesNo } from "../util/tty-prompt.js";
|
|
5
5
|
import { applyGitHook, gitHookState, planGitHook, removeGitHookForRepo, resolveGitRepo, verifyGitHook } from "../setup/git-hook.js";
|
|
6
|
-
export const
|
|
7
|
-
name: "guard
|
|
6
|
+
export const commitGuardCommand = {
|
|
7
|
+
name: "commit-guard",
|
|
8
8
|
summary: "Scan staged dependencies before every commit in this repo.",
|
|
9
|
-
usage: "dg guard
|
|
9
|
+
usage: "dg commit-guard [off | --check | --print] [--yes]",
|
|
10
10
|
args: [{ name: "[off]", summary: "Remove the hook and restore any hook it chained (alias remove/uninstall)." }],
|
|
11
11
|
flags: [
|
|
12
12
|
{ flag: "--check", summary: "Verify the hook is installed and will actually fire." },
|
|
13
13
|
{ flag: "--print", summary: "Preview what it will write, change nothing." },
|
|
14
14
|
{ flag: "--yes", summary: "Install without the confirmation prompt (alias -y)." }
|
|
15
15
|
],
|
|
16
|
-
examples: ["dg guard
|
|
16
|
+
examples: ["dg commit-guard", "dg commit-guard --check", "dg commit-guard off"],
|
|
17
17
|
details: [
|
|
18
18
|
"Installs a git pre-commit hook that scans staged lockfile changes and blocks a commit that would add a malicious dependency. Override a block with 'git commit --no-verify'.",
|
|
19
19
|
"It resolves the real hooks directory (honouring core.hooksPath/husky and worktrees) and chains any existing pre-commit hook. Tune behaviour with the gitHook.onWarn / gitHook.onIncomplete config keys."
|
|
@@ -53,13 +53,13 @@ function run(context) {
|
|
|
53
53
|
return {
|
|
54
54
|
exitCode: EXIT_USAGE,
|
|
55
55
|
stdout: "",
|
|
56
|
-
stderr: `dg guard
|
|
56
|
+
stderr: `dg commit-guard: ${parsed.error}. Usage: ${commitGuardCommand.usage}\n`
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
const theme = createTheme(resolvePresentation().color);
|
|
60
60
|
const repo = resolveGitRepo();
|
|
61
61
|
if ("error" in repo) {
|
|
62
|
-
return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg guard
|
|
62
|
+
return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg commit-guard: ${repo.error}.\n` };
|
|
63
63
|
}
|
|
64
64
|
if (parsed.off) {
|
|
65
65
|
return remove(repo, theme);
|
|
@@ -82,7 +82,7 @@ function install(repo, theme, yes) {
|
|
|
82
82
|
if (plan.willChain) {
|
|
83
83
|
err.write(` ${muted("Your existing pre-commit hook will be kept and run after dg.")}\n`);
|
|
84
84
|
}
|
|
85
|
-
err.write(` ${muted("Reversible with")} ${accent("dg guard
|
|
85
|
+
err.write(` ${muted("Reversible with")} ${accent("dg commit-guard off")}${muted(".")}\n\n`);
|
|
86
86
|
const proceed = promptYesNo(` ${accent("Proceed?")}`, true);
|
|
87
87
|
if (proceed === false) {
|
|
88
88
|
return { exitCode: 0, stdout: "", stderr: ` ${muted("cancelled — nothing written")}\n` };
|
|
@@ -107,7 +107,7 @@ function check(repo, theme) {
|
|
|
107
107
|
return {
|
|
108
108
|
exitCode: 1,
|
|
109
109
|
stdout: "",
|
|
110
|
-
stderr: ` ${theme.paint("warn", "⚠")} not set up in this repo — run ${theme.paint("accent", "dg guard
|
|
110
|
+
stderr: ` ${theme.paint("warn", "⚠")} not set up in this repo — run ${theme.paint("accent", "dg commit-guard")}\n`
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
const checks = verifyGitHook(repo);
|
|
@@ -120,9 +120,9 @@ function check(repo, theme) {
|
|
|
120
120
|
function print(repo) {
|
|
121
121
|
const plan = planGitHook(repo);
|
|
122
122
|
const lines = [
|
|
123
|
-
"dg guard
|
|
123
|
+
"dg commit-guard write plan",
|
|
124
124
|
"",
|
|
125
|
-
"No files are changed until you run dg guard
|
|
125
|
+
"No files are changed until you run dg commit-guard.",
|
|
126
126
|
`- write pre-commit hook: ${plan.context.hookTarget}`
|
|
127
127
|
];
|
|
128
128
|
if (plan.willChain) {
|
|
@@ -131,7 +131,7 @@ function print(repo) {
|
|
|
131
131
|
else if (plan.state === "managed") {
|
|
132
132
|
lines.push("- (a dg hook is already installed; it will be refreshed)");
|
|
133
133
|
}
|
|
134
|
-
lines.push("- record it for 'dg guard
|
|
134
|
+
lines.push("- record it for 'dg commit-guard off' / 'dg uninstall'");
|
|
135
135
|
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
136
136
|
}
|
|
137
137
|
function remove(repo, theme) {
|
|
@@ -140,7 +140,7 @@ function remove(repo, theme) {
|
|
|
140
140
|
return {
|
|
141
141
|
exitCode: 0,
|
|
142
142
|
stdout: "",
|
|
143
|
-
stderr: ` ${theme.paint("muted", "no dg guard
|
|
143
|
+
stderr: ` ${theme.paint("muted", "no dg commit-guard hook in this repo")}\n`
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
const restored = result.removed.some((path) => path.includes(".dg-chained-"));
|
|
@@ -4,6 +4,7 @@ import { canonicalCooldownName } from "../policy/pypi-name.js";
|
|
|
4
4
|
import { ConfigError, loadUserConfig, setConfigValue, updateUserConfig } from "../config/settings.js";
|
|
5
5
|
import { appendCooldownExemptions, cooldownExemptionActive, CooldownExemptionCapError, findProjectRoot, loadDgFile, mutateDgFile, removeCooldownExemptions, resolveAcceptedBy } from "../project/dgfile.js";
|
|
6
6
|
import { EXIT_USAGE } from "./types.js";
|
|
7
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
7
8
|
export const cooldownCommand = {
|
|
8
9
|
name: "cooldown",
|
|
9
10
|
summary: "Hold new releases for a window before they can install, with per-package exemptions.",
|
|
@@ -342,7 +343,7 @@ function listExemptions(cwd, env, json, now) {
|
|
|
342
343
|
}
|
|
343
344
|
const rows = file.cooldownExemptions.map((e) => [
|
|
344
345
|
`${e.ecosystem}:${e.name}`,
|
|
345
|
-
e.reason || "-",
|
|
346
|
+
sanitizeLine(e.reason) || "-",
|
|
346
347
|
e.acceptedBy || "-",
|
|
347
348
|
e.acceptedAt.slice(0, 10) || "-",
|
|
348
349
|
e.expiresAt ? formatExpiryDisplay(e.expiresAt, e.acceptedAt) : "-",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recordDecisionEvents } from "../decisions/remember-prompt.js";
|
|
2
2
|
import { packageKey } from "../decisions/apply.js";
|
|
3
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
3
4
|
import { findProjectRoot, loadDgFile, mutateDgFile, removeDecisions } from "../project/dgfile.js";
|
|
4
5
|
import { EXIT_USAGE } from "./types.js";
|
|
5
6
|
export const decisionsCommand = {
|
|
@@ -63,11 +64,11 @@ function listDecisions(cwd, env, json) {
|
|
|
63
64
|
}
|
|
64
65
|
const rows = file.decisions.map((entry) => [
|
|
65
66
|
entry.id.slice(0, 8),
|
|
66
|
-
`${entry.ecosystem}:${entry.name}@${scopeLabel(entry)}`,
|
|
67
|
+
`${entry.ecosystem}:${sanitizeLine(entry.name)}@${scopeLabel(entry)}`,
|
|
67
68
|
findingsLabel(entry),
|
|
68
|
-
entry.acceptedBy,
|
|
69
|
-
entry.acceptedAt.slice(0, 10) || "-",
|
|
70
|
-
entry.expiresAt ? entry.expiresAt.slice(0, 10) : "-",
|
|
69
|
+
sanitizeLine(entry.acceptedBy),
|
|
70
|
+
sanitizeLine(entry.acceptedAt).slice(0, 10) || "-",
|
|
71
|
+
entry.expiresAt ? sanitizeLine(entry.expiresAt).slice(0, 10) : "-",
|
|
71
72
|
isExpired(entry) ? "expired" : "active"
|
|
72
73
|
]);
|
|
73
74
|
const header = ["ID", "PACKAGE", "ACCEPTED FINDINGS", "BY", "WHEN", "EXPIRES", "STATUS"];
|
|
@@ -103,7 +104,7 @@ function revokeDecisions(selector, cwd, env) {
|
|
|
103
104
|
return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing matches '${selector}' in ${located.file.path}.\n` };
|
|
104
105
|
}
|
|
105
106
|
recordDecisionEvents("decision.revoked", matched.map((entry) => `${entry.ecosystem}:${packageKey(entry.name, scopeLabel(entry))}`), `revoked via dg decisions (${selector})`, env);
|
|
106
|
-
const lines = matched.map((entry) => `Revoked ${entry.ecosystem}:${entry.name}@${scopeLabel(entry)} (${entry.id.slice(0, 8)}) — the warn will surface again.`);
|
|
107
|
+
const lines = matched.map((entry) => `Revoked ${entry.ecosystem}:${sanitizeLine(entry.name)}@${scopeLabel(entry)} (${entry.id.slice(0, 8)}) — the warn will surface again.`);
|
|
107
108
|
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
108
109
|
}
|
|
109
110
|
function locateDgFile(cwd, env) {
|
|
@@ -133,10 +134,10 @@ function matchesSelector(entry, selector) {
|
|
|
133
134
|
return entry.name === selector;
|
|
134
135
|
}
|
|
135
136
|
function scopeLabel(entry) {
|
|
136
|
-
return entry.scope.kind === "exact" ? entry.scope.version : "*";
|
|
137
|
+
return entry.scope.kind === "exact" ? sanitizeLine(entry.scope.version) : "*";
|
|
137
138
|
}
|
|
138
139
|
function findingsLabel(entry) {
|
|
139
|
-
const pairs = Object.entries(entry.findings).map(([category, severity]) => `${category}:${severity}`);
|
|
140
|
+
const pairs = Object.entries(entry.findings).map(([category, severity]) => `${sanitizeLine(category)}:${severity}`);
|
|
140
141
|
return pairs.length > 0 ? pairs.sort().join(",") : "(action-only warn)";
|
|
141
142
|
}
|
|
142
143
|
function isExpired(entry) {
|
package/dist/commands/help.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { optionalPackageManagerNames } from "../setup/optional-support.js";
|
|
1
2
|
const PRODUCT_DESCRIPTION = "Dependency Guardian";
|
|
2
|
-
const COMMON_COMMANDS = ["scan", "verify", "audit", "licenses", "setup", "guard
|
|
3
|
-
const GLOBAL_FLAGS_FOOTER = "Global: --help/-h · --version/-v
|
|
3
|
+
const COMMON_COMMANDS = ["scan", "verify", "audit", "licenses", "setup", "commit-guard", "doctor", "login"];
|
|
4
|
+
const GLOBAL_FLAGS_FOOTER = "Global: --help/-h · --version/-v";
|
|
4
5
|
function isWrapper(command) {
|
|
5
6
|
return command.summary.includes("prefix-mode routing");
|
|
6
7
|
}
|
|
@@ -37,7 +38,14 @@ export function renderRootHelp(version, commands, options = {}) {
|
|
|
37
38
|
for (const command of standalone) {
|
|
38
39
|
lines.push(` ${command.name.padEnd(14)} ${command.summary}`);
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
const gated = new Set(optionalPackageManagerNames());
|
|
42
|
+
const wrapperNames = wrappers.map((command) => command.name);
|
|
43
|
+
const claimed = wrapperNames.filter((name) => !gated.has(name));
|
|
44
|
+
const gatedNames = wrapperNames.filter((name) => gated.has(name));
|
|
45
|
+
lines.push("", "Package-manager prefix mode (run any through dg):", ` ${claimed.join(", ")}`);
|
|
46
|
+
if (gatedNames.length > 0) {
|
|
47
|
+
lines.push("", "Gated (refuse with exit 69 until their support gate ships):", ` ${gatedNames.join(", ")}`);
|
|
48
|
+
}
|
|
41
49
|
lines.push("", "Run 'dg <command> --help' for that command's flags and examples.", GLOBAL_FLAGS_FOOTER);
|
|
42
50
|
return `${lines.join("\n")}\n`;
|
|
43
51
|
}
|
|
@@ -4,7 +4,9 @@ import { writeReportAtomic } from "../util/report-writer.js";
|
|
|
4
4
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { scanProject } from "../scan/discovery.js";
|
|
6
6
|
import { isSupportedLockfilePath, verifyLockfile } from "../verify/preflight.js";
|
|
7
|
-
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_USAGE_VERDICT } from "./types.js";
|
|
7
|
+
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
|
|
8
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
9
|
+
import { csvCell } from "../security/csv.js";
|
|
8
10
|
const LICENSE_RISKS = [
|
|
9
11
|
"network-copyleft",
|
|
10
12
|
"no-license",
|
|
@@ -40,7 +42,7 @@ export const licensesCommand = {
|
|
|
40
42
|
examples: ["dg licenses", "dg licenses --markdown -o licenses.md", "dg licenses --fail-on strong-copyleft,network-copyleft"],
|
|
41
43
|
details: [
|
|
42
44
|
"Reports project and lockfile license metadata without running package managers or package code.",
|
|
43
|
-
"License exports support text, JSON, CSV, and Markdown. Exit codes: 0 pass, 2 policy block, 4
|
|
45
|
+
"License exports support text, JSON, CSV, and Markdown. Exit codes: 0 pass, 2 policy block, 4 analysis incomplete, 64 usage error, 70 write error."
|
|
44
46
|
],
|
|
45
47
|
handler: (context) => runLicensesCommand(context.args)
|
|
46
48
|
};
|
|
@@ -92,7 +94,7 @@ async function runLicensesCommand(args) {
|
|
|
92
94
|
}
|
|
93
95
|
catch (error) {
|
|
94
96
|
return {
|
|
95
|
-
exitCode:
|
|
97
|
+
exitCode: EXIT_TOOL_ERROR,
|
|
96
98
|
stdout: "",
|
|
97
99
|
stderr: `dg licenses could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
98
100
|
};
|
|
@@ -406,10 +408,20 @@ function dedupeEntries(entries) {
|
|
|
406
408
|
const seen = new Set();
|
|
407
409
|
const deduped = [];
|
|
408
410
|
for (const entry of entries) {
|
|
409
|
-
|
|
411
|
+
// name / version / license / location come from untrusted lockfiles &
|
|
412
|
+
// manifests and are rendered to the terminal and md/csv exports — strip
|
|
413
|
+
// control bytes here, the single point every entry passes through.
|
|
414
|
+
const clean = {
|
|
415
|
+
...entry,
|
|
416
|
+
name: sanitizeLine(entry.name),
|
|
417
|
+
...(entry.version != null ? { version: sanitizeLine(entry.version) } : {}),
|
|
418
|
+
...(entry.license != null ? { license: sanitizeLine(entry.license) } : {}),
|
|
419
|
+
location: sanitizeLine(entry.location)
|
|
420
|
+
};
|
|
421
|
+
const key = `${clean.ecosystem}|${clean.name}|${clean.version ?? ""}|${clean.license ?? ""}|${clean.source}|${clean.location}`;
|
|
410
422
|
if (!seen.has(key)) {
|
|
411
423
|
seen.add(key);
|
|
412
|
-
deduped.push(
|
|
424
|
+
deduped.push(clean);
|
|
413
425
|
}
|
|
414
426
|
}
|
|
415
427
|
return deduped.sort((left, right) => `${left.ecosystem}:${left.name}`.localeCompare(`${right.ecosystem}:${right.name}`));
|
|
@@ -428,9 +440,6 @@ function displayPath(root, path) {
|
|
|
428
440
|
const display = relativePath.length === 0 ? "." : relativePath;
|
|
429
441
|
return display.split(sep).join("/");
|
|
430
442
|
}
|
|
431
|
-
function csvCell(value) {
|
|
432
|
-
return /[",\n\r]/u.test(value) ? `"${value.replace(/"/gu, "\"\"")}"` : value;
|
|
433
|
-
}
|
|
434
443
|
function markdownCell(value) {
|
|
435
444
|
return value.replace(/\|/gu, "\\|");
|
|
436
445
|
}
|
package/dist/commands/login.js
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
1
2
|
import { EXIT_TOOL_ERROR, EXIT_USAGE } from "./types.js";
|
|
2
3
|
import { AuthError, writeAuthState } from "../auth/store.js";
|
|
3
4
|
import { ConfigError, loadUserConfig } from "../config/settings.js";
|
|
5
|
+
import { envAuthToken } from "../auth/env-token.js";
|
|
4
6
|
export const loginCommand = {
|
|
5
7
|
name: "login",
|
|
6
8
|
summary: "Authenticate this machine with Dependency Guardian.",
|
|
7
|
-
usage: "dg login [--token <token>]",
|
|
9
|
+
usage: "dg login [--token <token> | --token-stdin]",
|
|
8
10
|
flags: [
|
|
9
|
-
{ flag: "--token", value: "<token>", summary: "Authenticate with an API key instead of the browser (for CI/headless)." }
|
|
11
|
+
{ flag: "--token", value: "<token>", summary: "Authenticate with an API key instead of the browser (for CI/headless)." },
|
|
12
|
+
{ flag: "--token-stdin", summary: "Read the API key from stdin, keeping it out of the process list." }
|
|
10
13
|
],
|
|
11
|
-
examples: ["dg login", "dg login --token
|
|
14
|
+
examples: ["dg login", "dg login --token-stdin", "DG_API_TOKEN=dg_live_… dg login"],
|
|
12
15
|
details: [
|
|
13
16
|
"In a terminal, 'dg login' opens your browser to sign in — no token to copy.",
|
|
14
|
-
"For CI and headless shells,
|
|
17
|
+
"For CI and headless shells, set DG_API_TOKEN (or DG_API_KEY) before running 'dg login', pipe the key to --token-stdin, or pass --token <key>.",
|
|
18
|
+
"Prefer the environment variable or --token-stdin: a key passed as --token <key> is visible to other users in the process list.",
|
|
15
19
|
"Stores dg-owned auth state under the user config directory; never executes project-local code or weakens install enforcement."
|
|
16
20
|
],
|
|
17
21
|
handler: (context) => loginHandler(context.args)
|
|
18
22
|
};
|
|
19
|
-
function loginHandler(args) {
|
|
23
|
+
export function loginHandler(args, env = process.env, readStdin = readStdinSync) {
|
|
20
24
|
const parsed = parseLoginArgs(args);
|
|
21
25
|
if ("error" in parsed) {
|
|
22
26
|
return {
|
|
@@ -25,17 +29,25 @@ function loginHandler(args) {
|
|
|
25
29
|
stderr: `dg login: ${parsed.error}. Run 'dg login --help'.\n`
|
|
26
30
|
};
|
|
27
31
|
}
|
|
32
|
+
const resolved = resolveToken(parsed, env, readStdin);
|
|
33
|
+
if ("error" in resolved) {
|
|
34
|
+
return {
|
|
35
|
+
exitCode: EXIT_USAGE,
|
|
36
|
+
stdout: "",
|
|
37
|
+
stderr: `dg login: ${resolved.error}. Run 'dg login --help'.\n`
|
|
38
|
+
};
|
|
39
|
+
}
|
|
28
40
|
try {
|
|
29
|
-
const config = loadUserConfig();
|
|
41
|
+
const config = loadUserConfig(env);
|
|
30
42
|
const state = writeAuthState({
|
|
31
|
-
token:
|
|
43
|
+
token: resolved.token,
|
|
32
44
|
apiBaseUrl: config.api.baseUrl,
|
|
33
45
|
orgId: config.org.id
|
|
34
|
-
});
|
|
46
|
+
}, env);
|
|
35
47
|
return {
|
|
36
48
|
exitCode: 0,
|
|
37
49
|
stdout: `Logged in to ${state.apiBaseUrl}${state.orgId ? ` for org ${state.orgId}` : ""} with token ${state.tokenPreview}\n`,
|
|
38
|
-
stderr: ""
|
|
50
|
+
stderr: resolved.source === "env" ? "Using the API key from DG_API_TOKEN/DG_API_KEY in the environment.\n" : ""
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
53
|
catch (error) {
|
|
@@ -53,16 +65,34 @@ function loginHandler(args) {
|
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
67
|
}
|
|
68
|
+
function resolveToken(parsed, env, readStdin) {
|
|
69
|
+
if (parsed.fromStdin) {
|
|
70
|
+
const token = readStdin().trim();
|
|
71
|
+
if (!token) {
|
|
72
|
+
return { error: "no API key was provided on stdin" };
|
|
73
|
+
}
|
|
74
|
+
return { token, source: "stdin" };
|
|
75
|
+
}
|
|
76
|
+
if (parsed.token !== undefined) {
|
|
77
|
+
return { token: parsed.token, source: "flag" };
|
|
78
|
+
}
|
|
79
|
+
const fromEnv = envAuthToken(env);
|
|
80
|
+
if (fromEnv) {
|
|
81
|
+
return { token: fromEnv, source: "env" };
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
error: "run 'dg login' in a terminal to sign in via your browser, or set DG_API_TOKEN / pipe the key to 'dg login --token-stdin' / pass 'dg login --token <key>' for CI"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
56
87
|
function parseLoginArgs(args) {
|
|
57
|
-
let token
|
|
88
|
+
let token;
|
|
89
|
+
let fromStdin = false;
|
|
58
90
|
for (let index = 0; index < args.length; index += 1) {
|
|
59
91
|
const arg = args[index];
|
|
60
92
|
if (arg === "--token") {
|
|
61
93
|
const value = args[index + 1];
|
|
62
94
|
if (!value) {
|
|
63
|
-
return {
|
|
64
|
-
error: "--token requires a value"
|
|
65
|
-
};
|
|
95
|
+
return { error: "--token requires a value" };
|
|
66
96
|
}
|
|
67
97
|
token = value;
|
|
68
98
|
index += 1;
|
|
@@ -70,10 +100,11 @@ function parseLoginArgs(args) {
|
|
|
70
100
|
else if (arg?.startsWith("--token=")) {
|
|
71
101
|
token = arg.slice("--token=".length);
|
|
72
102
|
}
|
|
103
|
+
else if (arg === "--token-stdin") {
|
|
104
|
+
fromStdin = true;
|
|
105
|
+
}
|
|
73
106
|
else if (arg?.startsWith("-")) {
|
|
74
|
-
return {
|
|
75
|
-
error: `unknown option '${arg}'`
|
|
76
|
-
};
|
|
107
|
+
return { error: `unknown option '${arg}'` };
|
|
77
108
|
}
|
|
78
109
|
else {
|
|
79
110
|
return {
|
|
@@ -81,10 +112,16 @@ function parseLoginArgs(args) {
|
|
|
81
112
|
};
|
|
82
113
|
}
|
|
83
114
|
}
|
|
84
|
-
if (
|
|
85
|
-
return {
|
|
86
|
-
|
|
87
|
-
|
|
115
|
+
if (token !== undefined && fromStdin) {
|
|
116
|
+
return { error: "--token and --token-stdin cannot be combined" };
|
|
117
|
+
}
|
|
118
|
+
return fromStdin ? { fromStdin: true } : token !== undefined ? { token } : {};
|
|
119
|
+
}
|
|
120
|
+
function readStdinSync() {
|
|
121
|
+
try {
|
|
122
|
+
return readFileSync(0, "utf8");
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return "";
|
|
88
126
|
}
|
|
89
|
-
return { token };
|
|
90
127
|
}
|
package/dist/commands/router.js
CHANGED
|
@@ -3,7 +3,7 @@ import { auditCommand } from "./audit.js";
|
|
|
3
3
|
import { cooldownCommand } from "./cooldown.js";
|
|
4
4
|
import { decisionsCommand } from "./decisions.js";
|
|
5
5
|
import { doctorCommand } from "./doctor.js";
|
|
6
|
-
import {
|
|
6
|
+
import { commitGuardCommand } from "./commit-guard.js";
|
|
7
7
|
import { agentsCommand } from "./agents.js";
|
|
8
8
|
import { renderCommandHelp, renderRootHelp } from "./help.js";
|
|
9
9
|
import { licensesCommand } from "./licenses.js";
|
|
@@ -11,6 +11,7 @@ import { sbomCommand } from "./sbom.js";
|
|
|
11
11
|
import { loginCommand } from "./login.js";
|
|
12
12
|
import { logoutCommand } from "./logout.js";
|
|
13
13
|
import { packageManagerCommands } from "./wrap.js";
|
|
14
|
+
import { isSupportedPackageManager, normalizeManagerName } from "../launcher/classify.js";
|
|
14
15
|
import { scanCommand } from "./scan.js";
|
|
15
16
|
import { serviceCommand } from "./service.js";
|
|
16
17
|
import { setupCommand } from "./setup.js";
|
|
@@ -26,7 +27,7 @@ export const commandCatalog = [
|
|
|
26
27
|
sbomCommand,
|
|
27
28
|
verifyCommand,
|
|
28
29
|
setupCommand,
|
|
29
|
-
|
|
30
|
+
commitGuardCommand,
|
|
30
31
|
agentsCommand,
|
|
31
32
|
decisionsCommand,
|
|
32
33
|
cooldownCommand,
|
|
@@ -79,6 +80,15 @@ export async function routeCommand(args) {
|
|
|
79
80
|
}
|
|
80
81
|
const command = commandCatalog.find((candidate) => candidate.name === commandName || candidate.aliases?.includes(commandName));
|
|
81
82
|
if (!command) {
|
|
83
|
+
// The pip3 shim execs `dg pip3`, but the wrap catalog holds only canonical
|
|
84
|
+
// names; fold as the classifier does so `dg pip3 …` reaches the pip manager.
|
|
85
|
+
const normalizedManager = normalizeManagerName(commandName);
|
|
86
|
+
if (normalizedManager !== commandName && isSupportedPackageManager(normalizedManager)) {
|
|
87
|
+
const managerCommand = commandCatalog.find((candidate) => candidate.name === normalizedManager);
|
|
88
|
+
if (managerCommand) {
|
|
89
|
+
return managerCommand.handler({ commandPath: [normalizedManager], args: rest });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
82
92
|
const suggestion = closestCommand(commandName, commandNames());
|
|
83
93
|
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
84
94
|
return {
|