@westbayberry/dg 2.0.11 → 2.2.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 (143) hide show
  1. package/README.md +216 -226
  2. package/dist/agents/claude-code.js +113 -0
  3. package/dist/agents/codex.js +65 -0
  4. package/dist/agents/copilot-cli.js +115 -0
  5. package/dist/agents/cursor.js +113 -0
  6. package/dist/agents/gemini.js +107 -0
  7. package/dist/agents/persistence.js +285 -0
  8. package/dist/agents/registry.js +127 -0
  9. package/dist/agents/types.js +1 -0
  10. package/dist/agents/windsurf.js +93 -0
  11. package/dist/api/analyze.js +6 -4
  12. package/dist/audit/detectors.js +0 -11
  13. package/dist/audit/events.js +5 -21
  14. package/dist/audit-ui/AuditApp.js +2 -0
  15. package/dist/audit-ui/components/AuditResultsView.js +55 -92
  16. package/dist/audit-ui/export.js +0 -4
  17. package/dist/audit-ui/format.js +0 -3
  18. package/dist/audit-ui/launch.js +10 -1
  19. package/dist/auth/device-login.js +4 -5
  20. package/dist/auth/login-app.js +7 -7
  21. package/dist/auth/store.js +8 -3
  22. package/dist/bin/dg.js +59 -52
  23. package/dist/commands/agents.js +231 -0
  24. package/dist/commands/audit.js +22 -3
  25. package/dist/commands/config.js +26 -10
  26. package/dist/commands/cooldown.js +389 -0
  27. package/dist/commands/decisions.js +169 -0
  28. package/dist/commands/doctor.js +1 -1
  29. package/dist/commands/help.js +1 -1
  30. package/dist/commands/licenses.js +10 -22
  31. package/dist/commands/logout.js +4 -11
  32. package/dist/commands/router.js +8 -4
  33. package/dist/commands/sbom.js +206 -0
  34. package/dist/commands/scan.js +3 -2
  35. package/dist/commands/service.js +32 -13
  36. package/dist/commands/setup.js +197 -27
  37. package/dist/commands/status.js +5 -2
  38. package/dist/commands/types.js +1 -0
  39. package/dist/commands/update.js +17 -8
  40. package/dist/commands/verify.js +8 -5
  41. package/dist/config/settings.js +154 -65
  42. package/dist/decisions/apply.js +128 -0
  43. package/dist/decisions/remember-prompt.js +92 -0
  44. package/dist/export-ui/ExportDialog.js +198 -0
  45. package/dist/install-ui/LiveInstall.js +2 -2
  46. package/dist/install-ui/block-render.js +21 -4
  47. package/dist/install-ui/prep-spinner.js +32 -0
  48. package/dist/install-ui/prompt.js +14 -0
  49. package/dist/launcher/agent-check.js +466 -0
  50. package/dist/launcher/agent-hook-exec.js +70 -0
  51. package/dist/launcher/agent-hook-io.js +31 -0
  52. package/dist/launcher/cargo-cache.js +40 -0
  53. package/dist/launcher/classify.js +17 -6
  54. package/dist/launcher/env.js +71 -24
  55. package/dist/launcher/install-preflight.js +167 -17
  56. package/dist/launcher/live-install.js +25 -5
  57. package/dist/launcher/output-redaction.js +7 -4
  58. package/dist/launcher/preflight-prompt.js +43 -3
  59. package/dist/launcher/run.js +100 -86
  60. package/dist/launcher/spawn-invocation.js +21 -0
  61. package/dist/policy/cooldown.js +117 -0
  62. package/dist/policy/evaluate.js +5 -21
  63. package/dist/policy/pypi-name.js +17 -0
  64. package/dist/presentation/mode.js +3 -2
  65. package/dist/presentation/package-page.js +9 -0
  66. package/dist/presentation/provenance.js +23 -0
  67. package/dist/presentation/theme.js +7 -7
  68. package/dist/project/dgfile.js +446 -0
  69. package/dist/proxy/auth.js +42 -0
  70. package/dist/proxy/ca.js +29 -9
  71. package/dist/proxy/cooldown-exemptions-file.js +33 -0
  72. package/dist/proxy/enforcement.js +57 -17
  73. package/dist/proxy/metadata-map.js +66 -4
  74. package/dist/proxy/preverified.js +55 -0
  75. package/dist/proxy/server.js +473 -45
  76. package/dist/proxy/worker.js +16 -1
  77. package/dist/publish-set/collect.js +1 -4
  78. package/dist/publish-set/npm.js +8 -5
  79. package/dist/publish-set/pack.js +9 -3
  80. package/dist/runtime/cli.js +0 -4
  81. package/dist/runtime/fatal.js +31 -0
  82. package/dist/runtime/first-run.js +12 -11
  83. package/dist/runtime/node-version.js +43 -6
  84. package/dist/runtime/nudges.js +35 -2
  85. package/dist/sbom/cyclonedx.js +211 -0
  86. package/dist/sbom-ui/SbomApp.js +158 -0
  87. package/dist/sbom-ui/components/SbomHeader.js +32 -0
  88. package/dist/sbom-ui/components/SbomList.js +52 -0
  89. package/dist/sbom-ui/inventory.js +128 -0
  90. package/dist/sbom-ui/launch.js +51 -0
  91. package/dist/sbom-ui/run.js +55 -0
  92. package/dist/sbom-ui/store.js +26 -0
  93. package/dist/scan/collect.js +10 -6
  94. package/dist/scan/command.js +51 -17
  95. package/dist/scan/discovery.js +11 -2
  96. package/dist/scan/render.js +63 -8
  97. package/dist/scan/scanner-report.js +42 -9
  98. package/dist/scan/staged.js +71 -11
  99. package/dist/scan-ui/LegacyApp.js +12 -16
  100. package/dist/scan-ui/alt-screen.js +5 -8
  101. package/dist/scan-ui/components/InteractiveResultsView.js +193 -123
  102. package/dist/scan-ui/components/ProgressBar.js +3 -14
  103. package/dist/scan-ui/components/ProjectSelector.js +1 -1
  104. package/dist/scan-ui/components/ScoreHeader.js +2 -3
  105. package/dist/scan-ui/components/SetupBanner.js +0 -6
  106. package/dist/scan-ui/format-helpers.js +61 -5
  107. package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
  108. package/dist/scan-ui/hooks/useScan.js +46 -4
  109. package/dist/scan-ui/launch.js +7 -4
  110. package/dist/scan-ui/shims.js +14 -4
  111. package/dist/scripts/detect.js +158 -0
  112. package/dist/scripts/gate.js +170 -0
  113. package/dist/service/state.js +27 -8
  114. package/dist/service/trust-refresh.js +92 -0
  115. package/dist/service/worker.js +23 -1
  116. package/dist/setup/activate-shell.js +28 -0
  117. package/dist/setup/git-hook.js +49 -4
  118. package/dist/setup/plan.js +98 -29
  119. package/dist/setup-ui/gate.js +39 -0
  120. package/dist/setup-ui/offer.js +42 -0
  121. package/dist/setup-ui/selector.js +27 -0
  122. package/dist/setup-ui/tasks.js +56 -0
  123. package/dist/setup-ui/wizard.js +225 -0
  124. package/dist/state/cooldown-held.js +66 -0
  125. package/dist/state/index.js +1 -0
  126. package/dist/state/locks.js +4 -2
  127. package/dist/state/store.js +2 -1
  128. package/dist/util/external-tool.js +25 -0
  129. package/dist/util/git.js +10 -3
  130. package/dist/util/json-file.js +24 -0
  131. package/dist/util/report-writer.js +57 -0
  132. package/dist/util/tty-prompt.js +13 -6
  133. package/dist/verify/local.js +240 -42
  134. package/dist/verify/package-check.js +86 -18
  135. package/dist/verify/preflight.js +242 -49
  136. package/dist/verify/render.js +15 -1
  137. package/npm-shrinkwrap.json +2383 -0
  138. package/package.json +14 -8
  139. package/NOTICE +0 -5
  140. package/dist/commands/completion.js +0 -116
  141. package/dist/commands/explain.js +0 -232
  142. package/dist/commands/unavailable.js +0 -11
  143. package/dist/telemetry/events.js +0 -40
@@ -3,9 +3,9 @@ import { clearAuthState } from "../auth/store.js";
3
3
  export const logoutCommand = {
4
4
  name: "logout",
5
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."],
6
+ usage: "dg logout",
7
+ flags: [{ flag: "--yes", summary: "Accepted for scripts; logout no longer needs confirmation." }],
8
+ details: ["Removes the dg-owned local auth token only. Config and setup files are not changed; log back in with dg login."],
9
9
  handler: (context) => logoutHandler(context.args)
10
10
  };
11
11
  function logoutHandler(args, env = process.env) {
@@ -17,13 +17,6 @@ function logoutHandler(args, env = process.env) {
17
17
  stderr: `dg logout: unknown option '${unknown}'. Run 'dg logout --help'.\n`
18
18
  };
19
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
20
  let removed;
28
21
  try {
29
22
  removed = clearAuthState();
@@ -35,7 +28,7 @@ function logoutHandler(args, env = process.env) {
35
28
  stderr: `dg logout: could not remove the local auth token: ${error instanceof Error ? error.message : "unknown error"}\n`
36
29
  };
37
30
  }
38
- const lines = [removed ? "Removed local dg auth token." : "No local dg auth token was present."];
31
+ const lines = [removed ? "Logged out." : "Already logged out."];
39
32
  const activeEnvVars = ["DG_API_KEY", "DG_API_TOKEN"].filter((name) => env[name]);
40
33
  if (activeEnvVars.length > 0) {
41
34
  lines.push(`${activeEnvVars.join(" and ")} ${activeEnvVars.length === 1 ? "is" : "are"} still set, so env-var auth remains active (file token removed only).`);
@@ -1,11 +1,13 @@
1
1
  import { configCommand } from "./config.js";
2
- import { completionCommand } from "./completion.js";
3
2
  import { auditCommand } from "./audit.js";
3
+ import { cooldownCommand } from "./cooldown.js";
4
+ import { decisionsCommand } from "./decisions.js";
4
5
  import { doctorCommand } from "./doctor.js";
5
- import { explainCommand } from "./explain.js";
6
6
  import { guardCommitCommand } from "./guard-commit.js";
7
+ import { agentsCommand } from "./agents.js";
7
8
  import { renderCommandHelp, renderRootHelp } from "./help.js";
8
9
  import { licensesCommand } from "./licenses.js";
10
+ import { sbomCommand } from "./sbom.js";
9
11
  import { loginCommand } from "./login.js";
10
12
  import { logoutCommand } from "./logout.js";
11
13
  import { packageManagerCommands } from "./wrap.js";
@@ -21,18 +23,20 @@ import { verifyCommand } from "./verify.js";
21
23
  import { dgVersion, versionResult } from "./version.js";
22
24
  export const commandCatalog = [
23
25
  scanCommand,
26
+ sbomCommand,
24
27
  verifyCommand,
25
28
  setupCommand,
26
29
  guardCommitCommand,
30
+ agentsCommand,
31
+ decisionsCommand,
32
+ cooldownCommand,
27
33
  uninstallCommand,
28
34
  doctorCommand,
29
35
  statusCommand,
30
36
  ...packageManagerCommands(),
31
37
  loginCommand,
32
38
  logoutCommand,
33
- explainCommand,
34
39
  configCommand,
35
- completionCommand,
36
40
  licensesCommand,
37
41
  auditCommand,
38
42
  updateCommand,
@@ -0,0 +1,206 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { basename, resolve } from "node:path";
3
+ import { writeReportAtomic } from "../util/report-writer.js";
4
+ import { discoverScanProjects, SBOM_LOCKFILE_ECOSYSTEMS } from "../scan/collect.js";
5
+ import { buildCycloneDxSbom, collectSbomComponents, readRootComponent } from "../sbom/cyclonedx.js";
6
+ import { resolvePresentation } from "../presentation/mode.js";
7
+ import { createTheme } from "../presentation/theme.js";
8
+ import { buildSbomRows } from "../sbom-ui/inventory.js";
9
+ import { launchSbomTui, shouldLaunchSbomTui } from "../sbom-ui/launch.js";
10
+ import { dgVersion } from "./version.js";
11
+ import { EXIT_TOOL_ERROR, EXIT_USAGE } from "./types.js";
12
+ export const sbomCommand = {
13
+ name: "sbom",
14
+ summary: "Inventory the project's dependencies as a CycloneDX 1.5 software bill of materials.",
15
+ usage: "dg sbom [path] [--output <path>] [--reproducible]",
16
+ args: [{ name: "[path]", summary: "Project directory to inventory (default: current directory)." }],
17
+ flags: [
18
+ { flag: "--output", value: "<path>", summary: "Write the document to a file without opening the interactive view (alias -o)." },
19
+ { flag: "--json", summary: "Print the raw CycloneDX document to stdout instead of the interactive view." },
20
+ { flag: "--reproducible", summary: "Byte-stable output: derive the serial number from the components and drop the timestamp (honors SOURCE_DATE_EPOCH)." }
21
+ ],
22
+ examples: ["dg sbom", "dg sbom -o sbom.cdx.json", "dg sbom --json | cyclonedx validate", "dg sbom --reproducible"],
23
+ details: [
24
+ "Reads the project's lockfiles and inventories every resolved dependency as a CycloneDX 1.5 component with its purl, license, and integrity hash.",
25
+ "In a terminal it opens an interactive view: the inventory appears immediately, then dg's scanner runs over the npm and PyPI components and fills in a BLOCK/WARN/PASS verdict per package — malware, provenance downgrades, and cooldown — while cargo stays inventory only. Browse with the arrow keys, search with /, filter with f, and press w to write the document. Signing in unlocks verdicts; without it the view shows the inventory alone.",
26
+ "Piped, with -o, or with --json it stays offline and prints the raw CycloneDX document so you can attach it to a release or feed any CycloneDX-aware tool — no scan, no account needed. Use --reproducible (or set SOURCE_DATE_EPOCH) for identical bytes across runs so a committed SBOM diffs cleanly."
27
+ ],
28
+ handler: (context) => runSbomCommand(context)
29
+ };
30
+ export function runSbomCommand(context, cwd = process.cwd(), now = new Date(), env = process.env) {
31
+ const parsed = parseArgs(context.args);
32
+ if ("error" in parsed) {
33
+ return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg sbom: ${parsed.error}. Usage: ${sbomCommand.usage}\n` };
34
+ }
35
+ const root = resolve(cwd, parsed.targetPath ?? ".");
36
+ const projects = discoverScanProjects(root, SBOM_LOCKFILE_ECOSYSTEMS);
37
+ const { components, dropped } = collectSbomComponents(projects);
38
+ const rootComponent = readRootComponent(root);
39
+ const reproducible = parsed.reproducible || env.SOURCE_DATE_EPOCH !== undefined;
40
+ const bom = buildCycloneDxSbom(components, {
41
+ ...timestampOption(reproducible, now, env),
42
+ serialNumber: "urn:uuid:00000000-0000-0000-0000-000000000000",
43
+ toolVersion: dgVersion(),
44
+ ...(rootComponent.component ? { rootComponent: rootComponent.component } : {})
45
+ });
46
+ const finalBom = {
47
+ ...bom,
48
+ serialNumber: reproducible ? deterministicSerialNumber(bom) : `urn:uuid:${randomUUID()}`
49
+ };
50
+ const document = `${JSON.stringify(finalBom, null, 2)}\n`;
51
+ const notes = [];
52
+ if (components.length === 0) {
53
+ notes.push(`dg sbom: no resolvable dependencies found under ${root} — emitting an empty SBOM.`);
54
+ }
55
+ if (dropped.length > 0) {
56
+ notes.push(`dg sbom: omitted ${dropped.length} dependency(ies) without a pinned version (${formatDropped(dropped)}). Pin them (e.g. name==version) to include them.`);
57
+ }
58
+ if (rootComponent.malformed) {
59
+ notes.push(`dg sbom: ${root}/package.json could not be parsed — the SBOM has no named subject component.`);
60
+ }
61
+ const stderr = notes.length > 0 ? `${notes.join("\n")}\n` : "";
62
+ if (parsed.outputPath) {
63
+ try {
64
+ writeReportAtomic(resolve(cwd, parsed.outputPath), document);
65
+ }
66
+ catch (error) {
67
+ return { exitCode: EXIT_TOOL_ERROR, stdout: "", stderr: `dg sbom: could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n` };
68
+ }
69
+ return { exitCode: 0, stdout: `Wrote CycloneDX SBOM (${components.length} components) to ${parsed.outputPath}\n`, stderr };
70
+ }
71
+ const presentation = resolvePresentation();
72
+ if (parsed.json || !presentation.isTTY) {
73
+ return { exitCode: 0, stdout: document, stderr };
74
+ }
75
+ const subject = finalBom.metadata.component
76
+ ? `${finalBom.metadata.component.name}${finalBom.metadata.component.version ? `@${finalBom.metadata.component.version}` : ""}`
77
+ : basename(root) || ".";
78
+ if (shouldLaunchSbomTui({ json: parsed.json, outputPath: parsed.outputPath })) {
79
+ void launchSbomTui({ rows: buildSbomRows(components), dropped, subject, document, env, cwd }).then(() => process.exit(typeof process.exitCode === "number" ? process.exitCode : 0), (error) => {
80
+ process.stderr.write(`dg sbom view failed: ${error instanceof Error ? error.message : "unknown error"}\n`);
81
+ process.exit(1);
82
+ });
83
+ return { exitCode: 0, stdout: "", stderr: "" };
84
+ }
85
+ return { exitCode: 0, stdout: renderSbomSummary(finalBom, dropped, subject, createTheme(presentation.color)), stderr };
86
+ }
87
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/gu;
88
+ function visibleWidth(text) {
89
+ return text.replace(ANSI_PATTERN, "").length;
90
+ }
91
+ export function renderSbomSummary(bom, dropped, subject, theme, terminalWidth = process.stdout.columns ?? 100) {
92
+ const components = bom.components;
93
+ const total = components.length;
94
+ const count = (prefix) => components.filter((c) => c["bom-ref"].startsWith(prefix)).length;
95
+ const ecosystems = [["npm", count("pkg:npm/")], ["pypi", count("pkg:pypi/")], ["cargo", count("pkg:cargo/")]]
96
+ .filter(([, n]) => n > 0)
97
+ .sort((a, b) => b[1] - a[1]);
98
+ const withLicense = components.filter((c) => c.licenses && c.licenses.length > 0).length;
99
+ const withHash = components.filter((c) => c.hashes && c.hashes.length > 0).length;
100
+ const accent = (text) => theme.paint("accent", text);
101
+ const muted = (text) => theme.paint("muted", text);
102
+ const warn = (text) => theme.paint("warn", text);
103
+ const denomWidth = String(total).length;
104
+ const license = { label: "License", covered: withLicense, deficit: total - withLicense, gap: "unknown" };
105
+ const integrity = { label: "Integrity", covered: withHash, deficit: total - withHash, gap: "no checksum" };
106
+ const gapWidth = Math.max(1, String(license.deficit).length, String(integrity.deficit).length);
107
+ const gapClause = (q) => (q.deficit > 0 ? `${String(q.deficit).padStart(gapWidth)} ${q.gap}` : "");
108
+ const clauseWidth = Math.max(gapClause(license).length, gapClause(integrity).length);
109
+ const qualityRow = (q) => {
110
+ const clause = gapClause(q);
111
+ const painted = clause ? warn(clause) : "";
112
+ const fraction = muted(`${String(q.covered).padStart(denomWidth)} / ${total} covered`);
113
+ return `${muted(q.label.padEnd(10))}${painted}${" ".repeat(clauseWidth - clause.length)} ${fraction}`;
114
+ };
115
+ const breakdown = ecosystems.map(([name, n]) => `${n} ${name}`).join(muted(" · "));
116
+ const rows = [
117
+ `${accent(String(total))} ${total === 1 ? "component" : "components"}${breakdown ? ` ${muted(breakdown)}` : ""}`,
118
+ "",
119
+ qualityRow(license),
120
+ qualityRow(integrity),
121
+ ...(dropped.length > 0
122
+ ? [`${muted("Omitted".padEnd(10))}${warn(`${String(dropped.length).padStart(gapWidth)} unpinned`)}${muted(", left out of the document")}`]
123
+ : []),
124
+ "",
125
+ muted(`CycloneDX 1.5 · ${subject} · inventory only`)
126
+ ];
127
+ const inner = Math.min(Math.max(0, ...rows.map(visibleWidth)), Math.max(24, terminalWidth - 4));
128
+ const bar = muted("─".repeat(inner + 2));
129
+ const side = muted("│");
130
+ const boxed = [
131
+ `${muted("╭")}${bar}${muted("╮")}`,
132
+ ...rows.map((row) => `${side} ${row}${" ".repeat(Math.max(0, inner - visibleWidth(row)))} ${side}`),
133
+ `${muted("╰")}${bar}${muted("╯")}`
134
+ ];
135
+ const footer = [
136
+ ` ${muted("→ write it:")} dg sbom -o sbom.cdx.json ${muted("· --json prints it")}`,
137
+ ` ${accent("dg scan")} ${muted("for BLOCK / WARN / PASS verdicts")}`
138
+ ];
139
+ return `${[...boxed, ...footer].join("\n")}\n`;
140
+ }
141
+ function timestampOption(reproducible, now, env) {
142
+ if (!reproducible) {
143
+ return { timestamp: now.toISOString() };
144
+ }
145
+ const epoch = env.SOURCE_DATE_EPOCH;
146
+ if (epoch !== undefined && /^\d+$/.test(epoch.trim())) {
147
+ const date = new Date(Number.parseInt(epoch.trim(), 10) * 1000);
148
+ const iso = Number.isFinite(date.getTime()) ? date.toISOString() : "";
149
+ if (/^\d{4}-/u.test(iso)) {
150
+ return { timestamp: iso };
151
+ }
152
+ }
153
+ return {};
154
+ }
155
+ function deterministicSerialNumber(bom) {
156
+ const hex = createHash("sha256").update(JSON.stringify(bom.components)).digest("hex");
157
+ const version = `4${hex.slice(13, 16)}`;
158
+ const variantNibble = ((Number.parseInt(hex.slice(16, 17) || "0", 16) & 0x3) | 0x8).toString(16);
159
+ const variant = `${variantNibble}${hex.slice(17, 20)}`;
160
+ return `urn:uuid:${hex.slice(0, 8)}-${hex.slice(8, 12)}-${version}-${variant}-${hex.slice(20, 32)}`;
161
+ }
162
+ function formatDropped(dropped) {
163
+ const shown = dropped.slice(0, 5).join(", ");
164
+ return dropped.length > 5 ? `${shown}, +${dropped.length - 5} more` : shown;
165
+ }
166
+ function parseArgs(args) {
167
+ let targetPath = null;
168
+ let outputPath = null;
169
+ let reproducible = false;
170
+ let json = false;
171
+ for (let i = 0; i < args.length; i += 1) {
172
+ const arg = args[i];
173
+ if (arg === undefined) {
174
+ continue;
175
+ }
176
+ if (arg === "--output" || arg === "-o") {
177
+ const value = args[i + 1];
178
+ if (value === undefined || value.startsWith("-")) {
179
+ return { error: `${arg} needs a path` };
180
+ }
181
+ outputPath = value;
182
+ i += 1;
183
+ continue;
184
+ }
185
+ if (arg.startsWith("--output=")) {
186
+ outputPath = arg.slice("--output=".length);
187
+ continue;
188
+ }
189
+ if (arg === "--reproducible") {
190
+ reproducible = true;
191
+ continue;
192
+ }
193
+ if (arg === "--json") {
194
+ json = true;
195
+ continue;
196
+ }
197
+ if (arg.startsWith("-")) {
198
+ return { error: `unknown flag '${arg}'` };
199
+ }
200
+ if (targetPath !== null) {
201
+ return { error: `unexpected argument '${arg}'` };
202
+ }
203
+ targetPath = arg;
204
+ }
205
+ return { targetPath, outputPath, reproducible, json };
206
+ }
@@ -8,11 +8,12 @@ export const scanCommand = {
8
8
  { flag: "--json", summary: "Machine-readable JSON report." },
9
9
  { flag: "--sarif", summary: "SARIF report for code-scanning tools." },
10
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)." }
11
+ { flag: "--staged", summary: "Scan only the git-staged lockfile changes (what dg guard-commit runs)." },
12
+ { flag: "--no-decisions", summary: "Ignore acceptances remembered in dg.json (see dg decisions)." }
12
13
  ],
13
14
  examples: ["dg scan", "dg scan ./packages/api", "dg scan --json -o scan.json", "dg scan --staged"],
14
15
  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, 64 usage error."
16
+ "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, 10 nothing to scan, 64 usage error."
16
17
  ],
17
18
  handler: runScanCommand
18
19
  };
@@ -2,6 +2,8 @@ import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
2
2
  import { renderCommandHelp } from "./help.js";
3
3
  import { buildServiceUninstallPlan, buildTrustInstallPlan, buildTrustUninstallPlan, installServiceTrust, readServiceState, renderServicePlan, resolveServicePaths, restartService, ServiceNotConfiguredError, ServiceProxyError, ServiceTrustStoreError, ServiceTrustToolMissingError, startService, stopService, uninstallService, uninstallServiceTrust } from "../service/state.js";
4
4
  import { LockBusyError } from "../state/locks.js";
5
+ import { resolvePresentation } from "../presentation/mode.js";
6
+ import { createTheme } from "../presentation/theme.js";
5
7
  const trustCommand = {
6
8
  name: "trust",
7
9
  summary: "Manage explicit service-mode trust installation.",
@@ -208,10 +210,12 @@ function mutationPlanWrites(action) {
208
210
  function serviceMutation(action, run) {
209
211
  try {
210
212
  const result = run();
213
+ const theme = createTheme(resolvePresentation().color);
211
214
  const changed = result.changed ? actionPastTense(action) : `already ${action === "start" ? "running" : "stopped"}`;
215
+ const detail = result.state.running && result.state.proxy ? ` ${theme.paint("muted", `at ${result.state.proxy.proxyUrl}`)}` : "";
212
216
  return {
213
217
  exitCode: 0,
214
- stdout: `Dependency Guardian service ${changed}.\n${renderServiceStatus(result.state)}`,
218
+ stdout: `${theme.paint("pass", "✓")} Service ${changed}${detail} ${theme.paint("muted", "· dg service status")}\n`,
215
219
  stderr: ""
216
220
  };
217
221
  }
@@ -399,20 +403,34 @@ function parseJsonArgs(command, args) {
399
403
  };
400
404
  }
401
405
  function renderServiceStatus(state) {
406
+ const theme = createTheme(resolvePresentation().color);
407
+ const pass = (text) => `${theme.paint("pass", "✓")} ${text}`;
408
+ const warn = (text) => `${theme.paint("warn", "⚠")} ${text}`;
409
+ const configured = state.configured
410
+ ? pass("yes")
411
+ : warn(`no — run ${theme.paint("accent", "dg setup --service --yes")}`);
412
+ const running = state.running && state.proxy
413
+ ? pass(`yes ${theme.paint("muted", `(${state.proxy.proxyUrl})`)}`)
414
+ : warn(state.configured ? `no — run ${theme.paint("accent", "dg service start")}` : "no");
415
+ const trust = state.trust
416
+ ? pass(`${state.trust.provider} ${theme.paint("muted", `(${state.trust.fingerprintSha256.slice(0, 12)}…)`)}`)
417
+ : warn(`not installed — run ${theme.paint("accent", "dg service trust install --yes")}`);
402
418
  const lines = [
403
- "Dependency Guardian service status",
419
+ "Dependency Guardian service",
404
420
  "",
405
- `configured: ${state.configured ? "yes" : "no"}`,
406
- `running: ${state.running ? "yes" : "no"}`,
407
- `trust installed: ${state.trustInstalled ? "yes" : "no"}`,
408
- `trust provider: ${state.trust ? state.trust.provider : "not installed"}`,
409
- `trust fingerprint: ${state.trust ? state.trust.fingerprintSha256 : "not installed"}`,
410
- `trust drift: ${state.trustDrift ? state.trustDrift.message : "none"}`,
411
- `service proxy: ${state.proxy ? state.proxy.proxyUrl : "not running"}`,
412
- `health endpoint: ${state.proxy ? state.proxy.healthUrl : "not running"}`,
413
- `last error: ${state.lastError ?? "none"}`,
414
- `policy synced: ${state.policySyncedAt ?? "never"}`
421
+ ` Configured ${configured}`,
422
+ ` Running ${running}`,
423
+ ` Trust ${trust}`
415
424
  ];
425
+ if (state.trustDrift) {
426
+ lines.push(` Trust drift ${warn(state.trustDrift.message)}`);
427
+ }
428
+ lines.push(` Policy sync ${state.policySyncedAt ? pass(state.policySyncedAt) : theme.paint("muted", "never")}`);
429
+ if (state.lastError) {
430
+ lines.push(` Last error ${warn(state.lastError)}`);
431
+ }
432
+ lines.push("");
433
+ lines.push(`Full diagnostics: ${theme.paint("accent", "dg service doctor")}`);
416
434
  return `${lines.join("\n")}\n`;
417
435
  }
418
436
  function serviceStatusJson(state) {
@@ -503,10 +521,11 @@ function actionPastTense(action) {
503
521
  return "restarted";
504
522
  }
505
523
  function serviceNotConfiguredResult() {
524
+ const theme = createTheme(resolvePresentation().color);
506
525
  return {
507
526
  exitCode: EXIT_USAGE,
508
527
  stdout: "",
509
- stderr: "dg service is not configured. Run 'dg setup --service --yes' first; no service or trust state was changed.\n"
528
+ stderr: `${theme.paint("warn", "dg service is not configured")} — run ${theme.paint("accent", "dg setup --service --yes")} first ${theme.paint("muted", "(no service or trust state was changed)")}\n`
510
529
  };
511
530
  }
512
531
  function trustStoreFailureResult(planText, action, message) {