@westbayberry/dg 2.1.0 → 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.
- package/README.md +216 -226
- package/dist/agents/claude-code.js +113 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +113 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +285 -0
- package/dist/agents/registry.js +127 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/windsurf.js +93 -0
- package/dist/api/analyze.js +1 -1
- package/dist/audit/detectors.js +0 -11
- package/dist/audit/events.js +5 -21
- package/dist/audit-ui/AuditApp.js +2 -0
- package/dist/audit-ui/components/AuditResultsView.js +55 -92
- package/dist/audit-ui/export.js +0 -4
- package/dist/audit-ui/format.js +0 -3
- package/dist/audit-ui/launch.js +10 -1
- package/dist/auth/device-login.js +4 -5
- package/dist/auth/login-app.js +7 -7
- package/dist/auth/store.js +8 -3
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +231 -0
- package/dist/commands/audit.js +22 -3
- package/dist/commands/config.js +15 -7
- package/dist/commands/cooldown.js +389 -0
- package/dist/commands/decisions.js +23 -9
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/help.js +1 -1
- package/dist/commands/licenses.js +10 -22
- package/dist/commands/logout.js +4 -11
- package/dist/commands/router.js +6 -4
- package/dist/commands/sbom.js +206 -0
- package/dist/commands/scan.js +1 -1
- package/dist/commands/service.js +32 -13
- package/dist/commands/setup.js +197 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +14 -44
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +2 -2
- package/dist/install-ui/block-render.js +1 -1
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +466 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +31 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +17 -6
- package/dist/launcher/env.js +71 -24
- package/dist/launcher/install-preflight.js +51 -14
- package/dist/launcher/live-install.js +25 -5
- package/dist/launcher/output-redaction.js +7 -4
- package/dist/launcher/preflight-prompt.js +14 -1
- package/dist/launcher/run.js +89 -86
- package/dist/launcher/spawn-invocation.js +21 -0
- package/dist/policy/cooldown.js +18 -5
- package/dist/policy/evaluate.js +5 -6
- package/dist/policy/pypi-name.js +17 -0
- package/dist/presentation/mode.js +3 -2
- package/dist/presentation/package-page.js +9 -0
- package/dist/presentation/theme.js +7 -7
- package/dist/project/dgfile.js +145 -6
- package/dist/proxy/auth.js +42 -0
- package/dist/proxy/ca.js +29 -9
- package/dist/proxy/cooldown-exemptions-file.js +33 -0
- package/dist/proxy/enforcement.js +55 -16
- package/dist/proxy/metadata-map.js +41 -3
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +448 -49
- package/dist/proxy/worker.js +16 -1
- package/dist/publish-set/collect.js +1 -4
- package/dist/publish-set/npm.js +8 -5
- package/dist/publish-set/pack.js +9 -3
- package/dist/runtime/cli.js +0 -4
- package/dist/runtime/fatal.js +31 -0
- package/dist/runtime/first-run.js +12 -11
- package/dist/runtime/node-version.js +43 -6
- package/dist/runtime/nudges.js +35 -2
- package/dist/sbom/cyclonedx.js +211 -0
- package/dist/sbom-ui/SbomApp.js +158 -0
- package/dist/sbom-ui/components/SbomHeader.js +32 -0
- package/dist/sbom-ui/components/SbomList.js +52 -0
- package/dist/sbom-ui/inventory.js +128 -0
- package/dist/sbom-ui/launch.js +51 -0
- package/dist/sbom-ui/run.js +55 -0
- package/dist/sbom-ui/store.js +26 -0
- package/dist/scan/collect.js +10 -6
- package/dist/scan/command.js +17 -10
- package/dist/scan/discovery.js +11 -2
- package/dist/scan/render.js +28 -4
- package/dist/scan/scanner-report.js +15 -9
- package/dist/scan/staged.js +2 -1
- package/dist/scan-ui/LegacyApp.js +9 -13
- package/dist/scan-ui/alt-screen.js +5 -8
- package/dist/scan-ui/components/InteractiveResultsView.js +133 -120
- package/dist/scan-ui/components/ProgressBar.js +3 -14
- package/dist/scan-ui/components/ProjectSelector.js +1 -1
- package/dist/scan-ui/components/ScoreHeader.js +2 -3
- package/dist/scan-ui/components/SetupBanner.js +0 -6
- package/dist/scan-ui/format-helpers.js +61 -5
- package/dist/scan-ui/hooks/useResizeRepaint.js +25 -0
- package/dist/scan-ui/hooks/useScan.js +16 -2
- package/dist/scan-ui/launch.js +7 -4
- package/dist/scan-ui/shims.js +11 -4
- package/dist/scripts/detect.js +7 -2
- package/dist/scripts/gate.js +1 -1
- package/dist/service/state.js +27 -8
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/worker.js +23 -1
- package/dist/setup/activate-shell.js +28 -0
- package/dist/setup/git-hook.js +49 -4
- package/dist/setup/plan.js +73 -39
- package/dist/setup-ui/gate.js +39 -0
- package/dist/setup-ui/offer.js +42 -0
- package/dist/setup-ui/selector.js +27 -0
- package/dist/setup-ui/tasks.js +56 -0
- package/dist/setup-ui/wizard.js +225 -0
- package/dist/state/cooldown-held.js +66 -0
- package/dist/state/index.js +1 -0
- package/dist/state/locks.js +4 -2
- package/dist/state/store.js +2 -1
- package/dist/util/external-tool.js +25 -0
- package/dist/util/git.js +10 -3
- package/dist/util/report-writer.js +57 -0
- package/dist/verify/local.js +240 -42
- package/dist/verify/package-check.js +84 -28
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +6 -8
- package/NOTICE +0 -5
- package/dist/commands/completion.js +0 -117
- package/dist/commands/explain.js +0 -236
- package/dist/commands/unavailable.js +0 -11
- package/dist/scripts/rebuild.js +0 -28
- package/dist/telemetry/events.js +0 -40
|
@@ -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
|
+
}
|
package/dist/commands/scan.js
CHANGED
|
@@ -13,7 +13,7 @@ export const scanCommand = {
|
|
|
13
13
|
],
|
|
14
14
|
examples: ["dg scan", "dg scan ./packages/api", "dg scan --json -o scan.json", "dg scan --staged"],
|
|
15
15
|
details: [
|
|
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, 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."
|
|
17
17
|
],
|
|
18
18
|
handler: runScanCommand
|
|
19
19
|
};
|
package/dist/commands/service.js
CHANGED
|
@@ -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:
|
|
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
|
|
419
|
+
"Dependency Guardian service",
|
|
404
420
|
"",
|
|
405
|
-
`
|
|
406
|
-
`
|
|
407
|
-
`
|
|
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.
|
|
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) {
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,34 +1,40 @@
|
|
|
1
1
|
import { EXIT_UNAVAILABLE, EXIT_USAGE } from "./types.js";
|
|
2
2
|
import { LockBusyError } from "../state/index.js";
|
|
3
|
-
import { activationCommand as shellActivationCommand, applySetupPlanWithLock, buildSetupPlan, renderSetupPlan } from "../setup/plan.js";
|
|
3
|
+
import { activationCommand as shellActivationCommand, applySetupPlanWithLock, buildSetupPlan, renderSetupPlan, tildifyPath as tildify } from "../setup/plan.js";
|
|
4
|
+
import { activateShell, activationOffer } from "../setup/activate-shell.js";
|
|
4
5
|
import { buildServiceSetupPlan, configureService, renderServicePlan } from "../service/state.js";
|
|
5
6
|
import { optionalSupportGate } from "../setup/optional-support.js";
|
|
6
7
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
7
8
|
import { createTheme } from "../presentation/theme.js";
|
|
8
9
|
import { closeSync, openSync, readSync } from "node:fs";
|
|
9
|
-
import { homedir } from "node:os";
|
|
10
10
|
import { authStatus } from "../auth/store.js";
|
|
11
|
+
import { resolveWebBase } from "../auth/device-login.js";
|
|
12
|
+
import { applyAgentHook, collectAgentOffers } from "../agents/registry.js";
|
|
13
|
+
import { applyGitHook, commitGuardOffer } from "../setup/git-hook.js";
|
|
14
|
+
import { markSecurityNotesShown, securityNotesShown } from "../setup-ui/gate.js";
|
|
11
15
|
export const setupCommand = {
|
|
12
16
|
name: "setup",
|
|
13
|
-
summary: "Protect
|
|
14
|
-
usage: "dg setup [--print] [--yes] [--shell <auto|zsh|bash|fish>] [--service]",
|
|
17
|
+
summary: "Protect this machine — shell installs, AI-agent installs, and a repo's commits.",
|
|
18
|
+
usage: "dg setup [--print] [--yes] [--agents] [--guard-commit] [--shell <auto|zsh|bash|fish>] [--service]",
|
|
15
19
|
flags: [
|
|
16
20
|
{ flag: "--print", summary: "Preview the exact write plan and change nothing." },
|
|
17
|
-
{ flag: "--yes", summary: "Apply
|
|
21
|
+
{ flag: "--yes", summary: "Apply the shell firewall only, without prompts (add --agents / --guard-commit to opt in the rest)." },
|
|
22
|
+
{ flag: "--agents", summary: "With --yes: also route every detected AI agent's installs through dg." },
|
|
23
|
+
{ flag: "--guard-commit", summary: "With --yes: also scan this repo's commits (per-repo)." },
|
|
18
24
|
{ flag: "--shell", value: "<auto|zsh|bash|fish>", summary: "Target shell rc to write (default: auto-detect)." },
|
|
19
25
|
{ flag: "--service", summary: "Set up service mode (Pro/Team; persistent proxy + managed CA)." }
|
|
20
26
|
],
|
|
21
|
-
examples: ["dg setup --print", "dg setup --yes", "dg setup --shell zsh --yes"],
|
|
27
|
+
examples: ["dg setup", "dg setup --print", "dg setup --yes --agents", "dg setup --shell zsh --yes"],
|
|
22
28
|
details: [
|
|
23
|
-
"In a terminal
|
|
24
|
-
"
|
|
29
|
+
"In a terminal it shows one consent screen listing everything it will protect — your shell's installs plus every detected AI agent, with the exact files it writes — and applies them on a single yes. Commits in the current repo are a separate per-repo question.",
|
|
30
|
+
"Non-interactively, --yes applies just the shell firewall; add --agents and/or --guard-commit to opt those in. --print previews. Writes only dg-owned reversible files; undo it all with dg uninstall."
|
|
25
31
|
],
|
|
26
32
|
handler: (context) => setupHandler(context.args)
|
|
27
33
|
};
|
|
28
34
|
const WINDOWS_UNSUPPORTED = "dg setup (automatic interception) does not support Windows yet.\n" +
|
|
29
35
|
"You can still protect installs on Windows: prefix the command with dg,\n" +
|
|
30
36
|
"for example `dg pip install <pkg>` or `dg npm install <pkg>`.\n";
|
|
31
|
-
function setupHandler(args) {
|
|
37
|
+
async function setupHandler(args) {
|
|
32
38
|
const parsed = parseSetupArgs(args);
|
|
33
39
|
if ("error" in parsed) {
|
|
34
40
|
return {
|
|
@@ -54,12 +60,16 @@ function setupHandler(args) {
|
|
|
54
60
|
if (parsed.printOnly) {
|
|
55
61
|
return {
|
|
56
62
|
exitCode: 0,
|
|
57
|
-
stdout: renderedPlan
|
|
63
|
+
stdout: `${renderedPlan}${printableSurfaceOffers()}`,
|
|
58
64
|
stderr: ""
|
|
59
65
|
};
|
|
60
66
|
}
|
|
61
67
|
if (!parsed.yes) {
|
|
62
68
|
if (isInteractive()) {
|
|
69
|
+
if (resolvePresentation().mode === "rich") {
|
|
70
|
+
const { runSetupWizard } = await import("../setup-ui/wizard.js");
|
|
71
|
+
return runSetupWizard(plan, { autoActivate: true });
|
|
72
|
+
}
|
|
63
73
|
return runInteractiveSetup(plan);
|
|
64
74
|
}
|
|
65
75
|
return {
|
|
@@ -81,23 +91,34 @@ function setupHandler(args) {
|
|
|
81
91
|
}
|
|
82
92
|
throw error;
|
|
83
93
|
}
|
|
94
|
+
const extra = await applyRequestedSurfaces(parsed);
|
|
95
|
+
const theme = createTheme(resolvePresentation().color);
|
|
84
96
|
return {
|
|
85
97
|
exitCode: 0,
|
|
86
|
-
stdout: `${
|
|
98
|
+
stdout: `${theme.paint("pass", "✓")} Setup complete — active in new terminals ${theme.paint("muted", `(wrote ${tildify(plan.shimDir)} and ${tildify(plan.rcPath)}; details: dg doctor)`)}\n${extra}Activate this shell now: ${theme.paint("accent", activationCommand(plan))}\n`,
|
|
87
99
|
stderr: ""
|
|
88
100
|
};
|
|
89
101
|
}
|
|
90
102
|
function isInteractive() {
|
|
91
103
|
return Boolean(process.stdin.isTTY && process.stderr.isTTY);
|
|
92
104
|
}
|
|
93
|
-
function runInteractiveSetup(plan) {
|
|
105
|
+
async function runInteractiveSetup(plan) {
|
|
94
106
|
const theme = createTheme(resolvePresentation().color);
|
|
95
107
|
const accent = (text) => theme.paint("accent", text);
|
|
96
108
|
const muted = (text) => theme.paint("muted", text);
|
|
97
109
|
const err = process.stderr;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
const agents = collectAgentOffers();
|
|
111
|
+
const repo = commitGuardOffer();
|
|
112
|
+
if (!securityNotesShown()) {
|
|
113
|
+
err.write(renderSecurityNotesText(theme));
|
|
114
|
+
err.write(` ${muted("Press Enter to agree")} `);
|
|
115
|
+
if (readLineFromTty() !== null) {
|
|
116
|
+
markSecurityNotesShown();
|
|
117
|
+
}
|
|
118
|
+
err.write("\n");
|
|
119
|
+
}
|
|
120
|
+
err.write(renderConsentScreen(consentSurfaces(plan, agents, repo !== null), theme));
|
|
121
|
+
const answer = promptYesNoSync(` ${accent("Protect installs on this machine?")}`, theme, true);
|
|
101
122
|
if (answer === "no-tty") {
|
|
102
123
|
return {
|
|
103
124
|
exitCode: EXIT_USAGE,
|
|
@@ -121,21 +142,152 @@ function runInteractiveSetup(plan) {
|
|
|
121
142
|
}
|
|
122
143
|
throw error;
|
|
123
144
|
}
|
|
124
|
-
err.write(`\n ${theme.paint("pass", "✓
|
|
125
|
-
|
|
145
|
+
err.write(`\n ${theme.paint("pass", "✓ shell installs protected — active in new terminals")}\n`);
|
|
146
|
+
await applyAgentOffers(agents, theme, err);
|
|
147
|
+
promptCommitSurface(repo, theme, err);
|
|
148
|
+
return finishWithActivation(plan, theme, err);
|
|
149
|
+
}
|
|
150
|
+
export function consentSurfaces(plan, agents, repoOffered) {
|
|
151
|
+
const surfaces = [
|
|
152
|
+
{ label: "shell installs", detail: `writes ${tildify(plan.rcPath)}`, deferred: false }
|
|
153
|
+
];
|
|
154
|
+
for (const offer of agents) {
|
|
155
|
+
surfaces.push({ label: `${offer.label} installs`, detail: `writes ${tildify(offer.ctx.settingsPath)}`, deferred: false });
|
|
156
|
+
}
|
|
157
|
+
if (repoOffered) {
|
|
158
|
+
surfaces.push({ label: "commits in this repo", detail: "asked separately", deferred: true });
|
|
159
|
+
}
|
|
160
|
+
return surfaces;
|
|
161
|
+
}
|
|
162
|
+
export function renderConsentScreen(surfaces, theme) {
|
|
163
|
+
const accent = (text) => theme.paint("accent", text);
|
|
164
|
+
const muted = (text) => theme.paint("muted", text);
|
|
165
|
+
const width = Math.max(...surfaces.map((surface) => surface.label.length));
|
|
166
|
+
const lines = [
|
|
167
|
+
"",
|
|
168
|
+
` Scans ${accent("npm")}/${accent("pip")}/${accent("yarn")}/${muted("…")} installs automatically — no ${accent("dg")} prefix.`,
|
|
169
|
+
` ${muted("Reversible with")} ${accent("dg uninstall")}${muted(".")}`,
|
|
170
|
+
"",
|
|
171
|
+
` ${muted("Sets up:")}`
|
|
172
|
+
];
|
|
173
|
+
for (const surface of surfaces) {
|
|
174
|
+
const label = surface.label.padEnd(width);
|
|
175
|
+
lines.push(surface.deferred ? ` ${muted(label)} ${muted(surface.detail)}` : ` ${accent(label)} ${muted(surface.detail)}`);
|
|
176
|
+
}
|
|
177
|
+
lines.push("", "");
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
function finishWithActivation(plan, theme, err) {
|
|
181
|
+
const offer = activationOffer();
|
|
182
|
+
if (offer === "none") {
|
|
183
|
+
return { exitCode: 0, stdout: "", stderr: "" };
|
|
184
|
+
}
|
|
185
|
+
const accent = (text) => theme.paint("accent", text);
|
|
186
|
+
const muted = (text) => theme.paint("muted", text);
|
|
187
|
+
if (offer === "prompt" && promptYesNoSync(`\n ${accent("Activate in this terminal now?")}`, theme, true) === "yes") {
|
|
188
|
+
err.write(` ${muted("Starting a protected shell — type")} ${accent("exit")} ${muted("to return to your previous one.")}\n`);
|
|
189
|
+
return { exitCode: activateShell(), stdout: "", stderr: "" };
|
|
190
|
+
}
|
|
191
|
+
err.write(`\n ${muted("Activate this shell now:")} ${accent(activationCommand(plan))}\n`);
|
|
126
192
|
return { exitCode: 0, stdout: "", stderr: "" };
|
|
127
193
|
}
|
|
194
|
+
async function applyAgentOffers(offers, theme, err) {
|
|
195
|
+
const muted = (text) => theme.paint("muted", text);
|
|
196
|
+
for (const offer of offers) {
|
|
197
|
+
try {
|
|
198
|
+
await applyAgentHook(offer.ctx);
|
|
199
|
+
err.write(` ${theme.paint("pass", `✓ ${offer.label} installs route through dg`)} ${muted(`(${tildify(offer.ctx.settingsPath)})`)}\n`);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
err.write(` ${theme.paint("warn", `✗ ${offer.label}`)} ${muted(`— ${error instanceof Error ? error.message : "unknown error"}`)}\n`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function promptCommitSurface(repo, theme, err) {
|
|
207
|
+
if (!repo) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const accent = (text) => theme.paint("accent", text);
|
|
211
|
+
const muted = (text) => theme.paint("muted", text);
|
|
212
|
+
err.write(`\n ${muted("This is a git repo. Scan its commits before they land?")}\n`);
|
|
213
|
+
if (promptYesNoSync(` ${accent("Guard commits in this repo?")}`, theme, false) !== "yes") {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
applyGitHook(repo);
|
|
218
|
+
err.write(` ${theme.paint("pass", "✓ commits in this repo are now scanned")}\n`);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
err.write(` ${theme.paint("warn", `✗ commits in this repo`)} ${muted(`— ${error instanceof Error ? error.message : "unknown error"}`)}\n`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function applyRequestedSurfaces(parsed) {
|
|
225
|
+
const lines = [];
|
|
226
|
+
if (parsed.agents) {
|
|
227
|
+
for (const offer of collectAgentOffers()) {
|
|
228
|
+
try {
|
|
229
|
+
await applyAgentHook(offer.ctx);
|
|
230
|
+
lines.push(`${offer.label} installs now route through dg (${tildify(offer.ctx.settingsPath)}).`);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
lines.push(`could not hook ${offer.label}: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (parsed.guardCommit) {
|
|
238
|
+
const repo = commitGuardOffer();
|
|
239
|
+
if (repo) {
|
|
240
|
+
try {
|
|
241
|
+
applyGitHook(repo);
|
|
242
|
+
lines.push("commits in this repo are now scanned.");
|
|
243
|
+
}
|
|
244
|
+
catch (error) {
|
|
245
|
+
lines.push(`could not install the commit guard: ${error instanceof Error ? error.message : "unknown error"}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return lines.length > 0 ? `${lines.join("\n")}\n` : "";
|
|
250
|
+
}
|
|
251
|
+
function printableSurfaceOffers() {
|
|
252
|
+
const lines = [];
|
|
253
|
+
for (const offer of collectAgentOffers()) {
|
|
254
|
+
lines.push(`would offer: route ${offer.label} installs through dg (--agents)`);
|
|
255
|
+
}
|
|
256
|
+
if (commitGuardOffer()) {
|
|
257
|
+
lines.push("would offer: scan this repo's commits (--guard-commit)");
|
|
258
|
+
}
|
|
259
|
+
return lines.length > 0 ? `\n${lines.join("\n")}\n` : "";
|
|
260
|
+
}
|
|
128
261
|
function activationCommand(plan) {
|
|
129
262
|
return shellActivationCommand(plan.shell, tildify(plan.rcPath));
|
|
130
263
|
}
|
|
131
|
-
function
|
|
132
|
-
|
|
264
|
+
function renderSecurityNotesText(theme) {
|
|
265
|
+
const muted = (text) => theme.paint("muted", text);
|
|
266
|
+
const webBase = resolveWebBase(process.env);
|
|
267
|
+
return [
|
|
268
|
+
"",
|
|
269
|
+
` ${theme.paint("accent", "Security notes")}`,
|
|
270
|
+
"",
|
|
271
|
+
" 1. dg can make mistakes.",
|
|
272
|
+
" A PASS verdict does not guarantee a package is safe. You are",
|
|
273
|
+
" responsible for what you install and should review new",
|
|
274
|
+
" dependencies.",
|
|
275
|
+
"",
|
|
276
|
+
" 2. By continuing you confirm you have read and understand the",
|
|
277
|
+
" Terms of Service and Privacy Policy.",
|
|
278
|
+
` ${muted(`${webBase}/terms`)}`,
|
|
279
|
+
` ${muted(`${webBase}/privacy`)}`,
|
|
280
|
+
"",
|
|
281
|
+
""
|
|
282
|
+
].join("\n");
|
|
283
|
+
}
|
|
284
|
+
function readLineFromTty() {
|
|
133
285
|
let tty;
|
|
134
286
|
try {
|
|
135
287
|
tty = openSync("/dev/tty", "rs");
|
|
136
288
|
}
|
|
137
289
|
catch {
|
|
138
|
-
return
|
|
290
|
+
return null;
|
|
139
291
|
}
|
|
140
292
|
try {
|
|
141
293
|
const byte = Buffer.alloc(1);
|
|
@@ -160,22 +312,31 @@ function promptYesNoSync(question, theme) {
|
|
|
160
312
|
}
|
|
161
313
|
answer += char;
|
|
162
314
|
}
|
|
163
|
-
|
|
164
|
-
return normalized === "y" || normalized === "yes" ? "yes" : "no";
|
|
315
|
+
return answer;
|
|
165
316
|
}
|
|
166
317
|
finally {
|
|
167
318
|
closeSync(tty);
|
|
168
319
|
}
|
|
169
320
|
}
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
321
|
+
function promptYesNoSync(question, theme, defaultYes = false) {
|
|
322
|
+
process.stderr.write(`${question} ${theme.paint("muted", defaultYes ? "[Y/n]" : "[y/N]")} `);
|
|
323
|
+
const answer = readLineFromTty();
|
|
324
|
+
if (answer === null) {
|
|
325
|
+
return "no-tty";
|
|
326
|
+
}
|
|
327
|
+
const normalized = answer.trim().toLowerCase();
|
|
328
|
+
if (normalized === "") {
|
|
329
|
+
return defaultYes ? "yes" : "no";
|
|
330
|
+
}
|
|
331
|
+
return normalized === "y" || normalized === "yes" ? "yes" : "no";
|
|
173
332
|
}
|
|
174
333
|
function parseSetupArgs(args) {
|
|
175
334
|
let printOnly = false;
|
|
176
335
|
let yes = false;
|
|
177
336
|
let shell = "auto";
|
|
178
337
|
let service = false;
|
|
338
|
+
let agents = false;
|
|
339
|
+
let guardCommit = false;
|
|
179
340
|
for (let index = 0; index < args.length; index += 1) {
|
|
180
341
|
const arg = args[index];
|
|
181
342
|
if (arg === "--print") {
|
|
@@ -187,6 +348,12 @@ function parseSetupArgs(args) {
|
|
|
187
348
|
else if (arg === "--service") {
|
|
188
349
|
service = true;
|
|
189
350
|
}
|
|
351
|
+
else if (arg === "--agents") {
|
|
352
|
+
agents = true;
|
|
353
|
+
}
|
|
354
|
+
else if (arg === "--guard-commit") {
|
|
355
|
+
guardCommit = true;
|
|
356
|
+
}
|
|
190
357
|
else if (arg === "--python-hook" || arg?.startsWith("--python-hook=")) {
|
|
191
358
|
return {
|
|
192
359
|
exitCode: EXIT_UNAVAILABLE,
|
|
@@ -251,7 +418,9 @@ function parseSetupArgs(args) {
|
|
|
251
418
|
printOnly,
|
|
252
419
|
yes,
|
|
253
420
|
shell,
|
|
254
|
-
service
|
|
421
|
+
service,
|
|
422
|
+
agents,
|
|
423
|
+
guardCommit
|
|
255
424
|
};
|
|
256
425
|
}
|
|
257
426
|
function parseShell(value) {
|
|
@@ -270,10 +439,11 @@ function serviceSetupHandler(parsed) {
|
|
|
270
439
|
};
|
|
271
440
|
}
|
|
272
441
|
if (!authStatus().authenticated) {
|
|
442
|
+
const theme = createTheme(resolvePresentation().color);
|
|
273
443
|
return {
|
|
274
444
|
exitCode: EXIT_UNAVAILABLE,
|
|
275
445
|
stdout: "",
|
|
276
|
-
stderr: "dg setup --service is a Pro or Team feature.
|
|
446
|
+
stderr: `${theme.paint("warn", "dg setup --service is a Pro or Team feature")} — ${theme.paint("accent", "dg login")} ${theme.paint("muted", "with a paid account, then re-run")} ${theme.paint("muted", "· westbayberry.com/pricing")}\n`
|
|
277
447
|
};
|
|
278
448
|
}
|
|
279
449
|
if (!parsed.yes) {
|