@westbayberry/dg 1.3.3 → 2.0.0

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.
Files changed (126) hide show
  1. package/LICENSE +1 -201
  2. package/NOTICE +1 -4
  3. package/README.md +293 -0
  4. package/dist/api/analyze.js +210 -0
  5. package/dist/audit/deep.js +180 -0
  6. package/dist/audit/detectors.js +247 -0
  7. package/dist/audit/events.js +41 -0
  8. package/dist/audit/rules.js +426 -0
  9. package/dist/audit-ui/AuditApp.js +39 -0
  10. package/dist/audit-ui/components/AuditHeader.js +24 -0
  11. package/dist/audit-ui/components/AuditResultsView.js +307 -0
  12. package/dist/audit-ui/components/DeepStatusRow.js +11 -0
  13. package/dist/audit-ui/export.js +85 -0
  14. package/dist/audit-ui/format.js +34 -0
  15. package/dist/audit-ui/launch.js +34 -0
  16. package/dist/auth/device-login.js +271 -0
  17. package/dist/auth/env-token.js +6 -0
  18. package/dist/auth/login-app.js +156 -0
  19. package/dist/auth/store.js +147 -0
  20. package/dist/bin/dg.js +71 -0
  21. package/dist/commands/audit.js +357 -0
  22. package/dist/commands/completion.js +116 -0
  23. package/dist/commands/config.js +99 -0
  24. package/dist/commands/doctor.js +39 -0
  25. package/dist/commands/explain.js +100 -0
  26. package/dist/commands/guard-commit.js +158 -0
  27. package/dist/commands/help.js +74 -0
  28. package/dist/commands/licenses.js +435 -0
  29. package/dist/commands/login.js +81 -0
  30. package/dist/commands/logout.js +37 -0
  31. package/dist/commands/router.js +98 -0
  32. package/dist/commands/scan.js +18 -0
  33. package/dist/commands/service.js +475 -0
  34. package/dist/commands/setup.js +302 -0
  35. package/dist/commands/status.js +115 -0
  36. package/dist/commands/suggest.js +35 -0
  37. package/dist/commands/types.js +4 -0
  38. package/dist/commands/unavailable.js +11 -0
  39. package/dist/commands/uninstall.js +111 -0
  40. package/dist/commands/update.js +210 -0
  41. package/dist/commands/verify.js +151 -0
  42. package/dist/commands/version.js +22 -0
  43. package/dist/commands/wrap.js +55 -0
  44. package/dist/config/settings.js +302 -0
  45. package/dist/install-ui/LiveInstall.js +24 -0
  46. package/dist/install-ui/block-render.js +83 -0
  47. package/dist/install-ui/live-install-app.js +48 -0
  48. package/dist/install-ui/prompt.js +24 -0
  49. package/dist/launcher/classify.js +116 -0
  50. package/dist/launcher/env.js +53 -0
  51. package/dist/launcher/live-install.js +50 -0
  52. package/dist/launcher/output-redaction.js +77 -0
  53. package/dist/launcher/preflight-prompt.js +139 -0
  54. package/dist/launcher/resolve-real-binary.js +73 -0
  55. package/dist/launcher/run.js +417 -0
  56. package/dist/policy/evaluate.js +128 -0
  57. package/dist/presentation/mode.js +52 -0
  58. package/dist/presentation/theme.js +29 -0
  59. package/dist/proxy/buffer-budget.js +64 -0
  60. package/dist/proxy/ca.js +126 -0
  61. package/dist/proxy/classify-host.js +26 -0
  62. package/dist/proxy/enforcement.js +102 -0
  63. package/dist/proxy/metadata-map.js +336 -0
  64. package/dist/proxy/server.js +909 -0
  65. package/dist/proxy/upstream-proxy.js +102 -0
  66. package/dist/proxy/worker.js +39 -0
  67. package/dist/publish-set/collect.js +51 -0
  68. package/dist/publish-set/no-exec-shell.js +19 -0
  69. package/dist/publish-set/npm.js +109 -0
  70. package/dist/publish-set/pack.js +36 -0
  71. package/dist/publish-set/pypi.js +59 -0
  72. package/dist/runtime/cli.js +17 -0
  73. package/dist/runtime/first-run.js +60 -0
  74. package/dist/runtime/node-version.js +58 -0
  75. package/dist/runtime/nudges.js +105 -0
  76. package/dist/scan/analyze-worker.js +21 -0
  77. package/dist/scan/collect.js +153 -0
  78. package/dist/scan/command.js +159 -0
  79. package/dist/scan/discovery.js +209 -0
  80. package/dist/scan/render.js +240 -0
  81. package/dist/scan/scanner-report.js +82 -0
  82. package/dist/scan/staged.js +173 -0
  83. package/dist/scan/types.js +1 -0
  84. package/dist/scan-ui/LegacyApp.js +156 -0
  85. package/dist/scan-ui/alt-screen.js +84 -0
  86. package/dist/scan-ui/api-aliases.js +1 -0
  87. package/dist/scan-ui/components/ErrorView.js +23 -0
  88. package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
  89. package/dist/scan-ui/components/ProgressBar.js +89 -0
  90. package/dist/scan-ui/components/ProjectSelector.js +62 -0
  91. package/dist/scan-ui/components/ScoreHeader.js +20 -0
  92. package/dist/scan-ui/components/SetupBanner.js +13 -0
  93. package/dist/scan-ui/components/Spinner.js +4 -0
  94. package/dist/scan-ui/format-helpers.js +40 -0
  95. package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
  96. package/dist/scan-ui/hooks/useScan.js +113 -0
  97. package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
  98. package/dist/scan-ui/launch.js +27 -0
  99. package/dist/scan-ui/logo.js +91 -0
  100. package/dist/scan-ui/shims.js +30 -0
  101. package/dist/security/sanitize.js +28 -0
  102. package/dist/service/state.js +837 -0
  103. package/dist/service/trust-store.js +234 -0
  104. package/dist/service/worker.js +88 -0
  105. package/dist/setup/git-hook.js +244 -0
  106. package/dist/setup/optional-support.js +58 -0
  107. package/dist/setup/plan.js +899 -0
  108. package/dist/state/cleanup-registry.js +60 -0
  109. package/dist/state/index.js +5 -0
  110. package/dist/state/locks.js +161 -0
  111. package/dist/state/paths.js +24 -0
  112. package/dist/state/sessions.js +170 -0
  113. package/dist/state/store.js +50 -0
  114. package/dist/telemetry/events.js +40 -0
  115. package/dist/util/git.js +20 -0
  116. package/dist/util/tty-prompt.js +43 -0
  117. package/dist/verify/local.js +400 -0
  118. package/dist/verify/package-check.js +240 -0
  119. package/dist/verify/preflight.js +698 -0
  120. package/dist/verify/render.js +184 -0
  121. package/dist/verify/types.js +1 -0
  122. package/package.json +33 -50
  123. package/dist/index.mjs +0 -54116
  124. package/dist/postinstall.mjs +0 -731
  125. package/dist/python-hook/dg_pip_hook.pth +0 -1
  126. package/dist/python-hook/dg_pip_hook.py +0 -130
@@ -0,0 +1,435 @@
1
+ import { mkdirSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { launchScanTui, shouldLaunchScanTui } from "../scan-ui/launch.js";
3
+ import { basename, dirname, relative, resolve, sep } from "node:path";
4
+ import { scanProject } from "../scan/discovery.js";
5
+ import { isSupportedLockfilePath, verifyLockfile } from "../verify/preflight.js";
6
+ import { EXIT_USAGE } from "./types.js";
7
+ const LICENSE_RISKS = [
8
+ "network-copyleft",
9
+ "no-license",
10
+ "permissive",
11
+ "strong-copyleft",
12
+ "unknown",
13
+ "unlicensed",
14
+ "weak-copyleft"
15
+ ];
16
+ const IGNORED_DIRECTORIES = new Set([
17
+ ".git",
18
+ ".hg",
19
+ ".svn",
20
+ "coverage",
21
+ "dist",
22
+ "node_modules",
23
+ "vendor"
24
+ ]);
25
+ const MAX_DISCOVERY_DEPTH = 8;
26
+ export const licensesCommand = {
27
+ name: "licenses",
28
+ summary: "Report dependency licenses and policy results.",
29
+ usage: "dg licenses [path] [--json|--csv|--markdown] [--fail-on <risk[,risk...]>]",
30
+ args: [{ name: "[path]", summary: "Project directory to report on (default: current directory)." }],
31
+ flags: [
32
+ { flag: "--json", summary: "JSON output." },
33
+ { flag: "--csv", summary: "CSV output." },
34
+ { flag: "--markdown", summary: "Markdown table output." },
35
+ { flag: "--output", value: "<path>", summary: "Write the report to a file (alias -o)." },
36
+ { flag: "--fail-on", value: "<risk[,risk...]>", summary: "Exit non-zero on these risks: permissive, weak-copyleft, strong-copyleft, network-copyleft, no-license, unlicensed, unknown." },
37
+ { flag: "--deny-license", value: "<id>", summary: "Fail if this SPDX license appears (repeatable)." }
38
+ ],
39
+ examples: ["dg licenses", "dg licenses --markdown -o licenses.md", "dg licenses --fail-on strong-copyleft,network-copyleft"],
40
+ details: [
41
+ "Reports project and lockfile license metadata without running package managers or package code.",
42
+ "License exports support text, JSON, CSV, and Markdown with stable exit codes for policy gates."
43
+ ],
44
+ handler: (context) => runLicensesCommand(context.args)
45
+ };
46
+ async function runLicensesCommand(args) {
47
+ const parsed = parseLicensesArgs(args);
48
+ if ("error" in parsed) {
49
+ return usageError(parsed.error);
50
+ }
51
+ if (parsed.deniedLicenses.length === 0 &&
52
+ parsed.failOn.length === 0 &&
53
+ shouldLaunchScanTui({
54
+ targetPath: parsed.target,
55
+ format: parsed.format,
56
+ outputPath: parsed.outputPath ?? undefined
57
+ })) {
58
+ try {
59
+ await launchScanTui("licenses");
60
+ }
61
+ catch (error) {
62
+ return {
63
+ exitCode: 1,
64
+ stdout: "",
65
+ stderr: `dg licenses TUI failed: ${error instanceof Error ? error.message : "unknown error"}\n`
66
+ };
67
+ }
68
+ return {
69
+ exitCode: 0,
70
+ stdout: "",
71
+ stderr: ""
72
+ };
73
+ }
74
+ let report;
75
+ try {
76
+ report = buildLicenseReport(parsed);
77
+ }
78
+ catch (error) {
79
+ return {
80
+ exitCode: 1,
81
+ stdout: "",
82
+ stderr: `dg licenses failed: ${error instanceof Error ? error.message : "unknown license error"}\n`
83
+ };
84
+ }
85
+ const rendered = renderLicenseReport(report, parsed.format);
86
+ if (parsed.outputPath) {
87
+ try {
88
+ writeReportAtomic(resolve(parsed.outputPath), rendered);
89
+ }
90
+ catch (error) {
91
+ return {
92
+ exitCode: 1,
93
+ stdout: "",
94
+ stderr: `dg licenses could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
95
+ };
96
+ }
97
+ return {
98
+ exitCode: exitCodeForReport(report),
99
+ stdout: `Wrote ${parsed.format} license report to ${parsed.outputPath}\n`,
100
+ stderr: ""
101
+ };
102
+ }
103
+ return {
104
+ exitCode: exitCodeForReport(report),
105
+ stdout: rendered,
106
+ stderr: ""
107
+ };
108
+ }
109
+ function parseLicensesArgs(args) {
110
+ let format = "text";
111
+ let outputPath = null;
112
+ let target = ".";
113
+ let sawTarget = false;
114
+ const deniedLicenses = [];
115
+ const failOn = [];
116
+ for (let index = 0; index < args.length; index += 1) {
117
+ const arg = args[index];
118
+ if (!arg) {
119
+ return { error: "empty argument" };
120
+ }
121
+ if (arg === "--json" || arg === "--csv" || arg === "--markdown") {
122
+ if (format !== "text") {
123
+ return { error: "choose only one output format" };
124
+ }
125
+ format = arg === "--json" ? "json" : arg === "--csv" ? "csv" : "markdown";
126
+ continue;
127
+ }
128
+ if (arg === "--output" || arg === "-o") {
129
+ const next = args[index + 1];
130
+ if (!next) {
131
+ return { error: `${arg} requires a path` };
132
+ }
133
+ outputPath = next;
134
+ index += 1;
135
+ continue;
136
+ }
137
+ if (arg === "--fail-on") {
138
+ const next = args[index + 1];
139
+ if (!next) {
140
+ return { error: "--fail-on requires a comma-separated risk list" };
141
+ }
142
+ const parsed = parseRiskList(next);
143
+ if ("error" in parsed) {
144
+ return parsed;
145
+ }
146
+ failOn.push(...parsed.risks);
147
+ index += 1;
148
+ continue;
149
+ }
150
+ if (arg.startsWith("--fail-on=")) {
151
+ const parsed = parseRiskList(arg.slice("--fail-on=".length));
152
+ if ("error" in parsed) {
153
+ return parsed;
154
+ }
155
+ failOn.push(...parsed.risks);
156
+ continue;
157
+ }
158
+ if (arg === "--deny-license") {
159
+ const next = args[index + 1];
160
+ if (!next) {
161
+ return { error: "--deny-license requires a license id" };
162
+ }
163
+ deniedLicenses.push(next);
164
+ index += 1;
165
+ continue;
166
+ }
167
+ if (arg.startsWith("-")) {
168
+ return { error: `unknown option '${arg}'` };
169
+ }
170
+ if (sawTarget) {
171
+ return { error: "licenses accepts at most one path" };
172
+ }
173
+ target = arg;
174
+ sawTarget = true;
175
+ }
176
+ return {
177
+ deniedLicenses,
178
+ failOn: uniqueRisks(failOn),
179
+ format,
180
+ outputPath,
181
+ target
182
+ };
183
+ }
184
+ function parseRiskList(value) {
185
+ const risks = [];
186
+ for (const raw of value.split(",")) {
187
+ const risk = raw.trim();
188
+ if (!isLicenseRisk(risk)) {
189
+ return { error: `unknown license risk '${risk}'` };
190
+ }
191
+ risks.push(risk);
192
+ }
193
+ return { risks };
194
+ }
195
+ function buildLicenseReport(parsed) {
196
+ const targetPath = resolve(parsed.target);
197
+ const targetInfo = statSync(targetPath);
198
+ const root = targetInfo.isFile() ? dirname(targetPath) : targetPath;
199
+ const lockfiles = targetInfo.isFile() && isSupportedLockfilePath(targetPath)
200
+ ? [targetPath]
201
+ : discoverLockfiles(root);
202
+ const entries = dedupeEntries([
203
+ ...manifestLicenseEntries(parsed.target),
204
+ ...lockfiles.flatMap((lockfile) => lockfileLicenseEntries(root, lockfile))
205
+ ]);
206
+ const denied = new Set(parsed.deniedLicenses.map(normalizeLicense));
207
+ const failOn = new Set(parsed.failOn);
208
+ const blocked = entries.filter((entry) => {
209
+ const normalizedLicense = normalizeLicense(entry.license ?? "");
210
+ return (entry.license ? denied.has(normalizedLicense) : false) || failOn.has(entry.risk);
211
+ });
212
+ const byRisk = Object.fromEntries(LICENSE_RISKS.map((risk) => [risk, 0]));
213
+ for (const entry of entries) {
214
+ byRisk[entry.risk] += 1;
215
+ }
216
+ return {
217
+ target: displayPath(process.cwd(), targetPath),
218
+ status: blocked.length > 0 ? "block" : "pass",
219
+ entries,
220
+ policy: {
221
+ deniedLicenses: [...denied].sort(),
222
+ failOn: [...failOn].sort()
223
+ },
224
+ summary: {
225
+ packageCount: entries.length,
226
+ blockedCount: blocked.length,
227
+ byRisk
228
+ }
229
+ };
230
+ }
231
+ function manifestLicenseEntries(target) {
232
+ const report = scanProject({
233
+ targetPath: target
234
+ });
235
+ return report.projects.map((project) => ({
236
+ ecosystem: ecosystemForManifest(project.manifestPath),
237
+ license: project.license,
238
+ location: project.manifestPath,
239
+ name: project.name,
240
+ risk: classifyLicense(project.license),
241
+ source: "manifest",
242
+ version: project.version
243
+ }));
244
+ }
245
+ const MANIFEST_ECOSYSTEMS = {
246
+ "package.json": "npm",
247
+ "pyproject.toml": "pypi",
248
+ "setup.py": "pypi",
249
+ "setup.cfg": "pypi",
250
+ "Cargo.toml": "cargo"
251
+ };
252
+ function ecosystemForManifest(manifestPath) {
253
+ return MANIFEST_ECOSYSTEMS[basename(manifestPath)] ?? "unknown";
254
+ }
255
+ function lockfileLicenseEntries(root, lockfile) {
256
+ const report = verifyLockfile(lockfile);
257
+ return report.packages.map((identity) => licenseEntryFromIdentity(root, lockfile, identity));
258
+ }
259
+ function licenseEntryFromIdentity(root, lockfile, identity) {
260
+ return {
261
+ ecosystem: identity.ecosystem,
262
+ license: identity.license,
263
+ location: displayPath(root, lockfile),
264
+ name: identity.name,
265
+ risk: classifyLicense(identity.license),
266
+ source: "lockfile",
267
+ version: identity.version
268
+ };
269
+ }
270
+ function discoverLockfiles(root) {
271
+ const lockfiles = [];
272
+ walk(root, 0, lockfiles);
273
+ return lockfiles.sort((left, right) => displayPath(root, left).localeCompare(displayPath(root, right)));
274
+ }
275
+ function walk(directory, depth, lockfiles) {
276
+ if (depth > MAX_DISCOVERY_DEPTH) {
277
+ return;
278
+ }
279
+ const entries = readdirSync(directory, {
280
+ withFileTypes: true
281
+ }).sort((left, right) => left.name.localeCompare(right.name));
282
+ for (const entry of entries) {
283
+ const absolutePath = resolve(directory, entry.name);
284
+ if (entry.isDirectory()) {
285
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
286
+ walk(absolutePath, depth + 1, lockfiles);
287
+ }
288
+ continue;
289
+ }
290
+ if (entry.isFile() && isSupportedLockfilePath(absolutePath)) {
291
+ lockfiles.push(absolutePath);
292
+ }
293
+ }
294
+ }
295
+ function classifyLicense(license) {
296
+ if (!license) {
297
+ return "no-license";
298
+ }
299
+ const normalized = normalizeLicense(license);
300
+ if (normalized === "UNLICENSED" || normalized === "NOASSERTION") {
301
+ return "unlicensed";
302
+ }
303
+ if (/\bAGPL\b/u.test(normalized)) {
304
+ return "network-copyleft";
305
+ }
306
+ if (/\bGPL\b/u.test(normalized)) {
307
+ return "strong-copyleft";
308
+ }
309
+ if (/\bLGPL\b|\bMPL\b|\bEPL\b|\bCDDL\b/u.test(normalized)) {
310
+ return "weak-copyleft";
311
+ }
312
+ if (/\bMIT\b|\bISC\b|\bBSD\b|\bAPACHE\b|\b0BSD\b/u.test(normalized)) {
313
+ return "permissive";
314
+ }
315
+ return "unknown";
316
+ }
317
+ function renderLicenseReport(report, format) {
318
+ if (format === "json") {
319
+ return `${JSON.stringify(report, null, 2)}\n`;
320
+ }
321
+ if (format === "csv") {
322
+ return renderCsv(report);
323
+ }
324
+ if (format === "markdown") {
325
+ return renderMarkdown(report);
326
+ }
327
+ return renderText(report);
328
+ }
329
+ function renderText(report) {
330
+ const lines = [
331
+ "Dependency Guardian licenses",
332
+ `Target: ${report.target}`,
333
+ `Status: ${report.status}`,
334
+ `Packages: ${report.summary.packageCount}`,
335
+ `Policy blocks: ${report.summary.blockedCount}`,
336
+ ""
337
+ ];
338
+ if (report.entries.length === 0) {
339
+ lines.push("No license metadata found.");
340
+ }
341
+ else {
342
+ for (const entry of report.entries) {
343
+ const version = entry.version ? `@${entry.version}` : "";
344
+ lines.push(`- ${entry.ecosystem}:${entry.name}${version} license=${entry.license ?? "none"} risk=${entry.risk} source=${entry.source} location=${entry.location}`);
345
+ }
346
+ }
347
+ return `${lines.join("\n").trimEnd()}\n`;
348
+ }
349
+ function renderCsv(report) {
350
+ const rows = [
351
+ ["ecosystem", "name", "version", "license", "risk", "source", "location"],
352
+ ...report.entries.map((entry) => [
353
+ entry.ecosystem,
354
+ entry.name,
355
+ entry.version ?? "",
356
+ entry.license ?? "",
357
+ entry.risk,
358
+ entry.source,
359
+ entry.location
360
+ ])
361
+ ];
362
+ return `${rows.map((row) => row.map(csvCell).join(",")).join("\n")}\n`;
363
+ }
364
+ function renderMarkdown(report) {
365
+ const lines = [
366
+ "# Dependency Guardian licenses",
367
+ "",
368
+ `- Target: ${report.target}`,
369
+ `- Status: ${report.status}`,
370
+ `- Packages: ${report.summary.packageCount}`,
371
+ `- Policy blocks: ${report.summary.blockedCount}`,
372
+ "",
373
+ "| Ecosystem | Package | Version | License | Risk | Source |",
374
+ "| --- | --- | --- | --- | --- | --- |"
375
+ ];
376
+ for (const entry of report.entries) {
377
+ lines.push(`| ${markdownCell(entry.ecosystem)} | ${markdownCell(entry.name)} | ${markdownCell(entry.version ?? "")} | ${markdownCell(entry.license ?? "")} | ${markdownCell(entry.risk)} | ${markdownCell(entry.source)} |`);
378
+ }
379
+ return `${lines.join("\n")}\n`;
380
+ }
381
+ function exitCodeForReport(report) {
382
+ return report.status === "block" ? 1 : 0;
383
+ }
384
+ function usageError(message) {
385
+ return {
386
+ exitCode: EXIT_USAGE,
387
+ stdout: "",
388
+ stderr: `dg licenses: ${message}. Usage: dg licenses [path] [--json|--csv|--markdown] [--output <path>] [--fail-on <risk[,risk...]>] [--deny-license <id>]\n`
389
+ };
390
+ }
391
+ function writeReportAtomic(outputPath, contents) {
392
+ const directory = dirname(outputPath);
393
+ mkdirSync(directory, { recursive: true });
394
+ const temporaryPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
395
+ try {
396
+ writeFileSync(temporaryPath, contents, "utf8");
397
+ renameSync(temporaryPath, outputPath);
398
+ }
399
+ catch (error) {
400
+ rmSync(temporaryPath, { force: true });
401
+ throw error;
402
+ }
403
+ }
404
+ function dedupeEntries(entries) {
405
+ const seen = new Set();
406
+ const deduped = [];
407
+ for (const entry of entries) {
408
+ const key = `${entry.ecosystem}|${entry.name}|${entry.version ?? ""}|${entry.license ?? ""}|${entry.source}|${entry.location}`;
409
+ if (!seen.has(key)) {
410
+ seen.add(key);
411
+ deduped.push(entry);
412
+ }
413
+ }
414
+ return deduped.sort((left, right) => `${left.ecosystem}:${left.name}`.localeCompare(`${right.ecosystem}:${right.name}`));
415
+ }
416
+ function uniqueRisks(risks) {
417
+ return [...new Set(risks)].sort();
418
+ }
419
+ function isLicenseRisk(value) {
420
+ return LICENSE_RISKS.includes(value);
421
+ }
422
+ function normalizeLicense(value) {
423
+ return value.trim().toUpperCase().replace(/\s+/gu, "-");
424
+ }
425
+ function displayPath(root, path) {
426
+ const relativePath = relative(root, path);
427
+ const display = relativePath.length === 0 ? "." : relativePath;
428
+ return display.split(sep).join("/");
429
+ }
430
+ function csvCell(value) {
431
+ return /[",\n\r]/u.test(value) ? `"${value.replace(/"/gu, "\"\"")}"` : value;
432
+ }
433
+ function markdownCell(value) {
434
+ return value.replace(/\|/gu, "\\|");
435
+ }
@@ -0,0 +1,81 @@
1
+ import { EXIT_USAGE } from "./types.js";
2
+ import { AuthError, writeAuthState } from "../auth/store.js";
3
+ import { ConfigError, loadUserConfig } from "../config/settings.js";
4
+ export const loginCommand = {
5
+ name: "login",
6
+ summary: "Authenticate this machine with Dependency Guardian.",
7
+ usage: "dg login [--token <token>]",
8
+ flags: [
9
+ { flag: "--token", value: "<token>", summary: "Authenticate with an API key instead of the browser (for CI/headless)." }
10
+ ],
11
+ examples: ["dg login", "dg login --token dg_live_…", "DG_API_TOKEN=dg_live_… dg login"],
12
+ details: [
13
+ "In a terminal, 'dg login' opens your browser to sign in — no token to copy.",
14
+ "For CI and headless shells, pass --token <key> or set the DG_API_TOKEN environment variable instead.",
15
+ "Stores dg-owned auth state under the user config directory; never executes project-local code or weakens install enforcement."
16
+ ],
17
+ handler: (context) => loginHandler(context.args)
18
+ };
19
+ function loginHandler(args) {
20
+ const parsed = parseLoginArgs(args);
21
+ if ("error" in parsed) {
22
+ return {
23
+ exitCode: EXIT_USAGE,
24
+ stdout: "",
25
+ stderr: `dg login: ${parsed.error}. Run 'dg login --help'.\n`
26
+ };
27
+ }
28
+ try {
29
+ const config = loadUserConfig();
30
+ const state = writeAuthState({
31
+ token: parsed.token,
32
+ apiBaseUrl: config.api.baseUrl,
33
+ orgId: config.org.id
34
+ });
35
+ return {
36
+ exitCode: 0,
37
+ stdout: `Logged in to ${state.apiBaseUrl}${state.orgId ? ` for org ${state.orgId}` : ""} with token ${state.tokenPreview}\n`,
38
+ stderr: ""
39
+ };
40
+ }
41
+ catch (error) {
42
+ if (error instanceof AuthError || error instanceof ConfigError) {
43
+ return {
44
+ exitCode: EXIT_USAGE,
45
+ stdout: "",
46
+ stderr: `dg login: ${error.message}\n`
47
+ };
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ function parseLoginArgs(args) {
53
+ let token = "";
54
+ for (let index = 0; index < args.length; index += 1) {
55
+ const arg = args[index];
56
+ if (arg === "--token") {
57
+ const value = args[index + 1];
58
+ if (!value) {
59
+ return {
60
+ error: "--token requires a value"
61
+ };
62
+ }
63
+ token = value;
64
+ index += 1;
65
+ }
66
+ else if (arg?.startsWith("--token=")) {
67
+ token = arg.slice("--token=".length);
68
+ }
69
+ else {
70
+ return {
71
+ error: `unknown option '${arg ?? ""}'`
72
+ };
73
+ }
74
+ }
75
+ if (!token) {
76
+ return {
77
+ error: "run 'dg login' in a terminal to sign in via your browser, or pass --token <key> (or set DG_API_TOKEN) for CI"
78
+ };
79
+ }
80
+ return { token };
81
+ }
@@ -0,0 +1,37 @@
1
+ import { EXIT_USAGE } from "./types.js";
2
+ import { clearAuthState } from "../auth/store.js";
3
+ export const logoutCommand = {
4
+ name: "logout",
5
+ summary: "Remove local Dependency Guardian authentication.",
6
+ usage: "dg logout [--yes]",
7
+ flags: [{ flag: "--yes", summary: "Required confirmation flag; logout removes the local token only when it is passed." }],
8
+ details: ["Removes dg-owned auth material without deleting unrelated user content."],
9
+ handler: (context) => logoutHandler(context.args)
10
+ };
11
+ function logoutHandler(args, env = process.env) {
12
+ const unknown = args.find((arg) => arg !== "--yes");
13
+ if (unknown) {
14
+ return {
15
+ exitCode: EXIT_USAGE,
16
+ stdout: "",
17
+ stderr: `dg logout: unknown option '${unknown}'. Run 'dg logout --help'.\n`
18
+ };
19
+ }
20
+ if (!args.includes("--yes")) {
21
+ return {
22
+ exitCode: EXIT_USAGE,
23
+ stdout: "This removes the dg-owned local auth token only. Config and setup files are not changed.\n",
24
+ stderr: "dg logout requires --yes to confirm.\n"
25
+ };
26
+ }
27
+ const removed = clearAuthState();
28
+ const lines = [removed ? "Removed local dg auth token." : "No local dg auth token was present."];
29
+ if (env.DG_API_TOKEN) {
30
+ lines.push("DG_API_TOKEN is still set, so env-var auth remains active (file token removed only).");
31
+ }
32
+ return {
33
+ exitCode: 0,
34
+ stdout: `${lines.join("\n")}\n`,
35
+ stderr: ""
36
+ };
37
+ }
@@ -0,0 +1,98 @@
1
+ import { configCommand } from "./config.js";
2
+ import { completionCommand } from "./completion.js";
3
+ import { auditCommand } from "./audit.js";
4
+ import { doctorCommand } from "./doctor.js";
5
+ import { explainCommand } from "./explain.js";
6
+ import { guardCommitCommand } from "./guard-commit.js";
7
+ import { renderCommandHelp, renderRootHelp } from "./help.js";
8
+ import { licensesCommand } from "./licenses.js";
9
+ import { loginCommand } from "./login.js";
10
+ import { logoutCommand } from "./logout.js";
11
+ import { packageManagerCommands } from "./wrap.js";
12
+ import { scanCommand } from "./scan.js";
13
+ import { serviceCommand } from "./service.js";
14
+ import { setupCommand } from "./setup.js";
15
+ import { statusCommand } from "./status.js";
16
+ import { closestCommand } from "./suggest.js";
17
+ import { EXIT_USAGE } from "./types.js";
18
+ import { uninstallCommand } from "./uninstall.js";
19
+ import { updateCommand } from "./update.js";
20
+ import { verifyCommand } from "./verify.js";
21
+ import { dgVersion, versionResult } from "./version.js";
22
+ export const commandCatalog = [
23
+ scanCommand,
24
+ verifyCommand,
25
+ setupCommand,
26
+ guardCommitCommand,
27
+ uninstallCommand,
28
+ doctorCommand,
29
+ statusCommand,
30
+ ...packageManagerCommands(),
31
+ loginCommand,
32
+ logoutCommand,
33
+ explainCommand,
34
+ configCommand,
35
+ completionCommand,
36
+ licensesCommand,
37
+ auditCommand,
38
+ updateCommand,
39
+ serviceCommand
40
+ ];
41
+ function commandNames() {
42
+ const names = new Set(["help", "version", "upgrade"]);
43
+ for (const command of commandCatalog) {
44
+ names.add(command.name);
45
+ for (const alias of command.aliases ?? []) {
46
+ names.add(alias);
47
+ }
48
+ }
49
+ return [...names];
50
+ }
51
+ export async function routeCommand(args) {
52
+ const [commandName, ...rest] = args;
53
+ if (commandName === "--help-all" || commandName === "help-all") {
54
+ return {
55
+ exitCode: 0,
56
+ stdout: renderRootHelp(dgVersion(), commandCatalog, { all: true }),
57
+ stderr: ""
58
+ };
59
+ }
60
+ if (!commandName || commandName === "--help" || commandName === "-h" || commandName === "help") {
61
+ return {
62
+ exitCode: 0,
63
+ stdout: renderRootHelp(dgVersion(), commandCatalog),
64
+ stderr: ""
65
+ };
66
+ }
67
+ if (commandName === "--version" || commandName === "-v" || commandName === "version") {
68
+ return versionResult();
69
+ }
70
+ if (commandName === "upgrade") {
71
+ return updateCommand.handler({
72
+ commandPath: ["upgrade"],
73
+ args: rest
74
+ });
75
+ }
76
+ const command = commandCatalog.find((candidate) => candidate.name === commandName || candidate.aliases?.includes(commandName));
77
+ if (!command) {
78
+ const suggestion = closestCommand(commandName, commandNames());
79
+ const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
80
+ return {
81
+ exitCode: EXIT_USAGE,
82
+ stdout: "",
83
+ stderr: `dg: unknown command '${commandName}'.${hint} Run 'dg --help'.\n`
84
+ };
85
+ }
86
+ const [firstArg] = rest;
87
+ if (firstArg === "--help" || firstArg === "-h" || firstArg === "help") {
88
+ return {
89
+ exitCode: 0,
90
+ stdout: renderCommandHelp(command),
91
+ stderr: ""
92
+ };
93
+ }
94
+ return command.handler({
95
+ commandPath: [commandName],
96
+ args: rest
97
+ });
98
+ }
@@ -0,0 +1,18 @@
1
+ import { runScanCommand } from "../scan/command.js";
2
+ export const scanCommand = {
3
+ name: "scan",
4
+ summary: "Scan a project and show Dependency Guardian findings.",
5
+ usage: "dg scan [path] [--json|--sarif] [--output <path>]",
6
+ args: [{ name: "[path]", summary: "Project directory or a package.json to scan (default: current directory)." }],
7
+ flags: [
8
+ { flag: "--json", summary: "Machine-readable JSON report." },
9
+ { flag: "--sarif", summary: "SARIF report for code-scanning tools." },
10
+ { flag: "--output", value: "<path>", summary: "Write the report to a file instead of stdout (alias -o)." },
11
+ { flag: "--staged", summary: "Scan only the git-staged lockfile changes (what dg guard-commit runs)." }
12
+ ],
13
+ examples: ["dg scan", "dg scan ./packages/api", "dg scan --json -o scan.json", "dg scan --staged"],
14
+ details: [
15
+ "Reads lockfiles, scores each dependency through the scanner, and never changes project files. In a terminal it opens the full-screen results browser; piped or with --json/--sarif it prints machine output. Exit codes: 0 clean, 1 warn, 2 block, 4 analysis incomplete."
16
+ ],
17
+ handler: runScanCommand
18
+ };