@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
package/dist/commands/update.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { toolInvocation } from "../util/external-tool.js";
|
|
3
3
|
import { EXIT_USAGE } from "./types.js";
|
|
4
4
|
import { dgVersion } from "./version.js";
|
|
5
5
|
const PACKAGE_NAME = "@westbayberry/dg";
|
|
6
|
+
const VERSION_PATTERN = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/u;
|
|
6
7
|
export const updateCommand = {
|
|
7
8
|
name: "update",
|
|
8
9
|
summary: "Check for dg CLI updates.",
|
|
@@ -26,7 +27,7 @@ function runUpdateCommand(args, commandName) {
|
|
|
26
27
|
if (parsed.format === "json") {
|
|
27
28
|
return {
|
|
28
29
|
exitCode: report.status === "unknown" ? 1 : 0,
|
|
29
|
-
stdout: `${JSON.stringify(report, null, 2)}\n`,
|
|
30
|
+
stdout: `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n`,
|
|
30
31
|
stderr: ""
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -62,17 +63,22 @@ function parseUpdateArgs(args) {
|
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
export function readLatestVersion(timeoutMs = 5000) {
|
|
65
|
-
const injected = process.env.DG_UPDATE_LATEST_VERSION;
|
|
66
|
+
const injected = process.env.NODE_ENV === "test" ? process.env.DG_UPDATE_LATEST_VERSION : undefined;
|
|
66
67
|
if (injected) {
|
|
67
|
-
return injected;
|
|
68
|
+
return validVersion(injected);
|
|
68
69
|
}
|
|
69
|
-
const
|
|
70
|
+
const invocation = toolInvocation("npm", ["view", PACKAGE_NAME, "version", "--json"]);
|
|
71
|
+
if (!invocation) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const result = spawnSync(invocation.command, [...invocation.args], {
|
|
70
75
|
encoding: "utf8",
|
|
71
76
|
env: {
|
|
72
77
|
...process.env,
|
|
73
78
|
npm_config_ignore_scripts: "true"
|
|
74
79
|
},
|
|
75
|
-
timeout: timeoutMs
|
|
80
|
+
timeout: timeoutMs,
|
|
81
|
+
windowsVerbatimArguments: invocation.windowsVerbatimArguments
|
|
76
82
|
});
|
|
77
83
|
if (result.status !== 0) {
|
|
78
84
|
return null;
|
|
@@ -83,12 +89,15 @@ export function readLatestVersion(timeoutMs = 5000) {
|
|
|
83
89
|
}
|
|
84
90
|
try {
|
|
85
91
|
const parsed = JSON.parse(raw);
|
|
86
|
-
return typeof parsed === "string" ? parsed : null;
|
|
92
|
+
return typeof parsed === "string" ? validVersion(parsed) : null;
|
|
87
93
|
}
|
|
88
94
|
catch {
|
|
89
|
-
return raw.replace(/^"|"$/gu, "");
|
|
95
|
+
return validVersion(raw.replace(/^"|"$/gu, ""));
|
|
90
96
|
}
|
|
91
97
|
}
|
|
98
|
+
function validVersion(value) {
|
|
99
|
+
return VERSION_PATTERN.test(value) ? value : null;
|
|
100
|
+
}
|
|
92
101
|
function buildUpdateReport(latestVersion) {
|
|
93
102
|
if (!latestVersion) {
|
|
94
103
|
return {
|
package/dist/commands/verify.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EXIT_UNAVAILABLE,
|
|
1
|
+
import { EXIT_ANALYSIS_INCOMPLETE, EXIT_UNAVAILABLE, EXIT_USAGE_VERDICT } from "./types.js";
|
|
2
2
|
import { writeFileSync } from "node:fs";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import { resolve } from "node:path";
|
|
@@ -22,14 +22,14 @@ export const verifyCommand = {
|
|
|
22
22
|
],
|
|
23
23
|
examples: ["dg verify npm:react", "dg verify pypi:requests@2.31.0", "dg verify ./pkg.tgz --verbose", "dg verify package-lock.json --json"],
|
|
24
24
|
details: [
|
|
25
|
-
"dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it.
|
|
25
|
+
"dg verify npm:react (or pypi:requests, with an optional @version — defaults to latest) runs a real scanner check on a published package before you install it. Signed-out runs show the verdict and top reasons; dg login unlocks full findings, license info, --json, and --output.",
|
|
26
26
|
"Local paths, workspaces, tgz/zip/wheel artifacts, and lockfiles are verified offline as advisory preflight (free); proxy enforcement remains authoritative for network artifact fetches."
|
|
27
27
|
],
|
|
28
28
|
handler: (context) => {
|
|
29
29
|
const parsed = parseVerifyArgs(context.args);
|
|
30
30
|
if ("error" in parsed) {
|
|
31
31
|
return {
|
|
32
|
-
exitCode:
|
|
32
|
+
exitCode: EXIT_USAGE_VERDICT,
|
|
33
33
|
stdout: "",
|
|
34
34
|
stderr: `dg verify: ${parsed.error}. Usage: dg verify <spec|path|lockfile> [--verbose] [--json|--sarif] [--output <path>]\n`
|
|
35
35
|
};
|
|
@@ -52,7 +52,7 @@ export const verifyCommand = {
|
|
|
52
52
|
}
|
|
53
53
|
catch (error) {
|
|
54
54
|
return {
|
|
55
|
-
exitCode:
|
|
55
|
+
exitCode: EXIT_ANALYSIS_INCOMPLETE,
|
|
56
56
|
stdout: "",
|
|
57
57
|
stderr: `dg verify could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "unknown write error"}\n`
|
|
58
58
|
};
|
|
@@ -147,5 +147,8 @@ function exitCodeForReport(report) {
|
|
|
147
147
|
if (report.status === "block") {
|
|
148
148
|
return 2;
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
if (report.status === "error") {
|
|
151
|
+
return EXIT_ANALYSIS_INCOMPLETE;
|
|
152
|
+
}
|
|
153
|
+
return report.status === "warn" ? 1 : 0;
|
|
151
154
|
}
|
package/dist/config/settings.js
CHANGED
|
@@ -9,6 +9,8 @@ export const CONFIG_KEYS = Object.freeze([
|
|
|
9
9
|
"policy.trustProjectAllowlists",
|
|
10
10
|
"policy.allowForceOverride",
|
|
11
11
|
"policy.scriptHardening",
|
|
12
|
+
"policy.shimFailClosed",
|
|
13
|
+
"policy.strictEgress",
|
|
12
14
|
"scriptGate.mode",
|
|
13
15
|
"scriptGate.observe",
|
|
14
16
|
"gitHook.onWarn",
|
|
@@ -19,9 +21,7 @@ export const CONFIG_KEYS = Object.freeze([
|
|
|
19
21
|
"cooldown.cargo.age",
|
|
20
22
|
"cooldown.onUnknown",
|
|
21
23
|
"cooldown.exempt",
|
|
22
|
-
"audit.upload"
|
|
23
|
-
"telemetry.enabled",
|
|
24
|
-
"webhooks.enabled"
|
|
24
|
+
"audit.upload"
|
|
25
25
|
]);
|
|
26
26
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
27
27
|
version: 1,
|
|
@@ -35,7 +35,9 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
35
35
|
mode: "block",
|
|
36
36
|
trustProjectAllowlists: false,
|
|
37
37
|
allowForceOverride: true,
|
|
38
|
-
scriptHardening: false
|
|
38
|
+
scriptHardening: false,
|
|
39
|
+
shimFailClosed: false,
|
|
40
|
+
strictEgress: false
|
|
39
41
|
},
|
|
40
42
|
scriptGate: {
|
|
41
43
|
mode: "observe",
|
|
@@ -50,17 +52,11 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
50
52
|
npmAge: "",
|
|
51
53
|
pypiAge: "",
|
|
52
54
|
cargoAge: "",
|
|
53
|
-
onUnknown: "
|
|
55
|
+
onUnknown: "block",
|
|
54
56
|
exempt: ""
|
|
55
57
|
},
|
|
56
58
|
audit: {
|
|
57
59
|
upload: false
|
|
58
|
-
},
|
|
59
|
-
telemetry: {
|
|
60
|
-
enabled: false
|
|
61
|
-
},
|
|
62
|
-
webhooks: {
|
|
63
|
-
enabled: true
|
|
64
60
|
}
|
|
65
61
|
});
|
|
66
62
|
export class ConfigError extends Error {
|
|
@@ -114,6 +110,17 @@ export function updateUserConfig(apply, env = process.env) {
|
|
|
114
110
|
return next;
|
|
115
111
|
});
|
|
116
112
|
}
|
|
113
|
+
// Whether repo-supplied project overrides (allowlists, cooldown exemptions,
|
|
114
|
+
// warn-decisions) are trusted wholesale. A corrupt config fails closed to
|
|
115
|
+
// untrusted so a hostile repo can never relax screening by breaking the config.
|
|
116
|
+
export function trustsProjectOverrides(env = process.env) {
|
|
117
|
+
try {
|
|
118
|
+
return loadUserConfig(env).policy.trustProjectAllowlists;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
117
124
|
export function getConfigValue(config, key) {
|
|
118
125
|
if (key === "api.baseUrl") {
|
|
119
126
|
return config.api.baseUrl;
|
|
@@ -133,6 +140,12 @@ export function getConfigValue(config, key) {
|
|
|
133
140
|
if (key === "policy.scriptHardening") {
|
|
134
141
|
return String(config.policy.scriptHardening);
|
|
135
142
|
}
|
|
143
|
+
if (key === "policy.shimFailClosed") {
|
|
144
|
+
return String(config.policy.shimFailClosed);
|
|
145
|
+
}
|
|
146
|
+
if (key === "policy.strictEgress") {
|
|
147
|
+
return String(config.policy.strictEgress);
|
|
148
|
+
}
|
|
136
149
|
if (key === "scriptGate.mode") {
|
|
137
150
|
return config.scriptGate.mode;
|
|
138
151
|
}
|
|
@@ -163,16 +176,20 @@ export function getConfigValue(config, key) {
|
|
|
163
176
|
if (key === "cooldown.exempt") {
|
|
164
177
|
return config.cooldown.exempt;
|
|
165
178
|
}
|
|
166
|
-
|
|
167
|
-
return String(config.audit.upload);
|
|
168
|
-
}
|
|
169
|
-
if (key === "telemetry.enabled") {
|
|
170
|
-
return String(config.telemetry.enabled);
|
|
171
|
-
}
|
|
172
|
-
return String(config.webhooks.enabled);
|
|
179
|
+
return String(config.audit.upload);
|
|
173
180
|
}
|
|
174
|
-
export
|
|
175
|
-
|
|
181
|
+
export const ADVANCED_CONFIG_KEYS = new Set([
|
|
182
|
+
"org.id",
|
|
183
|
+
"policy.scriptHardening",
|
|
184
|
+
"policy.shimFailClosed",
|
|
185
|
+
"policy.strictEgress",
|
|
186
|
+
"scriptGate.observe",
|
|
187
|
+
"cooldown.npm.age",
|
|
188
|
+
"cooldown.pypi.age",
|
|
189
|
+
"cooldown.cargo.age"
|
|
190
|
+
]);
|
|
191
|
+
export function listConfig(config, includeAdvanced = false) {
|
|
192
|
+
return CONFIG_KEYS.filter((key) => includeAdvanced || !ADVANCED_CONFIG_KEYS.has(key)).map((key) => ({
|
|
176
193
|
key,
|
|
177
194
|
value: getConfigValue(config, key)
|
|
178
195
|
}));
|
|
@@ -212,6 +229,12 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
212
229
|
if (key === "policy.scriptHardening") {
|
|
213
230
|
return withPolicyBoolean(config, "scriptHardening", rawValue);
|
|
214
231
|
}
|
|
232
|
+
if (key === "policy.shimFailClosed") {
|
|
233
|
+
return withPolicyBoolean(config, "shimFailClosed", rawValue);
|
|
234
|
+
}
|
|
235
|
+
if (key === "policy.strictEgress") {
|
|
236
|
+
return withPolicyBoolean(config, "strictEgress", rawValue);
|
|
237
|
+
}
|
|
215
238
|
if (key === "scriptGate.mode") {
|
|
216
239
|
return {
|
|
217
240
|
...config,
|
|
@@ -266,26 +289,10 @@ export function setConfigValue(config, key, rawValue) {
|
|
|
266
289
|
if (key === "cooldown.exempt") {
|
|
267
290
|
return withCooldown(config, { exempt: parseCooldownExempt(rawValue) });
|
|
268
291
|
}
|
|
269
|
-
if (key === "audit.upload") {
|
|
270
|
-
return {
|
|
271
|
-
...config,
|
|
272
|
-
audit: {
|
|
273
|
-
upload: parseBoolean(rawValue, key)
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
if (key === "telemetry.enabled") {
|
|
278
|
-
return {
|
|
279
|
-
...config,
|
|
280
|
-
telemetry: {
|
|
281
|
-
enabled: parseBoolean(rawValue, key)
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
292
|
return {
|
|
286
293
|
...config,
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
audit: {
|
|
295
|
+
upload: parseBoolean(rawValue, key)
|
|
289
296
|
}
|
|
290
297
|
};
|
|
291
298
|
}
|
|
@@ -309,8 +316,6 @@ function normalizeConfig(raw) {
|
|
|
309
316
|
const gitHook = fieldObject(raw, "gitHook");
|
|
310
317
|
const cooldown = fieldObject(raw, "cooldown");
|
|
311
318
|
const audit = fieldObject(raw, "audit");
|
|
312
|
-
const telemetry = fieldObject(raw, "telemetry");
|
|
313
|
-
const webhooks = fieldObject(raw, "webhooks");
|
|
314
319
|
const scriptHardening = fieldBoolean(policy, "policy.scriptHardening", "scriptHardening") ?? DEFAULT_CONFIG.policy.scriptHardening;
|
|
315
320
|
return {
|
|
316
321
|
version: 1,
|
|
@@ -324,7 +329,9 @@ function normalizeConfig(raw) {
|
|
|
324
329
|
mode: parsePolicyMode(fieldString(policy, "policy.mode", "mode") ?? DEFAULT_CONFIG.policy.mode),
|
|
325
330
|
trustProjectAllowlists: fieldBoolean(policy, "policy.trustProjectAllowlists", "trustProjectAllowlists") ?? DEFAULT_CONFIG.policy.trustProjectAllowlists,
|
|
326
331
|
allowForceOverride: fieldBoolean(policy, "policy.allowForceOverride", "allowForceOverride") ?? DEFAULT_CONFIG.policy.allowForceOverride,
|
|
327
|
-
scriptHardening
|
|
332
|
+
scriptHardening,
|
|
333
|
+
shimFailClosed: fieldBoolean(policy, "policy.shimFailClosed", "shimFailClosed") ?? DEFAULT_CONFIG.policy.shimFailClosed,
|
|
334
|
+
strictEgress: fieldBoolean(policy, "policy.strictEgress", "strictEgress") ?? DEFAULT_CONFIG.policy.strictEgress
|
|
328
335
|
},
|
|
329
336
|
scriptGate: {
|
|
330
337
|
mode: parseScriptGateMode(fieldString(scriptGate, "scriptGate.mode", "mode") ?? (scriptHardening ? "enforce" : DEFAULT_CONFIG.scriptGate.mode)),
|
|
@@ -344,12 +351,6 @@ function normalizeConfig(raw) {
|
|
|
344
351
|
},
|
|
345
352
|
audit: {
|
|
346
353
|
upload: fieldBoolean(audit, "audit.upload", "upload") ?? DEFAULT_CONFIG.audit.upload
|
|
347
|
-
},
|
|
348
|
-
telemetry: {
|
|
349
|
-
enabled: fieldBoolean(telemetry, "telemetry.enabled", "enabled") ?? DEFAULT_CONFIG.telemetry.enabled
|
|
350
|
-
},
|
|
351
|
-
webhooks: {
|
|
352
|
-
enabled: fieldBoolean(webhooks, "webhooks.enabled", "enabled") ?? DEFAULT_CONFIG.webhooks.enabled
|
|
353
354
|
}
|
|
354
355
|
};
|
|
355
356
|
}
|
|
@@ -482,7 +483,7 @@ function parseBoolean(value, field) {
|
|
|
482
483
|
}
|
|
483
484
|
throw new ConfigError(`${field} must be true or false`);
|
|
484
485
|
}
|
|
485
|
-
function parseUrl(value) {
|
|
486
|
+
export function parseUrl(value) {
|
|
486
487
|
const trimmed = value.trim();
|
|
487
488
|
try {
|
|
488
489
|
const url = new URL(trimmed);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { recordAuditEvent } from "../audit/events.js";
|
|
2
2
|
import { loadUserConfig } from "../config/settings.js";
|
|
3
3
|
import { promptText, promptYesNo } from "../install-ui/prompt.js";
|
|
4
|
-
import {
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { appendDecisions, mutateDgFile } from "../project/dgfile.js";
|
|
5
6
|
import { promptLine as ttyPromptLine, promptYesNo as ttyPromptYesNo } from "../util/tty-prompt.js";
|
|
6
7
|
import { findingFingerprint, packageKey } from "./apply.js";
|
|
7
8
|
const defaultSyncPrompts = {
|
|
@@ -67,7 +68,7 @@ export function persistRemembered(file, packages, options) {
|
|
|
67
68
|
reason: options.reason,
|
|
68
69
|
acceptedBy: options.acceptedBy
|
|
69
70
|
}));
|
|
70
|
-
|
|
71
|
+
mutateDgFile(dirname(file.path), options.env, (current) => appendDecisions(current, additions));
|
|
71
72
|
recordDecisionEvents("decision.accepted", packages.map((pkg) => `${pkg.ecosystem}:${packageKey(pkg.name, pkg.version)}`), options.reason, options.env);
|
|
72
73
|
}
|
|
73
74
|
export function recordDecisionEvents(type, packageNames, reason, env) {
|
|
@@ -86,12 +87,6 @@ export function recordDecisionEvents(type, packageNames, reason, env) {
|
|
|
86
87
|
policyMode,
|
|
87
88
|
createdAt: new Date().toISOString()
|
|
88
89
|
};
|
|
89
|
-
|
|
90
|
-
recordAuditEvent(event, env);
|
|
91
|
-
emitWebhookEvent(event, env);
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// audit trail is best-effort; the decision write already succeeded
|
|
95
|
-
}
|
|
90
|
+
recordAuditEvent(event, env);
|
|
96
91
|
}
|
|
97
92
|
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from "react";
|
|
3
|
+
import { Box, Text, useInput } from "ink";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
6
|
+
import { basename, join, resolve, sep } from "node:path";
|
|
7
|
+
import { exportDestinations, resolveExportPath, userHomeDir, writeReportAtomic } from "../util/report-writer.js";
|
|
8
|
+
export function loginRequiredToast() {
|
|
9
|
+
return `${chalk.yellow("Sign in to export:")} ${chalk.cyan.bold("dg login")} ${chalk.dim("(free account)")}`;
|
|
10
|
+
}
|
|
11
|
+
const PATH_DISPLAY_MAX = 48;
|
|
12
|
+
function cursorBeforeExtension(name) {
|
|
13
|
+
const slash = name.lastIndexOf("/");
|
|
14
|
+
const dot = name.indexOf(".", slash + 2);
|
|
15
|
+
return dot > slash + 1 ? dot : name.length;
|
|
16
|
+
}
|
|
17
|
+
function fieldFor(name) {
|
|
18
|
+
return { value: name, cursor: cursorBeforeExtension(name) };
|
|
19
|
+
}
|
|
20
|
+
function abbreviateHome(dir, home) {
|
|
21
|
+
if (dir === home)
|
|
22
|
+
return "~";
|
|
23
|
+
if (dir.startsWith(home + sep))
|
|
24
|
+
return `~${dir.slice(home.length)}`;
|
|
25
|
+
return dir;
|
|
26
|
+
}
|
|
27
|
+
function truncatePath(text) {
|
|
28
|
+
if (text.length <= PATH_DISPLAY_MAX)
|
|
29
|
+
return text;
|
|
30
|
+
return `…${text.slice(text.length - PATH_DISPLAY_MAX + 1)}`;
|
|
31
|
+
}
|
|
32
|
+
function prefillFor(dir, cwd, home, filename) {
|
|
33
|
+
if (resolve(dir) === resolve(cwd))
|
|
34
|
+
return filename;
|
|
35
|
+
if (dir === home)
|
|
36
|
+
return `~/${filename}`;
|
|
37
|
+
if (dir.startsWith(home + sep))
|
|
38
|
+
return `~${dir.slice(home.length)}/${filename}`;
|
|
39
|
+
return join(dir, filename);
|
|
40
|
+
}
|
|
41
|
+
function completePath(value, cwd) {
|
|
42
|
+
const slash = value.lastIndexOf("/");
|
|
43
|
+
const dir = slash < 0 ? "" : value.slice(0, slash + 1);
|
|
44
|
+
const fragment = slash < 0 ? value : value.slice(slash + 1);
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(resolveExportPath(dir === "" ? "." : dir, cwd), { withFileTypes: true })
|
|
48
|
+
.map((entry) => ({ name: entry.name, isDir: entry.isDirectory() }));
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
const match = entries
|
|
54
|
+
.filter((entry) => entry.name.toLowerCase().startsWith(fragment.toLowerCase()))
|
|
55
|
+
.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))[0];
|
|
56
|
+
if (!match)
|
|
57
|
+
return value;
|
|
58
|
+
return `${dir}${match.name}${match.isDir ? "/" : ""}`;
|
|
59
|
+
}
|
|
60
|
+
export const ExportDialog = ({ options, theme, cwd, onDone, env = process.env }) => {
|
|
61
|
+
const multi = options.length > 1;
|
|
62
|
+
const [stage, setStage] = useState(multi ? "format" : "dest");
|
|
63
|
+
const [selected, setSelected] = useState(0);
|
|
64
|
+
const [destIndex, setDestIndex] = useState(0);
|
|
65
|
+
const [field, setField] = useState({ value: "", cursor: 0 });
|
|
66
|
+
const home = useMemo(() => userHomeDir(env), [env]);
|
|
67
|
+
const destinations = useMemo(() => exportDestinations(cwd, env), [cwd, env]);
|
|
68
|
+
const option = options[selected];
|
|
69
|
+
const filename = basename(option?.defaultName ?? "");
|
|
70
|
+
const rowCount = destinations.length + 1;
|
|
71
|
+
const writeTo = (target) => {
|
|
72
|
+
if (!option)
|
|
73
|
+
return;
|
|
74
|
+
try {
|
|
75
|
+
writeReportAtomic(target, option.render());
|
|
76
|
+
onDone({ path: target });
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
onDone({ error: error instanceof Error ? error.message : "write failed" });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const openEditor = (dest) => {
|
|
83
|
+
const prefill = dest ? prefillFor(dest.dir, cwd, home, filename) : filename;
|
|
84
|
+
setField(fieldFor(prefill));
|
|
85
|
+
setStage("path");
|
|
86
|
+
};
|
|
87
|
+
const insertAtCursor = (text) => {
|
|
88
|
+
setField((f) => ({ value: f.value.slice(0, f.cursor) + text + f.value.slice(f.cursor), cursor: f.cursor + text.length }));
|
|
89
|
+
};
|
|
90
|
+
const moveCursor = (to) => {
|
|
91
|
+
setField((f) => ({ ...f, cursor: Math.max(0, Math.min(f.value.length, to(f))) }));
|
|
92
|
+
};
|
|
93
|
+
useInput((input, key) => {
|
|
94
|
+
if (stage === "format") {
|
|
95
|
+
if (key.escape || input === "q") {
|
|
96
|
+
onDone(null);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (key.return) {
|
|
100
|
+
setStage("dest");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (key.upArrow || input === "k") {
|
|
104
|
+
setSelected((s) => Math.max(0, s - 1));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (key.downArrow || input === "j") {
|
|
108
|
+
setSelected((s) => Math.min(options.length - 1, s + 1));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (stage === "dest") {
|
|
114
|
+
if (key.escape) {
|
|
115
|
+
if (multi)
|
|
116
|
+
setStage("format");
|
|
117
|
+
else
|
|
118
|
+
onDone(null);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (input === "t") {
|
|
122
|
+
openEditor(destinations[destIndex]);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (key.upArrow || input === "k") {
|
|
126
|
+
setDestIndex((d) => Math.max(0, d - 1));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (key.downArrow || input === "j") {
|
|
130
|
+
setDestIndex((d) => Math.min(rowCount - 1, d + 1));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (key.return) {
|
|
134
|
+
const dest = destinations[destIndex];
|
|
135
|
+
if (dest)
|
|
136
|
+
writeTo(join(dest.dir, filename));
|
|
137
|
+
else
|
|
138
|
+
openEditor(destinations[destIndex - 1] ?? destinations[0]);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (key.escape) {
|
|
143
|
+
setStage("dest");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (key.return) {
|
|
147
|
+
const trimmed = field.value.trim();
|
|
148
|
+
if (trimmed.length > 0)
|
|
149
|
+
writeTo(resolveExportPath(trimmed, cwd));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (key.tab) {
|
|
153
|
+
setField((f) => fieldFor(completePath(f.value, cwd)));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (key.leftArrow) {
|
|
157
|
+
moveCursor((f) => f.cursor - 1);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (key.rightArrow) {
|
|
161
|
+
moveCursor((f) => f.cursor + 1);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.ctrl && input === "a") {
|
|
165
|
+
moveCursor(() => 0);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (key.ctrl && input === "e") {
|
|
169
|
+
moveCursor((f) => f.value.length);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (key.delete || key.backspace) {
|
|
173
|
+
setField((f) => (f.cursor === 0 ? f : { value: f.value.slice(0, f.cursor - 1) + f.value.slice(f.cursor), cursor: f.cursor - 1 }));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (input && !key.ctrl && !key.meta && /^[\x20-\x7e]+$/.test(input)) {
|
|
177
|
+
insertAtCursor(input);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
const target = field.value.trim().length > 0 ? resolveExportPath(field.value, cwd) : null;
|
|
181
|
+
const targetExists = target !== null && existsSync(target);
|
|
182
|
+
const beforeCursor = field.value.slice(0, field.cursor);
|
|
183
|
+
const atCursor = field.value.charAt(field.cursor);
|
|
184
|
+
const afterCursor = field.value.slice(field.cursor + 1);
|
|
185
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingLeft: 2, paddingRight: 2, children: [_jsx(Text, { bold: true, children: "Export" }), _jsx(Text, { children: "" }), stage === "format" && options.map((opt, idx) => {
|
|
186
|
+
const isSelected = idx === selected;
|
|
187
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 5, flexShrink: 0, children: _jsxs(Text, { children: [" ", isSelected ? chalk.cyan("●") : chalk.dim("○")] }) }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { wrap: "truncate-end", children: isSelected ? chalk.bold(opt.label) : opt.label }) })] }, opt.label));
|
|
188
|
+
}), stage === "dest" && (_jsxs(_Fragment, { children: [destinations.map((dest, idx) => {
|
|
189
|
+
const isSelected = idx === destIndex;
|
|
190
|
+
const replaces = existsSync(join(dest.dir, filename));
|
|
191
|
+
const shown = truncatePath(`${abbreviateHome(dest.dir, home)}/${filename}`.replace(/\/+/g, "/"));
|
|
192
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsxs(Text, { children: [" ", isSelected ? chalk.cyan("▸") : " "] }) }), _jsx(Box, { width: 14, flexShrink: 0, children: _jsx(Text, { children: isSelected ? chalk.bold(dest.label) : dest.label }) }), _jsx(Box, { flexShrink: 1, children: _jsxs(Text, { wrap: "truncate-start", children: [theme.paint("muted", shown), replaces ? theme.paint("warn", " (replaces)") : ""] }) })] }, dest.label));
|
|
193
|
+
}), _jsxs(Box, { children: [_jsx(Box, { width: 3, flexShrink: 0, children: _jsxs(Text, { children: [" ", destIndex === destinations.length ? chalk.cyan("▸") : " "] }) }), _jsx(Box, { flexShrink: 1, children: _jsx(Text, { children: destIndex === destinations.length ? chalk.bold("Type a path…") : theme.paint("muted", "Type a path…") }) })] })] })), stage === "path" && (_jsxs(_Fragment, { children: [option && _jsx(Text, { dimColor: true, wrap: "truncate-end", children: option.label }), _jsxs(Text, { wrap: "truncate-start", children: [chalk.cyan("▌ "), chalk.bold("Save as"), " ", beforeCursor, atCursor === "" ? chalk.cyan("█") : chalk.inverse(atCursor) + afterCursor] }), _jsx(Text, { wrap: "truncate-start", children: target === null
|
|
194
|
+
? theme.paint("muted", "enter a file name")
|
|
195
|
+
: targetExists
|
|
196
|
+
? theme.paint("warn", `${target} exists — Enter overwrites`)
|
|
197
|
+
: theme.paint("muted", `→ ${target}`) })] }))] }), _jsxs(Text, { children: [" ", stage === "format" ? (_jsxs(_Fragment, { children: [chalk.bold.cyan("↑↓"), " ", chalk.dim("choose"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim("next"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("cancel")] })) : stage === "dest" ? (_jsxs(_Fragment, { children: [chalk.bold.cyan("↑↓"), " ", chalk.dim("choose"), " ", chalk.bold.cyan("⏎"), " ", chalk.dim(`save ${filename}`), " ", chalk.bold.cyan("t"), " ", chalk.dim("type path"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim(multi ? "back" : "cancel")] })) : (_jsxs(_Fragment, { children: [chalk.bold.cyan("⏎"), " ", chalk.dim("save"), " ", chalk.bold.cyan("Tab"), " ", chalk.dim("complete"), " ", chalk.bold.cyan("Esc"), " ", chalk.dim("back")] }))] })] }));
|
|
198
|
+
};
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import InkSpinner from "ink-spinner";
|
|
4
4
|
import { formatResetDate } from "./block-render.js";
|
|
5
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
5
6
|
function packageCount(n) {
|
|
6
7
|
return n === 1 ? "1 package" : `${n} packages`;
|
|
7
8
|
}
|
|
@@ -13,9 +14,9 @@ export const LiveInstall = ({ view }) => {
|
|
|
13
14
|
if (view.phase === "scanning") {
|
|
14
15
|
return (_jsxs(Box, { paddingX: 1, children: [_jsx(Text, { color: "cyan", children: _jsx(InkSpinner, { type: "dots" }) }), _jsxs(Text, { children: [" ", view.total === 0
|
|
15
16
|
? "DG starting protection…"
|
|
16
|
-
: view.resolvedTotal !== undefined
|
|
17
|
-
? `DG verifying ${view.total}/${
|
|
18
|
-
: `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", view.current] }) : null] }));
|
|
17
|
+
: view.resolvedTotal !== undefined && view.total <= view.resolvedTotal
|
|
18
|
+
? `DG verifying ${view.total}/${view.resolvedTotal}…`
|
|
19
|
+
: `DG verifying ${packageCount(view.total)}…`] }), view.current ? _jsxs(Text, { dimColor: true, children: [" ", sanitizeLine(view.current)] }) : null] }));
|
|
19
20
|
}
|
|
20
21
|
if (view.total === 0 && !view.blocked) {
|
|
21
22
|
return null;
|
|
@@ -30,7 +31,7 @@ export const LiveInstall = ({ view }) => {
|
|
|
30
31
|
}
|
|
31
32
|
const total = view.verified + view.flagged;
|
|
32
33
|
if (view.flagged > 0) {
|
|
33
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: ["\u26A0 DG verified ", packageCount(total), " \u2014 ", view.flagged, " flagged"] }), (view.flaggedItems ?? []).map((item, index) => (_jsxs(Text, { dimColor: true, children: [" ", item.packageName, " ", item.reason] }, `${item.packageName}-${index}`)))] }));
|
|
34
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: "yellow", children: ["\u26A0 DG verified ", packageCount(total), " \u2014 ", view.flagged, " flagged"] }), (view.flaggedItems ?? []).map((item, index) => (_jsxs(Text, { dimColor: true, children: [" ", sanitizeLine(item.packageName), " ", sanitizeLine(item.reason)] }, `${item.packageName}-${index}`)))] }));
|
|
34
35
|
}
|
|
35
36
|
return (_jsx(Box, { paddingX: 1, children: _jsxs(Text, { color: "green", children: ["\u2713 DG verified ", packageCount(total), " \u2014 clean"] }) }));
|
|
36
37
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatCooldownDuration, formatPackageAge } from "../policy/cooldown.js";
|
|
2
|
+
import { sanitizeLine } from "../security/sanitize.js";
|
|
2
3
|
const VERIFIED_BAD = new Set([
|
|
3
4
|
"malware",
|
|
4
5
|
"policy",
|
|
@@ -32,7 +33,7 @@ const NEXT_STEP = {
|
|
|
32
33
|
"private-upload-disabled": "Enable private artifact scanning to verify this package.",
|
|
33
34
|
"needs-login": "Run 'dg login' (free) to check packages from the registry before they install.",
|
|
34
35
|
"quota-exceeded": "Upgrade your plan or wait for your monthly limit to reset. See westbayberry.com/pricing.",
|
|
35
|
-
cooldown: "Wait
|
|
36
|
+
cooldown: "Wait it out (see holds: dg cooldown), pin an older version, or exempt it: dg cooldown exempt <name>"
|
|
36
37
|
};
|
|
37
38
|
function cooldownDetailLine(cooldown) {
|
|
38
39
|
const window = formatCooldownDuration(cooldown.requiredDays);
|
|
@@ -53,9 +54,9 @@ export function describeBlockedInstall(decision) {
|
|
|
53
54
|
: "Re-check later with 'dg verify', or override if you accept the risk.";
|
|
54
55
|
return {
|
|
55
56
|
kind: verifiedBad ? "blocked" : "unverified",
|
|
56
|
-
packageName: decision.packageName,
|
|
57
|
+
packageName: sanitizeLine(decision.packageName),
|
|
57
58
|
headline: HEADLINES[decision.cause],
|
|
58
|
-
reason: decision.reason,
|
|
59
|
+
reason: sanitizeLine(decision.reason),
|
|
59
60
|
cause: decision.cause,
|
|
60
61
|
...(decision.resetsAt ? { resetsAt: decision.resetsAt } : {}),
|
|
61
62
|
...(nextStep ? { nextStep } : {}),
|
|
@@ -63,27 +64,30 @@ export function describeBlockedInstall(decision) {
|
|
|
63
64
|
};
|
|
64
65
|
}
|
|
65
66
|
export function describeFlaggedWarn(decision) {
|
|
67
|
+
const packageName = sanitizeLine(decision.packageName);
|
|
66
68
|
if (decision.cause === "quota-exceeded") {
|
|
67
|
-
return { packageName
|
|
69
|
+
return { packageName, reason: "installed unverified (over quota)" };
|
|
68
70
|
}
|
|
69
71
|
if (decision.forceOverride?.allowed) {
|
|
70
|
-
return { packageName
|
|
72
|
+
return { packageName, reason: "installed despite block (--dg-force-install)" };
|
|
71
73
|
}
|
|
72
|
-
return { packageName
|
|
74
|
+
return { packageName, reason: sanitizeLine(decision.reason) };
|
|
73
75
|
}
|
|
74
76
|
export function renderInstallDecision(decision) {
|
|
77
|
+
const packageName = sanitizeLine(decision.packageName);
|
|
78
|
+
const reason = sanitizeLine(decision.reason);
|
|
75
79
|
if (decision.action === "pass") {
|
|
76
|
-
return `✓ DG verified ${
|
|
80
|
+
return `✓ DG verified ${packageName} — clean\n`;
|
|
77
81
|
}
|
|
78
82
|
if (decision.action === "warn") {
|
|
79
83
|
if (decision.cause === "quota-exceeded") {
|
|
80
84
|
const reset = formatResetDate(decision.resetsAt);
|
|
81
|
-
return `⚠ Over quota — installed ${
|
|
85
|
+
return `⚠ Over quota — installed ${packageName} unverified${reset ? ` (resets ${reset})` : ""}\n`;
|
|
82
86
|
}
|
|
83
87
|
if (decision.forceOverride?.allowed) {
|
|
84
|
-
return `⚠ DG override — installing ${
|
|
88
|
+
return `⚠ DG override — installing ${packageName} despite block (--dg-force-install)\n`;
|
|
85
89
|
}
|
|
86
|
-
return `⚠ DG flagged ${
|
|
90
|
+
return `⚠ DG flagged ${packageName} (warn) — ${reason}\n`;
|
|
87
91
|
}
|
|
88
92
|
if (decision.cause === "quota-exceeded") {
|
|
89
93
|
const reset = formatResetDate(decision.resetsAt);
|
|
@@ -98,15 +102,15 @@ export function renderInstallDecision(decision) {
|
|
|
98
102
|
verifiedBad
|
|
99
103
|
? `✘ DG blocked install — ${headline}`
|
|
100
104
|
: decision.cause === "cooldown"
|
|
101
|
-
? `? DG quarantined ${
|
|
102
|
-
: `? DG could not verify ${
|
|
103
|
-
` ${
|
|
105
|
+
? `? DG quarantined ${packageName} — ${headline}`
|
|
106
|
+
: `? DG could not verify ${packageName} — ${headline}`,
|
|
107
|
+
` ${packageName} ${reason}`
|
|
104
108
|
];
|
|
105
109
|
if (decision.cause === "cooldown" && decision.cooldown) {
|
|
106
110
|
lines.push(` ${cooldownDetailLine(decision.cooldown)}`);
|
|
107
111
|
}
|
|
108
112
|
if (decision.dashboardUrl) {
|
|
109
|
-
lines.push(` Evidence: ${decision.dashboardUrl}`);
|
|
113
|
+
lines.push(` Evidence: ${sanitizeLine(decision.dashboardUrl)}`);
|
|
110
114
|
}
|
|
111
115
|
if (decision.unauthenticated && decision.cause !== "needs-login") {
|
|
112
116
|
lines.push(" Auth: local policy only (run 'dg login' for full coverage)");
|