@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
package/dist/auth/store.js
CHANGED
|
@@ -26,6 +26,7 @@ export function readAuthState(env = process.env) {
|
|
|
26
26
|
}
|
|
27
27
|
const email = typeof parsed.email === "string" && parsed.email.length > 0 ? parsed.email : undefined;
|
|
28
28
|
const tier = typeof parsed.tier === "string" && parsed.tier.length > 0 ? parsed.tier : undefined;
|
|
29
|
+
const name = typeof parsed.name === "string" && parsed.name.length > 0 ? parsed.name : undefined;
|
|
29
30
|
return {
|
|
30
31
|
version: 1,
|
|
31
32
|
token: parsed.token,
|
|
@@ -34,7 +35,8 @@ export function readAuthState(env = process.env) {
|
|
|
34
35
|
orgId: parsed.orgId ?? "",
|
|
35
36
|
loggedInAt: parsed.loggedInAt,
|
|
36
37
|
...(email ? { email } : {}),
|
|
37
|
-
...(tier ? { tier } : {})
|
|
38
|
+
...(tier ? { tier } : {}),
|
|
39
|
+
...(name ? { name } : {})
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
catch (error) {
|
|
@@ -63,6 +65,7 @@ export function writeAuthState(options, env = process.env) {
|
|
|
63
65
|
}
|
|
64
66
|
const email = typeof options.email === "string" && options.email.length > 0 ? options.email : undefined;
|
|
65
67
|
const tier = typeof options.tier === "string" && options.tier.length > 0 ? options.tier : undefined;
|
|
68
|
+
const name = typeof options.name === "string" && options.name.length > 0 ? options.name : undefined;
|
|
66
69
|
return withUserConfigLock(env, () => {
|
|
67
70
|
const config = loadUserConfig(env);
|
|
68
71
|
const apiBaseUrl = options.apiBaseUrl ?? config.api.baseUrl;
|
|
@@ -75,7 +78,8 @@ export function writeAuthState(options, env = process.env) {
|
|
|
75
78
|
orgId,
|
|
76
79
|
loggedInAt: (options.now ?? new Date()).toISOString(),
|
|
77
80
|
...(email ? { email } : {}),
|
|
78
|
-
...(tier ? { tier } : {})
|
|
81
|
+
...(tier ? { tier } : {}),
|
|
82
|
+
...(name ? { name } : {})
|
|
79
83
|
};
|
|
80
84
|
const paths = resolveDgPaths(env);
|
|
81
85
|
writeJsonAtomic(authPath(paths), state);
|
|
@@ -120,7 +124,8 @@ export function authStatus(env = process.env) {
|
|
|
120
124
|
apiBaseUrl: state.apiBaseUrl,
|
|
121
125
|
orgId: state.orgId,
|
|
122
126
|
...(state.email ? { email: state.email } : {}),
|
|
123
|
-
...(state.tier ? { tier: state.tier } : {})
|
|
127
|
+
...(state.tier ? { tier: state.tier } : {}),
|
|
128
|
+
...(state.name ? { name: state.name } : {})
|
|
124
129
|
};
|
|
125
130
|
}
|
|
126
131
|
return {
|
package/dist/bin/dg.js
CHANGED
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { assertCurrentNode } from "../runtime/node-version.js";
|
|
3
|
-
assertCurrentNode();
|
|
4
|
-
const
|
|
5
|
-
function exitOnFatal(error) {
|
|
6
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
7
|
-
try {
|
|
8
|
-
process.stderr.write(`dg: unexpected error — ${message}\nRun 'dg doctor' to check your installation; remove ~/.dg/config.json if it is corrupted.\n`);
|
|
9
|
-
}
|
|
10
|
-
catch {
|
|
11
|
-
// stderr is gone; nothing left to report to.
|
|
12
|
-
}
|
|
13
|
-
process.exit(EXIT_TOOL_ERROR);
|
|
14
|
-
}
|
|
3
|
+
await assertCurrentNode();
|
|
4
|
+
const { exitOnFatal } = await import("../runtime/fatal.js");
|
|
15
5
|
process.on("uncaughtException", (error) => {
|
|
16
6
|
if (error.code === "EPIPE") {
|
|
17
7
|
process.exit(process.exitCode ?? 0);
|
|
@@ -30,52 +20,69 @@ const { runCli, writeCliResult } = await import("../runtime/cli.js");
|
|
|
30
20
|
const { maybeShowFirstRun } = await import("../runtime/first-run.js");
|
|
31
21
|
const { maybePreflightInstallPrompt } = await import("../launcher/preflight-prompt.js");
|
|
32
22
|
const args = process.argv.slice(2);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
})
|
|
38
|
-
.catch(() => { });
|
|
39
|
-
const { maybeDeviceLogin } = await import("../auth/device-login.js");
|
|
40
|
-
const { maybeVerifyPackage } = await import("../verify/package-check.js");
|
|
41
|
-
const { maybeAudit } = await import("../commands/audit.js");
|
|
42
|
-
const notHandled = { handled: false };
|
|
43
|
-
const deviceLogin = await maybeDeviceLogin(args);
|
|
44
|
-
const verifyPackage = deviceLogin.handled ? notHandled : await maybeVerifyPackage(args);
|
|
45
|
-
const audit = deviceLogin.handled || verifyPackage.handled ? notHandled : await maybeAudit(args);
|
|
46
|
-
if (deviceLogin.handled) {
|
|
47
|
-
writeCliResult(deviceLogin.result);
|
|
48
|
-
}
|
|
49
|
-
else if (verifyPackage.handled) {
|
|
50
|
-
writeCliResult(verifyPackage.result);
|
|
51
|
-
}
|
|
52
|
-
else if (audit.handled) {
|
|
53
|
-
writeCliResult(audit.result);
|
|
23
|
+
const { maybeAgentHookExec } = await import("../launcher/agent-hook-exec.js");
|
|
24
|
+
const agentHookExec = await maybeAgentHookExec(args);
|
|
25
|
+
if (agentHookExec.handled) {
|
|
26
|
+
writeCliResult(agentHookExec.result);
|
|
54
27
|
}
|
|
55
28
|
else {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
29
|
+
const { maybeOfferSetupWizard } = await import("../setup-ui/offer.js");
|
|
30
|
+
const wizardOffer = await maybeOfferSetupWizard(args);
|
|
31
|
+
if (wizardOffer.handled) {
|
|
32
|
+
writeCliResult(wizardOffer.result);
|
|
59
33
|
}
|
|
60
34
|
else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
35
|
+
if (wizardOffer.result.stderr) {
|
|
36
|
+
process.stderr.write(wizardOffer.result.stderr);
|
|
37
|
+
}
|
|
38
|
+
maybeShowFirstRun(args);
|
|
39
|
+
import("../state/index.js")
|
|
40
|
+
.then(({ pruneDeadSessionsSync, resolveDgPaths }) => {
|
|
41
|
+
pruneDeadSessionsSync(resolveDgPaths());
|
|
42
|
+
})
|
|
43
|
+
.catch(() => { });
|
|
44
|
+
const { maybeDeviceLogin } = await import("../auth/device-login.js");
|
|
45
|
+
const { maybeVerifyPackage } = await import("../verify/package-check.js");
|
|
46
|
+
const { maybeAudit } = await import("../commands/audit.js");
|
|
47
|
+
const notHandled = { handled: false };
|
|
48
|
+
const deviceLogin = await maybeDeviceLogin(args);
|
|
49
|
+
const verifyPackage = deviceLogin.handled ? notHandled : await maybeVerifyPackage(args);
|
|
50
|
+
const audit = deviceLogin.handled || verifyPackage.handled ? notHandled : await maybeAudit(args);
|
|
51
|
+
if (deviceLogin.handled) {
|
|
52
|
+
writeCliResult(deviceLogin.result);
|
|
53
|
+
}
|
|
54
|
+
else if (verifyPackage.handled) {
|
|
55
|
+
writeCliResult(verifyPackage.result);
|
|
56
|
+
}
|
|
57
|
+
else if (audit.handled) {
|
|
58
|
+
writeCliResult(audit.result);
|
|
65
59
|
}
|
|
66
60
|
else {
|
|
67
|
-
|
|
61
|
+
const preflight = await maybePreflightInstallPrompt(args, { decisionsCwd: process.cwd() });
|
|
62
|
+
if (preflight.handled) {
|
|
63
|
+
writeCliResult(preflight.result);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const { maybeRunLiveInstall } = await import("../launcher/live-install.js");
|
|
67
|
+
const liveInstall = await maybeRunLiveInstall(args);
|
|
68
|
+
if (liveInstall.handled) {
|
|
69
|
+
writeCliResult(liveInstall.result);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
writeCliResult(await runCli(args));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// The auth flows (browser login, paid-verify gate, deep audit upload) already
|
|
77
|
+
// tell the user exactly what to do; the throttled nudges would just be noise.
|
|
78
|
+
if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
|
|
79
|
+
try {
|
|
80
|
+
const { maybeShowNudges } = await import("../runtime/nudges.js");
|
|
81
|
+
maybeShowNudges(args);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// dg deleted its own files mid-run (uninstall of itself); nudges are cosmetic.
|
|
85
|
+
}
|
|
68
86
|
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
// The auth flows (browser login, paid-verify gate, deep audit upload) already
|
|
72
|
-
// tell the user exactly what to do; the throttled nudges would just be noise.
|
|
73
|
-
if (!deviceLogin.handled && !verifyPackage.handled && !audit.handled) {
|
|
74
|
-
try {
|
|
75
|
-
const { maybeShowNudges } = await import("../runtime/nudges.js");
|
|
76
|
-
maybeShowNudges(args);
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// dg deleted its own files mid-run (uninstall of itself); nudges are cosmetic.
|
|
80
87
|
}
|
|
81
88
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { AGENT_IDS, AGENTS, agentLabel, collectAgentOffers, getAgent, isAgentId, resolveAgentHookContext, } from "../agents/registry.js";
|
|
3
|
+
import { AgentHookError } from "../agents/persistence.js";
|
|
4
|
+
import { resolvePresentation } from "../presentation/mode.js";
|
|
5
|
+
import { createTheme } from "../presentation/theme.js";
|
|
6
|
+
import { tildifyPath as tildify } from "../setup/plan.js";
|
|
7
|
+
import { EXIT_USAGE } from "./types.js";
|
|
8
|
+
const VERBS = ["on", "off", "--check", "--print"];
|
|
9
|
+
const LEGACY_VERB_MAP = {
|
|
10
|
+
install: "on",
|
|
11
|
+
on: "on",
|
|
12
|
+
off: "off",
|
|
13
|
+
remove: "off",
|
|
14
|
+
uninstall: "off",
|
|
15
|
+
"--check": "--check",
|
|
16
|
+
"--print": "--print",
|
|
17
|
+
};
|
|
18
|
+
function parseAgentsArgs(args) {
|
|
19
|
+
if (args.length === 0) {
|
|
20
|
+
return { verb: "status", agents: [], recordFixture: false };
|
|
21
|
+
}
|
|
22
|
+
let rest = [...args];
|
|
23
|
+
let verb = null;
|
|
24
|
+
const first = rest[0] ?? "";
|
|
25
|
+
if (isAgentId(first)) {
|
|
26
|
+
const legacy = rest[1] ?? "install";
|
|
27
|
+
const mapped = LEGACY_VERB_MAP[legacy];
|
|
28
|
+
if (!mapped || rest.length > 3 || (rest.length === 3 && (rest[2] !== "--record-fixture" || mapped !== "on"))) {
|
|
29
|
+
return { error: `unknown subcommand '${rest[1] ?? ""}'. Use: dg agents [on|off|--check|--print] [agent]` };
|
|
30
|
+
}
|
|
31
|
+
return { verb: mapped, agents: [first], recordFixture: rest[2] === "--record-fixture" };
|
|
32
|
+
}
|
|
33
|
+
if (VERBS.includes(first)) {
|
|
34
|
+
verb = first;
|
|
35
|
+
rest = rest.slice(1);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
return { error: `unknown subcommand '${first}'. Use: dg agents [on|off|--check|--print] [agent]` };
|
|
39
|
+
}
|
|
40
|
+
const agents = [];
|
|
41
|
+
let recordFixture = false;
|
|
42
|
+
for (const arg of rest) {
|
|
43
|
+
if (arg === "--record-fixture") {
|
|
44
|
+
recordFixture = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (!isAgentId(arg)) {
|
|
48
|
+
return { error: `unknown agent '${arg}'. Agents: ${AGENT_IDS.join(", ")}` };
|
|
49
|
+
}
|
|
50
|
+
agents.push(arg);
|
|
51
|
+
}
|
|
52
|
+
if (recordFixture && verb !== "on") {
|
|
53
|
+
return { error: "--record-fixture only applies to 'dg agents on'" };
|
|
54
|
+
}
|
|
55
|
+
return { verb, agents, recordFixture };
|
|
56
|
+
}
|
|
57
|
+
function agentRow(agent, home, env) {
|
|
58
|
+
const integration = AGENTS[agent];
|
|
59
|
+
if (!integration.detect(home)) {
|
|
60
|
+
return { agent, state: "not found", detail: "" };
|
|
61
|
+
}
|
|
62
|
+
const probe = integration.probeHookSupport(home);
|
|
63
|
+
if (!probe.supported) {
|
|
64
|
+
return { agent, state: "unsupported", detail: probe.detail };
|
|
65
|
+
}
|
|
66
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
67
|
+
const hooked = integration.verify(ctx).find((check) => check.name === integration.isInstalledCheckName)?.ok ?? false;
|
|
68
|
+
if (hooked) {
|
|
69
|
+
return { agent, state: "protected", detail: tildify(ctx.settingsPath, home) };
|
|
70
|
+
}
|
|
71
|
+
return { agent, state: "detected", detail: `dg agents on ${agent}` };
|
|
72
|
+
}
|
|
73
|
+
function renderStatusCard(rows, theme) {
|
|
74
|
+
const accent = (text) => theme.paint("accent", text);
|
|
75
|
+
const muted = (text) => theme.paint("muted", text);
|
|
76
|
+
const glyph = (state) => {
|
|
77
|
+
if (state === "protected") {
|
|
78
|
+
return theme.paint("pass", "✓");
|
|
79
|
+
}
|
|
80
|
+
if (state === "detected") {
|
|
81
|
+
return accent("·");
|
|
82
|
+
}
|
|
83
|
+
if (state === "unsupported") {
|
|
84
|
+
return theme.paint("warn", "✗");
|
|
85
|
+
}
|
|
86
|
+
return muted("–");
|
|
87
|
+
};
|
|
88
|
+
const labelWidth = Math.max(...rows.map((row) => agentLabel(row.agent).length));
|
|
89
|
+
const stateWidth = Math.max(...rows.map((row) => row.state.length));
|
|
90
|
+
const lines = [
|
|
91
|
+
"",
|
|
92
|
+
` ${accent("AI agents")} ${muted("— installs route through dg before fetching")}`,
|
|
93
|
+
""
|
|
94
|
+
];
|
|
95
|
+
for (const row of rows) {
|
|
96
|
+
const label = agentLabel(row.agent).padEnd(labelWidth);
|
|
97
|
+
const state = row.state === "protected" ? theme.paint("pass", row.state.padEnd(stateWidth)) : muted(row.state.padEnd(stateWidth));
|
|
98
|
+
const detail = row.state === "detected" ? accent(row.detail) : muted(row.detail);
|
|
99
|
+
lines.push(` ${glyph(row.state)} ${label} ${state} ${detail}`.trimEnd());
|
|
100
|
+
}
|
|
101
|
+
lines.push("", ` ${muted("protect all:")} ${accent("dg agents on")} ${muted("·")} ${muted("remove:")} ${accent("dg agents off")}`, "");
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
async function applyAgents(targets, env, home, theme, recordFixture) {
|
|
105
|
+
const accent = (text) => theme.paint("accent", text);
|
|
106
|
+
const muted = (text) => theme.paint("muted", text);
|
|
107
|
+
const lines = [];
|
|
108
|
+
let failures = 0;
|
|
109
|
+
for (const agent of targets) {
|
|
110
|
+
const integration = AGENTS[agent];
|
|
111
|
+
const probe = integration.probeHookSupport(home);
|
|
112
|
+
if (!probe.supported) {
|
|
113
|
+
failures += 1;
|
|
114
|
+
lines.push(` ${theme.paint("warn", `✗ ${integration.label}`)} ${muted(`— ${probe.detail}. The dg PATH shim still covers installs typed in its terminal.`)}`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
118
|
+
const dgCommand = recordFixture ? `${ctx.dgCommand} --record-fixture` : ctx.dgCommand;
|
|
119
|
+
try {
|
|
120
|
+
await integration.apply({ ...ctx, dgCommand });
|
|
121
|
+
lines.push(` ${theme.paint("pass", `✓ ${integration.label} installs route through dg`)} ${muted(`(${tildify(ctx.settingsPath, home)})`)}`);
|
|
122
|
+
if (integration.maturity === "unverified") {
|
|
123
|
+
lines.push(` ${muted(` hook installed from ${integration.label}'s documented schema; payload format not yet verified against a live ${integration.label} run`)}`);
|
|
124
|
+
}
|
|
125
|
+
if (integration.postInstallNote) {
|
|
126
|
+
lines.push(` ${muted(` ${integration.postInstallNote}`)}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
failures += 1;
|
|
131
|
+
const message = error instanceof AgentHookError ? error.message : error instanceof Error ? error.message : "agent hook error";
|
|
132
|
+
lines.push(` ${theme.paint("warn", `✗ ${integration.label}`)} ${muted(`— ${message}`)}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (lines.length === 0) {
|
|
136
|
+
return { exitCode: 0, stdout: ` ${muted("No unprotected agents detected.")} ${muted("See")} ${accent("dg agents")}\n`, stderr: "" };
|
|
137
|
+
}
|
|
138
|
+
lines.push(` ${muted("Reverse:")} ${accent("dg agents off")}`);
|
|
139
|
+
return { exitCode: failures > 0 && failures === targets.length ? 1 : 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
140
|
+
}
|
|
141
|
+
async function removeAgents(targets, env, home, theme) {
|
|
142
|
+
const muted = (text) => theme.paint("muted", text);
|
|
143
|
+
const lines = [];
|
|
144
|
+
for (const agent of targets) {
|
|
145
|
+
const integration = AGENTS[agent];
|
|
146
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
147
|
+
try {
|
|
148
|
+
const result = await integration.remove(ctx);
|
|
149
|
+
lines.push(result.removed
|
|
150
|
+
? ` ${theme.paint("pass", `✓ ${integration.label} hook removed`)} ${muted(`(${tildify(ctx.settingsPath, home)})`)}`
|
|
151
|
+
: ` ${muted(`· ${integration.label} — no dg hook was installed`)}`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
const message = error instanceof AgentHookError ? error.message : error instanceof Error ? error.message : "agent hook error";
|
|
155
|
+
lines.push(` ${theme.paint("warn", `✗ ${integration.label}`)} ${muted(`— ${message}`)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (lines.length === 0) {
|
|
159
|
+
lines.push(` ${muted("No agent hooks to remove.")}`);
|
|
160
|
+
}
|
|
161
|
+
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
162
|
+
}
|
|
163
|
+
function checkAgents(targets, env, home) {
|
|
164
|
+
const lines = [];
|
|
165
|
+
let allOk = true;
|
|
166
|
+
for (const agent of targets) {
|
|
167
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
168
|
+
const checks = getAgent(agent).verify(ctx);
|
|
169
|
+
lines.push(`${agentLabel(agent)}:`);
|
|
170
|
+
for (const check of checks) {
|
|
171
|
+
lines.push(` ${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`);
|
|
172
|
+
allOk = allOk && check.ok;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { exitCode: allOk ? 0 : 1, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
176
|
+
}
|
|
177
|
+
function printAgents(targets, env, home) {
|
|
178
|
+
const lines = [];
|
|
179
|
+
for (const agent of targets) {
|
|
180
|
+
const integration = AGENTS[agent];
|
|
181
|
+
const ctx = resolveAgentHookContext(agent, { env, home });
|
|
182
|
+
const probe = integration.probeHookSupport(home);
|
|
183
|
+
lines.push(`dg agents on ${agent} would write a dg hook to:`, ` ${ctx.settingsPath}`, "running:", ` ${ctx.dgCommand}`, `probe: ${probe.supported ? "supported" : "unsupported"} — ${probe.detail}`, `Reverse with: dg agents off ${agent} (or dg uninstall)`);
|
|
184
|
+
}
|
|
185
|
+
return { exitCode: 0, stdout: `${lines.join("\n")}\n`, stderr: "" };
|
|
186
|
+
}
|
|
187
|
+
export async function runAgentsCommand(args, env = process.env, home) {
|
|
188
|
+
const parsed = parseAgentsArgs(args);
|
|
189
|
+
if ("error" in parsed) {
|
|
190
|
+
return { exitCode: EXIT_USAGE, stdout: "", stderr: `dg agents: ${parsed.error}\n` };
|
|
191
|
+
}
|
|
192
|
+
const resolvedHome = home ?? env.HOME ?? homedir();
|
|
193
|
+
const theme = createTheme(resolvePresentation().color);
|
|
194
|
+
if (parsed.verb === "status") {
|
|
195
|
+
const rows = AGENT_IDS.map((agent) => agentRow(agent, resolvedHome, env));
|
|
196
|
+
return { exitCode: 0, stdout: `${renderStatusCard(rows, theme)}\n`, stderr: "" };
|
|
197
|
+
}
|
|
198
|
+
if (parsed.verb === "on") {
|
|
199
|
+
const targets = parsed.agents.length > 0 ? parsed.agents : collectAgentOffers({ env, home: resolvedHome }).map((offer) => offer.agent);
|
|
200
|
+
return applyAgents(targets, env, resolvedHome, theme, parsed.recordFixture);
|
|
201
|
+
}
|
|
202
|
+
if (parsed.verb === "off") {
|
|
203
|
+
const targets = parsed.agents.length > 0
|
|
204
|
+
? parsed.agents
|
|
205
|
+
: AGENT_IDS.filter((agent) => agentRow(agent, resolvedHome, env).state === "protected");
|
|
206
|
+
return removeAgents(targets, env, resolvedHome, theme);
|
|
207
|
+
}
|
|
208
|
+
const targets = parsed.agents.length > 0
|
|
209
|
+
? parsed.agents
|
|
210
|
+
: AGENT_IDS.filter((agent) => AGENTS[agent].detect(resolvedHome));
|
|
211
|
+
if (targets.length === 0) {
|
|
212
|
+
return { exitCode: 1, stdout: "", stderr: "dg agents: no AI agents detected on this machine.\n" };
|
|
213
|
+
}
|
|
214
|
+
return parsed.verb === "--check" ? checkAgents(targets, env, resolvedHome) : printAgents(targets, env, resolvedHome);
|
|
215
|
+
}
|
|
216
|
+
async function handle({ args }) {
|
|
217
|
+
return runAgentsCommand(args);
|
|
218
|
+
}
|
|
219
|
+
export const agentsCommand = {
|
|
220
|
+
name: "agents",
|
|
221
|
+
aliases: ["hook"],
|
|
222
|
+
summary: "Route AI coding agents' package installs through dg's firewall",
|
|
223
|
+
usage: "dg agents [on|off|--check|--print] [agent ...]",
|
|
224
|
+
details: [
|
|
225
|
+
"Bare 'dg agents' shows every supported agent and whether its installs are protected.",
|
|
226
|
+
"'dg agents on' installs a native pre-command hook into every detected agent (or name one, e.g. 'dg agents on claude-code') so the shell installs it runs are checked by dg and blocked before a malicious package is fetched. Reversible via 'dg agents off' or 'dg uninstall'.",
|
|
227
|
+
],
|
|
228
|
+
args: [{ name: "agent", summary: AGENT_IDS.join(" | ") }],
|
|
229
|
+
examples: ["dg agents", "dg agents on", "dg agents on claude-code", "dg agents --check", "dg agents off"],
|
|
230
|
+
handler: handle,
|
|
231
|
+
};
|
package/dist/commands/audit.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, resolve } from "node:path";
|
|
3
|
+
import { writeReportAtomic } from "../util/report-writer.js";
|
|
3
4
|
import { EXIT_TOOL_ERROR, EXIT_USAGE_VERDICT } from "./types.js";
|
|
4
5
|
import { resolvePresentation } from "../presentation/mode.js";
|
|
5
6
|
import { createTheme } from "../presentation/theme.js";
|
|
@@ -34,6 +35,15 @@ export const auditCommand = {
|
|
|
34
35
|
],
|
|
35
36
|
handler: (context) => runAuditCommand(context.args)
|
|
36
37
|
};
|
|
38
|
+
function privatePackageResult(scope) {
|
|
39
|
+
const theme = createTheme(resolvePresentation().color);
|
|
40
|
+
const name = typeof scope.packageJson?.name === "string" ? scope.packageJson.name : basename(scope.root);
|
|
41
|
+
return {
|
|
42
|
+
exitCode: 0,
|
|
43
|
+
stdout: `${theme.paint("pass", "✓")} ${name} is private — npm refuses to publish it, so there is nothing to audit ${theme.paint("muted", "(remove \"private\": true to audit it for publish)")}\n`,
|
|
44
|
+
stderr: ""
|
|
45
|
+
};
|
|
46
|
+
}
|
|
37
47
|
function gather(args) {
|
|
38
48
|
const parsed = parseAuditArgs(args);
|
|
39
49
|
if ("error" in parsed) {
|
|
@@ -43,6 +53,9 @@ function gather(args) {
|
|
|
43
53
|
if ("error" in scope) {
|
|
44
54
|
return { error: scope.error, usage: false };
|
|
45
55
|
}
|
|
56
|
+
if (scope.ecosystem === "npm" && scope.packageJson?.private === true) {
|
|
57
|
+
return { privatePackage: scope };
|
|
58
|
+
}
|
|
46
59
|
const set = scope.ecosystem === "pypi" ? pypiPublishSet(scope.root) : npmPublishSet(scope.root);
|
|
47
60
|
const files = set.relPaths
|
|
48
61
|
.map((relPath) => buildAuditFile(scope.root, relPath))
|
|
@@ -69,10 +82,10 @@ function finalize(gathered, deep) {
|
|
|
69
82
|
deep
|
|
70
83
|
};
|
|
71
84
|
const theme = createTheme(resolvePresentation().color);
|
|
72
|
-
const rendered = parsed.format === "json" ? `${JSON.stringify(report, null, 2)}\n` : renderReport(report, theme);
|
|
85
|
+
const rendered = parsed.format === "json" ? `${JSON.stringify({ schemaVersion: 1, ...report }, null, 2)}\n` : renderReport(report, theme);
|
|
73
86
|
if (parsed.outputPath) {
|
|
74
87
|
try {
|
|
75
|
-
|
|
88
|
+
writeReportAtomic(resolve(parsed.outputPath), rendered);
|
|
76
89
|
}
|
|
77
90
|
catch (error) {
|
|
78
91
|
return { exitCode: EXIT_TOOL_ERROR, stdout: "", stderr: `dg audit could not write ${parsed.outputPath}: ${error instanceof Error ? error.message : "write error"}\n` };
|
|
@@ -89,6 +102,9 @@ function runAuditCommand(args) {
|
|
|
89
102
|
if ("error" in gathered) {
|
|
90
103
|
return gatherError(gathered);
|
|
91
104
|
}
|
|
105
|
+
if ("privatePackage" in gathered) {
|
|
106
|
+
return privatePackageResult(gathered.privatePackage);
|
|
107
|
+
}
|
|
92
108
|
const decision = deepDecision(gathered.scope, gathered.parsed.local);
|
|
93
109
|
return finalize(gathered, { ran: false, reason: decision.reason });
|
|
94
110
|
}
|
|
@@ -104,6 +120,9 @@ export async function maybeAudit(args) {
|
|
|
104
120
|
if ("error" in gathered) {
|
|
105
121
|
return { handled: true, result: gatherError(gathered) };
|
|
106
122
|
}
|
|
123
|
+
if ("privatePackage" in gathered) {
|
|
124
|
+
return { handled: true, result: privatePackageResult(gathered.privatePackage) };
|
|
125
|
+
}
|
|
107
126
|
let decision = deepDecision(gathered.scope, gathered.parsed.local);
|
|
108
127
|
if (!decision.upload && canPromptConsent(gathered)) {
|
|
109
128
|
if (!(await teamPolicyBlocksUpload())) {
|
package/dist/commands/config.js
CHANGED
|
@@ -3,12 +3,15 @@ import { ConfigError, getConfigValue, isConfigKey, listConfig, loadUserConfig, s
|
|
|
3
3
|
export const configCommand = {
|
|
4
4
|
name: "config",
|
|
5
5
|
summary: "Inspect or edit trusted dg configuration.",
|
|
6
|
-
usage: "dg config <get|set|unset|list> [key] [value] [--json]",
|
|
6
|
+
usage: "dg config <get|set|unset|list> [key] [value] [--json] [--all]",
|
|
7
7
|
args: [
|
|
8
8
|
{ name: "<action>", summary: "list | get <key> | set <key> <value> | unset <key>." },
|
|
9
9
|
{ name: "[key]", summary: "e.g. policy.mode, gitHook.onWarn, cooldown.age, cooldown.exempt, api.baseUrl." }
|
|
10
10
|
],
|
|
11
|
-
flags: [
|
|
11
|
+
flags: [
|
|
12
|
+
{ flag: "--json", summary: "Machine-readable output for list/get." },
|
|
13
|
+
{ flag: "--all", summary: "Include advanced keys in list (per-ecosystem cooldowns, internal flags)." }
|
|
14
|
+
],
|
|
12
15
|
examples: [
|
|
13
16
|
"dg config list",
|
|
14
17
|
"dg config get policy.mode",
|
|
@@ -26,7 +29,8 @@ export const configCommand = {
|
|
|
26
29
|
};
|
|
27
30
|
function configHandler(args) {
|
|
28
31
|
const json = args.includes("--json");
|
|
29
|
-
const
|
|
32
|
+
const all = args.includes("--all");
|
|
33
|
+
const filtered = args.filter((arg) => arg !== "--json" && arg !== "--all");
|
|
30
34
|
const [action, key, value, extra] = filtered;
|
|
31
35
|
if (!action || extra) {
|
|
32
36
|
return usageError("expected get, set, unset, or list");
|
|
@@ -37,10 +41,10 @@ function configHandler(args) {
|
|
|
37
41
|
if (key || value) {
|
|
38
42
|
return usageError("list does not accept a key or value");
|
|
39
43
|
}
|
|
40
|
-
const entries = listConfig(config);
|
|
44
|
+
const entries = listConfig(config, all);
|
|
41
45
|
return {
|
|
42
46
|
exitCode: 0,
|
|
43
|
-
stdout: json ? `${JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])), null, 2)}\n` : renderConfigList(entries),
|
|
47
|
+
stdout: json ? `${JSON.stringify(Object.fromEntries(entries.map((entry) => [entry.key, entry.value])), null, 2)}\n` : renderConfigList(entries, !all),
|
|
44
48
|
stderr: ""
|
|
45
49
|
};
|
|
46
50
|
}
|
|
@@ -93,8 +97,12 @@ function configHandler(args) {
|
|
|
93
97
|
throw error;
|
|
94
98
|
}
|
|
95
99
|
}
|
|
96
|
-
function renderConfigList(entries) {
|
|
97
|
-
|
|
100
|
+
function renderConfigList(entries, truncated) {
|
|
101
|
+
const lines = entries.map((entry) => `${entry.key}=${entry.value}`);
|
|
102
|
+
if (truncated) {
|
|
103
|
+
lines.push("(advanced keys hidden — dg config list --all)");
|
|
104
|
+
}
|
|
105
|
+
return `${lines.join("\n")}\n`;
|
|
98
106
|
}
|
|
99
107
|
function usageError(message) {
|
|
100
108
|
return {
|