@westbayberry/dg 2.1.0 → 2.3.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 +123 -0
- package/dist/agents/codex.js +65 -0
- package/dist/agents/copilot-cli.js +115 -0
- package/dist/agents/cursor.js +118 -0
- package/dist/agents/gate-posture.js +21 -0
- package/dist/agents/gemini.js +107 -0
- package/dist/agents/persistence.js +351 -0
- package/dist/agents/registry.js +128 -0
- package/dist/agents/routing.js +118 -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/rules.js +6 -2
- 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 +17 -5
- package/dist/bin/dg.js +59 -52
- package/dist/commands/agents.js +275 -0
- package/dist/commands/audit.js +28 -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 +196 -27
- package/dist/commands/types.js +1 -0
- package/dist/commands/uninstall.js +2 -1
- package/dist/commands/update.js +17 -8
- package/dist/commands/verify.js +8 -5
- package/dist/config/settings.js +49 -48
- package/dist/decisions/remember-prompt.js +5 -10
- package/dist/export-ui/ExportDialog.js +198 -0
- package/dist/install-ui/LiveInstall.js +5 -4
- package/dist/install-ui/block-render.js +18 -14
- package/dist/install-ui/prep-spinner.js +32 -0
- package/dist/launcher/agent-check.js +896 -0
- package/dist/launcher/agent-hook-exec.js +70 -0
- package/dist/launcher/agent-hook-io.js +39 -0
- package/dist/launcher/cargo-cache.js +40 -0
- package/dist/launcher/classify.js +44 -7
- package/dist/launcher/env.js +105 -26
- package/dist/launcher/install-preflight.js +64 -18
- package/dist/launcher/live-install.js +29 -8
- package/dist/launcher/manifest-screen.js +171 -0
- package/dist/launcher/output-redaction.js +10 -4
- package/dist/launcher/preflight-prompt.js +18 -4
- package/dist/launcher/run.js +178 -103
- 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 +146 -6
- package/dist/project/override-trust.js +0 -0
- 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 +66 -12
- package/dist/proxy/preverified.js +55 -0
- package/dist/proxy/server.js +568 -53
- 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 +14 -12
- 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 +132 -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 +33 -9
- 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 +19 -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/security/sanitize.js +8 -4
- package/dist/service/state.js +28 -9
- package/dist/service/trust-refresh.js +92 -0
- package/dist/service/trust-store.js +5 -9
- 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 +227 -49
- package/dist/setup/uninstall-standalone.js +25 -0
- 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 +233 -0
- package/dist/standalone/uninstall.mjs +2123 -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 +242 -44
- package/dist/verify/package-check.js +87 -29
- package/dist/verify/preflight.js +242 -49
- package/dist/verify/render.js +15 -1
- package/npm-shrinkwrap.json +2383 -0
- package/package.json +7 -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,389 @@
|
|
|
1
|
+
import { describeCooldownSettings, durationToDays, formatCooldownDuration } from "../policy/cooldown.js";
|
|
2
|
+
import { readHeldPackages } from "../state/index.js";
|
|
3
|
+
import { canonicalCooldownName } from "../policy/pypi-name.js";
|
|
4
|
+
import { ConfigError, loadUserConfig, setConfigValue, updateUserConfig } from "../config/settings.js";
|
|
5
|
+
import { appendCooldownExemptions, cooldownExemptionActive, CooldownExemptionCapError, findProjectRoot, loadDgFile, mutateDgFile, removeCooldownExemptions, resolveAcceptedBy } from "../project/dgfile.js";
|
|
6
|
+
import { EXIT_USAGE } from "./types.js";
|
|
7
|
+
export const cooldownCommand = {
|
|
8
|
+
name: "cooldown",
|
|
9
|
+
summary: "Hold new releases for a window before they can install, with per-package exemptions.",
|
|
10
|
+
usage: "dg cooldown [<window>|off] [--json] | dg cooldown exempt <name> [--reason <text>] [--expires <30d>] | dg cooldown rm <name>",
|
|
11
|
+
subcommands: [
|
|
12
|
+
{ name: "list", summary: "Show the cooldown window and exemptions (default).", usage: "dg cooldown list [--json]", details: [], handler: () => unreachable() },
|
|
13
|
+
{ name: "<window>", summary: "Set the window directly: dg cooldown 7d, dg cooldown 24h, dg cooldown off.", usage: "dg cooldown <7d|24h|off>", details: [], handler: () => unreachable() },
|
|
14
|
+
{ name: "exempt", summary: "Always allow a package through cooldown.", usage: "dg cooldown exempt <name> [--reason <text>] [--expires <30d>] [--ecosystem npm|pypi]", details: [], handler: () => unreachable() },
|
|
15
|
+
{ name: "rm", summary: "Drop an exemption so cooldown applies again.", usage: "dg cooldown rm <name> [--ecosystem npm|pypi]", details: [], handler: () => unreachable() },
|
|
16
|
+
{ name: "prune", summary: "Remove expired exemptions.", usage: "dg cooldown prune", details: [], handler: () => unreachable() }
|
|
17
|
+
],
|
|
18
|
+
flags: [
|
|
19
|
+
{ flag: "--reason", value: "<text>", summary: "Why this package is exempt (stored in dg.json)." },
|
|
20
|
+
{ flag: "--expires", value: "<30d>", summary: "Auto-expire the exemption after a duration (e.g. 14d, 90d)." },
|
|
21
|
+
{ flag: "--ecosystem", value: "<npm|pypi>", summary: "Ecosystem when the name is not prefixed (default npm)." },
|
|
22
|
+
{ flag: "--json", summary: "Machine-readable listing." }
|
|
23
|
+
],
|
|
24
|
+
examples: [
|
|
25
|
+
"dg cooldown",
|
|
26
|
+
"dg cooldown 7d",
|
|
27
|
+
"dg cooldown off",
|
|
28
|
+
"dg cooldown exempt left-pad --reason 'vendored, pinned'",
|
|
29
|
+
"dg cooldown exempt pypi:requests --expires 30d",
|
|
30
|
+
"dg cooldown rm left-pad"
|
|
31
|
+
],
|
|
32
|
+
details: [
|
|
33
|
+
"Cooldown quarantines releases younger than your configured window so a freshly-published compromised version can't install before anyone notices. An exemption opts one package out — use it for internal packages you publish and install immediately, or a dependency you've already vetted. Exemptions live in dg.json at the git root; an --expires window makes them self-revoke so the quarantine comes back on its own."
|
|
34
|
+
],
|
|
35
|
+
handler: (context) => runCooldownCommand(context)
|
|
36
|
+
};
|
|
37
|
+
function unreachable() {
|
|
38
|
+
throw new Error("subcommand handled by the cooldown router");
|
|
39
|
+
}
|
|
40
|
+
export function runCooldownCommand(context, cwd = process.cwd(), env = process.env, now = new Date()) {
|
|
41
|
+
const [first, ...rest] = context.args;
|
|
42
|
+
if (first === undefined || first === "list" || first === "--json") {
|
|
43
|
+
const json = first === "--json" || rest.includes("--json");
|
|
44
|
+
const extras = rest.filter((arg) => arg !== "--json");
|
|
45
|
+
if (extras.length > 0) {
|
|
46
|
+
return usage(`unexpected argument '${extras[0]}'`);
|
|
47
|
+
}
|
|
48
|
+
return listExemptions(cwd, env, json, now);
|
|
49
|
+
}
|
|
50
|
+
if (first === "exempt") {
|
|
51
|
+
return addExemption(rest, cwd, env, now);
|
|
52
|
+
}
|
|
53
|
+
if (first === "rm" || first === "remove") {
|
|
54
|
+
return removeExemption(rest, cwd, env);
|
|
55
|
+
}
|
|
56
|
+
if (first === "prune") {
|
|
57
|
+
return pruneExemptions(rest, cwd, env, now);
|
|
58
|
+
}
|
|
59
|
+
if (/^(0|off|[1-9]\d{0,3}[hd])$/i.test(first)) {
|
|
60
|
+
return setWindow(first.toLowerCase(), rest, cwd, env, now);
|
|
61
|
+
}
|
|
62
|
+
return usage(`unknown subcommand '${first}'`);
|
|
63
|
+
}
|
|
64
|
+
function setWindow(window, rest, cwd, env, now) {
|
|
65
|
+
if (rest.length > 0) {
|
|
66
|
+
return usage(`unexpected argument '${rest[0]}'`);
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
updateUserConfig((current) => setConfigValue(current, "cooldown.age", window), env);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error instanceof ConfigError) {
|
|
73
|
+
return usage(error.message);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
return listExemptions(cwd, env, false, now);
|
|
78
|
+
}
|
|
79
|
+
function parseFlags(args) {
|
|
80
|
+
const positionals = [];
|
|
81
|
+
let reason;
|
|
82
|
+
let expires;
|
|
83
|
+
let ecosystem;
|
|
84
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
85
|
+
const arg = args[i];
|
|
86
|
+
if (arg === undefined) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg === "--reason" || arg === "--expires" || arg === "--ecosystem") {
|
|
90
|
+
const value = args[i + 1];
|
|
91
|
+
if (value === undefined || value.startsWith("--")) {
|
|
92
|
+
return { error: `${arg} needs a value` };
|
|
93
|
+
}
|
|
94
|
+
if (arg === "--reason")
|
|
95
|
+
reason = value;
|
|
96
|
+
else if (arg === "--expires")
|
|
97
|
+
expires = value;
|
|
98
|
+
else
|
|
99
|
+
ecosystem = value;
|
|
100
|
+
i += 1;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (arg.startsWith("--reason="))
|
|
104
|
+
reason = arg.slice("--reason=".length);
|
|
105
|
+
else if (arg.startsWith("--expires="))
|
|
106
|
+
expires = arg.slice("--expires=".length);
|
|
107
|
+
else if (arg.startsWith("--ecosystem="))
|
|
108
|
+
ecosystem = arg.slice("--ecosystem=".length);
|
|
109
|
+
else if (arg.startsWith("--"))
|
|
110
|
+
return { error: `unknown flag '${arg}'` };
|
|
111
|
+
else
|
|
112
|
+
positionals.push(arg);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
positionals,
|
|
116
|
+
...(reason !== undefined ? { reason } : {}),
|
|
117
|
+
...(expires !== undefined ? { expires } : {}),
|
|
118
|
+
...(ecosystem !== undefined ? { ecosystem } : {})
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const EXEMPTION_NAME_RE = /^[@A-Za-z0-9][@A-Za-z0-9._/-]*$/u;
|
|
122
|
+
function resolveTarget(token, ecosystemFlag) {
|
|
123
|
+
const colon = token.indexOf(":");
|
|
124
|
+
if (colon > 0) {
|
|
125
|
+
const prefix = token.slice(0, colon);
|
|
126
|
+
if (prefix === "npm" || prefix === "pypi" || prefix === "cargo") {
|
|
127
|
+
return validatedTarget(prefix, token.slice(colon + 1), token);
|
|
128
|
+
}
|
|
129
|
+
return { error: `unknown ecosystem prefix '${prefix}:' (only npm:, pypi:, and cargo: are supported)` };
|
|
130
|
+
}
|
|
131
|
+
const eco = ecosystemFlag ?? "npm";
|
|
132
|
+
if (eco !== "npm" && eco !== "pypi" && eco !== "cargo") {
|
|
133
|
+
return { error: `--ecosystem must be npm, pypi, or cargo, got '${eco}'` };
|
|
134
|
+
}
|
|
135
|
+
return validatedTarget(eco, token, token);
|
|
136
|
+
}
|
|
137
|
+
function validatedTarget(ecosystem, name, token) {
|
|
138
|
+
if (name.length === 0) {
|
|
139
|
+
return { error: `'${token}' has no package name` };
|
|
140
|
+
}
|
|
141
|
+
if (!EXEMPTION_NAME_RE.test(name)) {
|
|
142
|
+
return { error: `'${name}' is not a valid package name (no spaces, control characters, or '*' globs; use config cooldown.exempt for globs)` };
|
|
143
|
+
}
|
|
144
|
+
return { ecosystem, name: canonicalName(ecosystem, name) };
|
|
145
|
+
}
|
|
146
|
+
function canonicalName(ecosystem, name) {
|
|
147
|
+
return canonicalCooldownName(ecosystem, name);
|
|
148
|
+
}
|
|
149
|
+
function expiresAtFrom(expires, now) {
|
|
150
|
+
if (expires === undefined || expires === "off" || expires === "never") {
|
|
151
|
+
return {};
|
|
152
|
+
}
|
|
153
|
+
const days = durationToDays(expires);
|
|
154
|
+
if (days <= 0) {
|
|
155
|
+
if (/^[1-9]\d*(h|d)$/.test(expires)) {
|
|
156
|
+
return { error: `--expires '${expires}' is too large (maximum is 9999d or 9999h)` };
|
|
157
|
+
}
|
|
158
|
+
return { error: `--expires must be a positive duration like 30d or 12h, got '${expires}'` };
|
|
159
|
+
}
|
|
160
|
+
return { value: new Date(now.getTime() + days * 24 * 60 * 60 * 1000).toISOString() };
|
|
161
|
+
}
|
|
162
|
+
function formatExpiryDisplay(expiresAt, acceptedAt) {
|
|
163
|
+
const expiry = Date.parse(expiresAt);
|
|
164
|
+
const accepted = Date.parse(acceptedAt);
|
|
165
|
+
if (Number.isFinite(expiry) && Number.isFinite(accepted) && expiry - accepted < 24 * 60 * 60 * 1000) {
|
|
166
|
+
return `${expiresAt.slice(0, 16).replace("T", " ")}Z`;
|
|
167
|
+
}
|
|
168
|
+
return expiresAt.slice(0, 10);
|
|
169
|
+
}
|
|
170
|
+
function addExemption(args, cwd, env, now) {
|
|
171
|
+
const parsed = parseFlags(args);
|
|
172
|
+
if ("error" in parsed) {
|
|
173
|
+
return usage(parsed.error);
|
|
174
|
+
}
|
|
175
|
+
if (parsed.positionals.length !== 1 || parsed.positionals[0] === undefined) {
|
|
176
|
+
return usage("exempt takes exactly one <name>");
|
|
177
|
+
}
|
|
178
|
+
const target = resolveTarget(parsed.positionals[0], parsed.ecosystem);
|
|
179
|
+
if ("error" in target) {
|
|
180
|
+
return usage(target.error);
|
|
181
|
+
}
|
|
182
|
+
const expiry = expiresAtFrom(parsed.expires, now);
|
|
183
|
+
if ("error" in expiry) {
|
|
184
|
+
return usage(expiry.error);
|
|
185
|
+
}
|
|
186
|
+
const located = locateWritableDgFile(cwd, env);
|
|
187
|
+
if ("error" in located) {
|
|
188
|
+
return located.error;
|
|
189
|
+
}
|
|
190
|
+
const exemption = {
|
|
191
|
+
ecosystem: target.ecosystem,
|
|
192
|
+
name: target.name,
|
|
193
|
+
reason: parsed.reason ?? "",
|
|
194
|
+
acceptedBy: resolveAcceptedBy(located.root, env),
|
|
195
|
+
...(expiry.value ? { expiresAt: expiry.value } : {})
|
|
196
|
+
};
|
|
197
|
+
try {
|
|
198
|
+
mutateDgFile(located.root, env, (file) => appendCooldownExemptions(file, [exemption], now));
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
if (error instanceof CooldownExemptionCapError) {
|
|
202
|
+
return { exitCode: 1, stdout: "", stderr: `dg cooldown: ${error.message}.\n` };
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
const until = expiry.value ? ` until ${formatExpiryDisplay(expiry.value, now.toISOString())}` : "";
|
|
207
|
+
return {
|
|
208
|
+
exitCode: 0,
|
|
209
|
+
stdout: `Exempted ${target.ecosystem}:${target.name} from cooldown${until}.\n`,
|
|
210
|
+
stderr: ""
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function removeExemption(args, cwd, env) {
|
|
214
|
+
const parsed = parseFlags(args);
|
|
215
|
+
if ("error" in parsed) {
|
|
216
|
+
return usage(parsed.error);
|
|
217
|
+
}
|
|
218
|
+
if (parsed.positionals.length !== 1 || parsed.positionals[0] === undefined) {
|
|
219
|
+
return usage("rm takes exactly one <name>");
|
|
220
|
+
}
|
|
221
|
+
const target = resolveTarget(parsed.positionals[0], parsed.ecosystem);
|
|
222
|
+
if ("error" in target) {
|
|
223
|
+
return usage(target.error);
|
|
224
|
+
}
|
|
225
|
+
const located = locateWritableDgFile(cwd, env);
|
|
226
|
+
if ("error" in located) {
|
|
227
|
+
return located.error;
|
|
228
|
+
}
|
|
229
|
+
const matches = (e) => e.ecosystem === target.ecosystem && e.name === target.name;
|
|
230
|
+
let removed = false;
|
|
231
|
+
mutateDgFile(located.root, env, (file) => {
|
|
232
|
+
if (!file.cooldownExemptions.some(matches)) {
|
|
233
|
+
return file;
|
|
234
|
+
}
|
|
235
|
+
removed = true;
|
|
236
|
+
return removeCooldownExemptions(file, matches);
|
|
237
|
+
});
|
|
238
|
+
if (!removed) {
|
|
239
|
+
return { exitCode: 1, stdout: "", stderr: `dg cooldown: ${target.ecosystem}:${target.name} is not exempt in ${located.file.path}.\n` };
|
|
240
|
+
}
|
|
241
|
+
return { exitCode: 0, stdout: `Removed cooldown exemption for ${target.ecosystem}:${target.name} — cooldown applies again.\n`, stderr: "" };
|
|
242
|
+
}
|
|
243
|
+
function pruneExemptions(args, cwd, env, now) {
|
|
244
|
+
if (args.length > 0) {
|
|
245
|
+
return usage(`unexpected argument '${args[0]}'`);
|
|
246
|
+
}
|
|
247
|
+
const located = locateWritableDgFile(cwd, env);
|
|
248
|
+
if ("error" in located) {
|
|
249
|
+
return located.error;
|
|
250
|
+
}
|
|
251
|
+
let pruned = 0;
|
|
252
|
+
mutateDgFile(located.root, env, (file) => {
|
|
253
|
+
const expired = (e) => !cooldownExemptionActive(e, now);
|
|
254
|
+
pruned = file.cooldownExemptions.filter(expired).length;
|
|
255
|
+
return pruned === 0 ? file : removeCooldownExemptions(file, expired);
|
|
256
|
+
});
|
|
257
|
+
return { exitCode: 0, stdout: `Pruned ${pruned} expired cooldown exemption${pruned === 1 ? "" : "s"}.\n`, stderr: "" };
|
|
258
|
+
}
|
|
259
|
+
function windowStatusLines(env) {
|
|
260
|
+
const config = loadUserConfig(env);
|
|
261
|
+
const window = describeCooldownSettings(config, env);
|
|
262
|
+
const windowLine = window === "off"
|
|
263
|
+
? "off — new releases install immediately"
|
|
264
|
+
: `${window} set: dg cooldown 3d`;
|
|
265
|
+
const unknownLine = config.cooldown.onUnknown === "block"
|
|
266
|
+
? "block (no known publish date → held back)"
|
|
267
|
+
: "allow (no known publish date → not held back)";
|
|
268
|
+
return [
|
|
269
|
+
"Cooldown — versions published less than your window ago wait before installing.",
|
|
270
|
+
"",
|
|
271
|
+
` window ${windowLine}`,
|
|
272
|
+
` unknown ${unknownLine}`,
|
|
273
|
+
""
|
|
274
|
+
];
|
|
275
|
+
}
|
|
276
|
+
const EXEMPTION_FOOTER = [
|
|
277
|
+
" add: dg cooldown exempt <name> [--expires 30d] · remove: dg cooldown rm <name>",
|
|
278
|
+
" An exemption waives only the wait — the package is still scanned. No CVE fast-track yet."
|
|
279
|
+
];
|
|
280
|
+
function renderTable(columns, rows) {
|
|
281
|
+
const widths = columns.map((label, column) => Math.max(label.length, ...rows.map((row) => row[column]?.length ?? 0)));
|
|
282
|
+
const renderRow = (row) => ` ${row.map((cell, column) => cell.padEnd(widths[column] ?? 0)).join(" ").trimEnd()}`;
|
|
283
|
+
return [renderRow(columns), ...rows.map(renderRow)];
|
|
284
|
+
}
|
|
285
|
+
function eligibleDisplay(entry, now) {
|
|
286
|
+
if (!entry.eligibleAt) {
|
|
287
|
+
return "unknown";
|
|
288
|
+
}
|
|
289
|
+
const eligible = Date.parse(entry.eligibleAt);
|
|
290
|
+
if (!Number.isFinite(eligible)) {
|
|
291
|
+
return "unknown";
|
|
292
|
+
}
|
|
293
|
+
const relative = formatCooldownDuration(Math.max(0, (eligible - now.getTime()) / 86_400_000));
|
|
294
|
+
return `${entry.eligibleAt.slice(0, 10)} (in ${relative})`;
|
|
295
|
+
}
|
|
296
|
+
function heldSectionLines(env, now) {
|
|
297
|
+
const held = readHeldPackages(env, now);
|
|
298
|
+
if (held.length === 0) {
|
|
299
|
+
return ["Currently held: none.", ""];
|
|
300
|
+
}
|
|
301
|
+
const rows = held.map((entry) => [
|
|
302
|
+
`${entry.ecosystem}:${entry.name}@${entry.version}`,
|
|
303
|
+
entry.publishedAt ? entry.publishedAt.slice(0, 10) : "-",
|
|
304
|
+
eligibleDisplay(entry, now)
|
|
305
|
+
]);
|
|
306
|
+
return [
|
|
307
|
+
"Currently held:",
|
|
308
|
+
"",
|
|
309
|
+
...renderTable(["PACKAGE", "PUBLISHED", "ELIGIBLE"], rows),
|
|
310
|
+
"",
|
|
311
|
+
" release now: dg cooldown exempt <name>",
|
|
312
|
+
""
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
function listExemptions(cwd, env, json, now) {
|
|
316
|
+
const root = findProjectRoot(cwd, env);
|
|
317
|
+
if (!root) {
|
|
318
|
+
return { exitCode: EXIT_USAGE, stdout: "", stderr: "dg cooldown: not inside a git repository.\n" };
|
|
319
|
+
}
|
|
320
|
+
const file = loadDgFile(root);
|
|
321
|
+
if (file.exists && !file.readable) {
|
|
322
|
+
return { exitCode: 1, stdout: "", stderr: `dg cooldown: cannot use ${file.path} — ${file.failure ?? "unreadable"}.\n` };
|
|
323
|
+
}
|
|
324
|
+
const config = loadUserConfig(env);
|
|
325
|
+
if (json) {
|
|
326
|
+
return {
|
|
327
|
+
exitCode: 0,
|
|
328
|
+
stdout: `${JSON.stringify({
|
|
329
|
+
schemaVersion: 1,
|
|
330
|
+
path: file.path,
|
|
331
|
+
window: { effective: describeCooldownSettings(config, env), onUnknown: config.cooldown.onUnknown },
|
|
332
|
+
held: readHeldPackages(env, now),
|
|
333
|
+
cooldownExemptions: file.cooldownExemptions.map((e) => exemptionJson(e, now))
|
|
334
|
+
}, null, 2)}\n`,
|
|
335
|
+
stderr: ""
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const header = [...windowStatusLines(env), ...heldSectionLines(env, now)];
|
|
339
|
+
if (file.cooldownExemptions.length === 0) {
|
|
340
|
+
const note = file.exists ? `No exemptions in ${file.path}.` : "No exemptions yet — add one with the command below.";
|
|
341
|
+
return { exitCode: 0, stdout: `${[...header, note, ...EXEMPTION_FOOTER].join("\n")}\n`, stderr: "" };
|
|
342
|
+
}
|
|
343
|
+
const rows = file.cooldownExemptions.map((e) => [
|
|
344
|
+
`${e.ecosystem}:${e.name}`,
|
|
345
|
+
e.reason || "-",
|
|
346
|
+
e.acceptedBy || "-",
|
|
347
|
+
e.acceptedAt.slice(0, 10) || "-",
|
|
348
|
+
e.expiresAt ? formatExpiryDisplay(e.expiresAt, e.acceptedAt) : "-",
|
|
349
|
+
cooldownExemptionActive(e, now) ? "active" : "expired"
|
|
350
|
+
]);
|
|
351
|
+
const lines = [
|
|
352
|
+
...header,
|
|
353
|
+
`Exemptions in ${file.path}:`,
|
|
354
|
+
"",
|
|
355
|
+
...renderTable(["PACKAGE", "REASON", "BY", "WHEN", "EXPIRES", "STATUS"], rows),
|
|
356
|
+
"",
|
|
357
|
+
...EXEMPTION_FOOTER
|
|
358
|
+
];
|
|
359
|
+
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
360
|
+
}
|
|
361
|
+
function locateWritableDgFile(cwd, env) {
|
|
362
|
+
const root = findProjectRoot(cwd, env);
|
|
363
|
+
if (!root) {
|
|
364
|
+
return { error: { exitCode: EXIT_USAGE, stdout: "", stderr: "dg cooldown: not inside a git repository.\n" } };
|
|
365
|
+
}
|
|
366
|
+
const file = loadDgFile(root);
|
|
367
|
+
if (!file.readable) {
|
|
368
|
+
return { error: { exitCode: 1, stdout: "", stderr: `dg cooldown: cannot write ${file.path} — ${file.failure ?? "unreadable"}.\n` } };
|
|
369
|
+
}
|
|
370
|
+
return { root, file };
|
|
371
|
+
}
|
|
372
|
+
function exemptionJson(e, now) {
|
|
373
|
+
return {
|
|
374
|
+
ecosystem: e.ecosystem,
|
|
375
|
+
name: e.name,
|
|
376
|
+
reason: e.reason,
|
|
377
|
+
acceptedBy: e.acceptedBy,
|
|
378
|
+
acceptedAt: e.acceptedAt,
|
|
379
|
+
...(e.expiresAt ? { expiresAt: e.expiresAt } : {}),
|
|
380
|
+
status: cooldownExemptionActive(e, now) ? "active" : "expired"
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
function usage(message) {
|
|
384
|
+
return {
|
|
385
|
+
exitCode: EXIT_USAGE,
|
|
386
|
+
stdout: "",
|
|
387
|
+
stderr: `dg cooldown: ${message}. Usage: ${cooldownCommand.usage}\n`
|
|
388
|
+
};
|
|
389
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { recordDecisionEvents } from "../decisions/remember-prompt.js";
|
|
2
2
|
import { packageKey } from "../decisions/apply.js";
|
|
3
|
-
import { findProjectRoot, loadDgFile,
|
|
3
|
+
import { findProjectRoot, loadDgFile, mutateDgFile, removeDecisions } from "../project/dgfile.js";
|
|
4
4
|
import { EXIT_USAGE } from "./types.js";
|
|
5
5
|
export const decisionsCommand = {
|
|
6
6
|
name: "decisions",
|
|
@@ -24,7 +24,7 @@ export function runDecisionsCommand(context, cwd = process.cwd(), env = process.
|
|
|
24
24
|
const [first, ...rest] = context.args;
|
|
25
25
|
if (first === undefined || first === "list" || first === "--json") {
|
|
26
26
|
const json = first === "--json" || rest.includes("--json");
|
|
27
|
-
const extras =
|
|
27
|
+
const extras = rest.filter((arg) => arg !== "--json");
|
|
28
28
|
if (extras.length > 0) {
|
|
29
29
|
return usage(`unexpected argument '${extras[0]}'`);
|
|
30
30
|
}
|
|
@@ -44,11 +44,17 @@ function listDecisions(cwd, env, json) {
|
|
|
44
44
|
if ("error" in located) {
|
|
45
45
|
return located.error;
|
|
46
46
|
}
|
|
47
|
+
if ("missing" in located) {
|
|
48
|
+
if (json) {
|
|
49
|
+
return { exitCode: 0, stdout: `${JSON.stringify({ schemaVersion: 1, path: null, decisions: [] }, null, 2)}\n`, stderr: "" };
|
|
50
|
+
}
|
|
51
|
+
return { exitCode: 0, stdout: `No dg.json at ${located.missing} — nothing remembered yet.\n`, stderr: "" };
|
|
52
|
+
}
|
|
47
53
|
const file = located.file;
|
|
48
54
|
if (json) {
|
|
49
55
|
return {
|
|
50
56
|
exitCode: 0,
|
|
51
|
-
stdout: `${JSON.stringify({ path: file.path, decisions: file.decisions.map((entry) => entryJson(entry)) }, null, 2)}\n`,
|
|
57
|
+
stdout: `${JSON.stringify({ schemaVersion: 1, path: file.path, decisions: file.decisions.map((entry) => entryJson(entry)) }, null, 2)}\n`,
|
|
52
58
|
stderr: ""
|
|
53
59
|
};
|
|
54
60
|
}
|
|
@@ -82,12 +88,20 @@ function revokeDecisions(selector, cwd, env) {
|
|
|
82
88
|
if ("error" in located) {
|
|
83
89
|
return located.error;
|
|
84
90
|
}
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
if ("missing" in located) {
|
|
92
|
+
return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing to revoke — no dg.json at ${located.missing} yet.\n` };
|
|
93
|
+
}
|
|
94
|
+
let matched = [];
|
|
95
|
+
mutateDgFile(located.root, env, (file) => {
|
|
96
|
+
matched = file.decisions.filter((entry) => matchesSelector(entry, selector));
|
|
97
|
+
if (matched.length === 0) {
|
|
98
|
+
return file;
|
|
99
|
+
}
|
|
100
|
+
return removeDecisions(file, new Set(matched.map((entry) => entry.id)));
|
|
101
|
+
});
|
|
87
102
|
if (matched.length === 0) {
|
|
88
|
-
return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing matches '${selector}' in ${file.path}.\n` };
|
|
103
|
+
return { exitCode: 1, stdout: "", stderr: `dg decisions: nothing matches '${selector}' in ${located.file.path}.\n` };
|
|
89
104
|
}
|
|
90
|
-
saveDgFile(removeDecisions(file, new Set(matched.map((entry) => entry.id))));
|
|
91
105
|
recordDecisionEvents("decision.revoked", matched.map((entry) => `${entry.ecosystem}:${packageKey(entry.name, scopeLabel(entry))}`), `revoked via dg decisions (${selector})`, env);
|
|
92
106
|
const lines = matched.map((entry) => `Revoked ${entry.ecosystem}:${entry.name}@${scopeLabel(entry)} (${entry.id.slice(0, 8)}) — the warn will surface again.`);
|
|
93
107
|
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
@@ -99,12 +113,12 @@ function locateDgFile(cwd, env) {
|
|
|
99
113
|
}
|
|
100
114
|
const file = loadDgFile(root);
|
|
101
115
|
if (!file.exists) {
|
|
102
|
-
return {
|
|
116
|
+
return { missing: root };
|
|
103
117
|
}
|
|
104
118
|
if (!file.readable) {
|
|
105
119
|
return { error: { exitCode: 1, stdout: "", stderr: `dg decisions: cannot use ${file.path} — ${file.failure ?? "unreadable"}.\n` } };
|
|
106
120
|
}
|
|
107
|
-
return { file };
|
|
121
|
+
return { root, file };
|
|
108
122
|
}
|
|
109
123
|
function matchesSelector(entry, selector) {
|
|
110
124
|
if (selector.length >= 4 && entry.id.startsWith(selector)) {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -33,7 +33,7 @@ async function doctorHandler(args) {
|
|
|
33
33
|
const theme = createTheme(resolvePresentation().color);
|
|
34
34
|
return {
|
|
35
35
|
exitCode: hasFailure ? 1 : 0,
|
|
36
|
-
stdout: json ? `${JSON.stringify(report, null, 2)}\n` : renderDoctorReport(report, theme, verbose),
|
|
36
|
+
stdout: json ? `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n` : renderDoctorReport(report, theme, verbose),
|
|
37
37
|
stderr: ""
|
|
38
38
|
};
|
|
39
39
|
}
|
package/dist/commands/help.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const PRODUCT_DESCRIPTION = "Dependency Guardian
|
|
1
|
+
const PRODUCT_DESCRIPTION = "Dependency Guardian";
|
|
2
2
|
const COMMON_COMMANDS = ["scan", "verify", "audit", "licenses", "setup", "guard-commit", "doctor", "login"];
|
|
3
3
|
const GLOBAL_FLAGS_FOOTER = "Global: --help/-h · --version/-v · --json (where supported) · --no-color / --force-color (also NO_COLOR, FORCE_COLOR).";
|
|
4
4
|
function isWrapper(command) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { launchScanTui, shouldLaunchScanTui } from "../scan-ui/launch.js";
|
|
3
|
+
import { writeReportAtomic } from "../util/report-writer.js";
|
|
3
4
|
import { basename, dirname, relative, resolve, sep } from "node:path";
|
|
4
5
|
import { scanProject } from "../scan/discovery.js";
|
|
5
6
|
import { isSupportedLockfilePath, verifyLockfile } from "../verify/preflight.js";
|
|
6
|
-
import { EXIT_USAGE_VERDICT } from "./types.js";
|
|
7
|
+
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_USAGE_VERDICT } from "./types.js";
|
|
7
8
|
const LICENSE_RISKS = [
|
|
8
9
|
"network-copyleft",
|
|
9
10
|
"no-license",
|
|
@@ -39,7 +40,7 @@ export const licensesCommand = {
|
|
|
39
40
|
examples: ["dg licenses", "dg licenses --markdown -o licenses.md", "dg licenses --fail-on strong-copyleft,network-copyleft"],
|
|
40
41
|
details: [
|
|
41
42
|
"Reports project and lockfile license metadata without running package managers or package code.",
|
|
42
|
-
"License exports support text, JSON, CSV, and Markdown
|
|
43
|
+
"License exports support text, JSON, CSV, and Markdown. Exit codes: 0 pass, 2 policy block, 4 report error, 64 usage error."
|
|
43
44
|
],
|
|
44
45
|
handler: (context) => runLicensesCommand(context.args)
|
|
45
46
|
};
|
|
@@ -60,13 +61,13 @@ async function runLicensesCommand(args) {
|
|
|
60
61
|
}
|
|
61
62
|
catch (error) {
|
|
62
63
|
return {
|
|
63
|
-
exitCode:
|
|
64
|
+
exitCode: EXIT_ANALYSIS_INCOMPLETE,
|
|
64
65
|
stdout: "",
|
|
65
66
|
stderr: `dg licenses TUI failed: ${error instanceof Error ? error.message : "unknown error"}\n`
|
|
66
67
|
};
|
|
67
68
|
}
|
|
68
69
|
return {
|
|
69
|
-
exitCode: 0,
|
|
70
|
+
exitCode: typeof process.exitCode === "number" ? process.exitCode : 0,
|
|
70
71
|
stdout: "",
|
|
71
72
|
stderr: ""
|
|
72
73
|
};
|
|
@@ -78,7 +79,7 @@ async function runLicensesCommand(args) {
|
|
|
78
79
|
}
|
|
79
80
|
catch (error) {
|
|
80
81
|
return {
|
|
81
|
-
exitCode:
|
|
82
|
+
exitCode: EXIT_ANALYSIS_INCOMPLETE,
|
|
82
83
|
stdout: "",
|
|
83
84
|
stderr: `dg licenses failed: ${error instanceof Error ? error.message : "unknown license error"}\n`
|
|
84
85
|
};
|
|
@@ -91,7 +92,7 @@ async function runLicensesCommand(args) {
|
|
|
91
92
|
}
|
|
92
93
|
catch (error) {
|
|
93
94
|
return {
|
|
94
|
-
exitCode:
|
|
95
|
+
exitCode: EXIT_ANALYSIS_INCOMPLETE,
|
|
95
96
|
stdout: "",
|
|
96
97
|
stderr: `dg licenses could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
97
98
|
};
|
|
@@ -329,7 +330,7 @@ function classifyLicense(license) {
|
|
|
329
330
|
}
|
|
330
331
|
function renderLicenseReport(report, format) {
|
|
331
332
|
if (format === "json") {
|
|
332
|
-
return `${JSON.stringify(report, null, 2)}\n`;
|
|
333
|
+
return `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n`;
|
|
333
334
|
}
|
|
334
335
|
if (format === "csv") {
|
|
335
336
|
return renderCsv(report);
|
|
@@ -392,7 +393,7 @@ function renderMarkdown(report) {
|
|
|
392
393
|
return `${lines.join("\n")}\n`;
|
|
393
394
|
}
|
|
394
395
|
function exitCodeForReport(report) {
|
|
395
|
-
return report.status === "block" ?
|
|
396
|
+
return report.status === "block" ? 2 : 0;
|
|
396
397
|
}
|
|
397
398
|
function usageError(message) {
|
|
398
399
|
return {
|
|
@@ -401,19 +402,6 @@ function usageError(message) {
|
|
|
401
402
|
stderr: `dg licenses: ${message}. Usage: dg licenses [path] [--json|--csv|--markdown] [--output <path>] [--fail-on <risk[,risk...]>] [--deny-license <id>]\n`
|
|
402
403
|
};
|
|
403
404
|
}
|
|
404
|
-
function writeReportAtomic(outputPath, contents) {
|
|
405
|
-
const directory = dirname(outputPath);
|
|
406
|
-
mkdirSync(directory, { recursive: true });
|
|
407
|
-
const temporaryPath = `${outputPath}.${process.pid}.${Date.now()}.tmp`;
|
|
408
|
-
try {
|
|
409
|
-
writeFileSync(temporaryPath, contents, "utf8");
|
|
410
|
-
renameSync(temporaryPath, outputPath);
|
|
411
|
-
}
|
|
412
|
-
catch (error) {
|
|
413
|
-
rmSync(temporaryPath, { force: true });
|
|
414
|
-
throw error;
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
405
|
function dedupeEntries(entries) {
|
|
418
406
|
const seen = new Set();
|
|
419
407
|
const deduped = [];
|
package/dist/commands/logout.js
CHANGED
|
@@ -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
|
|
7
|
-
flags: [{ flag: "--yes", summary: "
|
|
8
|
-
details: ["Removes dg-owned auth
|
|
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 ? "
|
|
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).`);
|
package/dist/commands/router.js
CHANGED
|
@@ -1,12 +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
4
|
import { decisionsCommand } from "./decisions.js";
|
|
5
5
|
import { doctorCommand } from "./doctor.js";
|
|
6
|
-
import { explainCommand } from "./explain.js";
|
|
7
6
|
import { guardCommitCommand } from "./guard-commit.js";
|
|
7
|
+
import { agentsCommand } from "./agents.js";
|
|
8
8
|
import { renderCommandHelp, renderRootHelp } from "./help.js";
|
|
9
9
|
import { licensesCommand } from "./licenses.js";
|
|
10
|
+
import { sbomCommand } from "./sbom.js";
|
|
10
11
|
import { loginCommand } from "./login.js";
|
|
11
12
|
import { logoutCommand } from "./logout.js";
|
|
12
13
|
import { packageManagerCommands } from "./wrap.js";
|
|
@@ -22,19 +23,20 @@ import { verifyCommand } from "./verify.js";
|
|
|
22
23
|
import { dgVersion, versionResult } from "./version.js";
|
|
23
24
|
export const commandCatalog = [
|
|
24
25
|
scanCommand,
|
|
26
|
+
sbomCommand,
|
|
25
27
|
verifyCommand,
|
|
26
28
|
setupCommand,
|
|
27
29
|
guardCommitCommand,
|
|
30
|
+
agentsCommand,
|
|
28
31
|
decisionsCommand,
|
|
32
|
+
cooldownCommand,
|
|
29
33
|
uninstallCommand,
|
|
30
34
|
doctorCommand,
|
|
31
35
|
statusCommand,
|
|
32
36
|
...packageManagerCommands(),
|
|
33
37
|
loginCommand,
|
|
34
38
|
logoutCommand,
|
|
35
|
-
explainCommand,
|
|
36
39
|
configCommand,
|
|
37
|
-
completionCommand,
|
|
38
40
|
licensesCommand,
|
|
39
41
|
auditCommand,
|
|
40
42
|
updateCommand,
|