@westbayberry/dg 1.3.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +1 -201
  2. package/NOTICE +1 -4
  3. package/README.md +293 -0
  4. package/dist/api/analyze.js +210 -0
  5. package/dist/audit/deep.js +180 -0
  6. package/dist/audit/detectors.js +247 -0
  7. package/dist/audit/events.js +41 -0
  8. package/dist/audit/rules.js +426 -0
  9. package/dist/audit-ui/AuditApp.js +39 -0
  10. package/dist/audit-ui/components/AuditHeader.js +24 -0
  11. package/dist/audit-ui/components/AuditResultsView.js +307 -0
  12. package/dist/audit-ui/components/DeepStatusRow.js +11 -0
  13. package/dist/audit-ui/export.js +85 -0
  14. package/dist/audit-ui/format.js +34 -0
  15. package/dist/audit-ui/launch.js +34 -0
  16. package/dist/auth/device-login.js +271 -0
  17. package/dist/auth/env-token.js +6 -0
  18. package/dist/auth/login-app.js +156 -0
  19. package/dist/auth/store.js +147 -0
  20. package/dist/bin/dg.js +71 -0
  21. package/dist/commands/audit.js +357 -0
  22. package/dist/commands/completion.js +116 -0
  23. package/dist/commands/config.js +99 -0
  24. package/dist/commands/doctor.js +39 -0
  25. package/dist/commands/explain.js +100 -0
  26. package/dist/commands/guard-commit.js +158 -0
  27. package/dist/commands/help.js +74 -0
  28. package/dist/commands/licenses.js +435 -0
  29. package/dist/commands/login.js +81 -0
  30. package/dist/commands/logout.js +37 -0
  31. package/dist/commands/router.js +98 -0
  32. package/dist/commands/scan.js +18 -0
  33. package/dist/commands/service.js +475 -0
  34. package/dist/commands/setup.js +302 -0
  35. package/dist/commands/status.js +115 -0
  36. package/dist/commands/suggest.js +35 -0
  37. package/dist/commands/types.js +4 -0
  38. package/dist/commands/unavailable.js +11 -0
  39. package/dist/commands/uninstall.js +111 -0
  40. package/dist/commands/update.js +210 -0
  41. package/dist/commands/verify.js +151 -0
  42. package/dist/commands/version.js +22 -0
  43. package/dist/commands/wrap.js +55 -0
  44. package/dist/config/settings.js +302 -0
  45. package/dist/install-ui/LiveInstall.js +24 -0
  46. package/dist/install-ui/block-render.js +83 -0
  47. package/dist/install-ui/live-install-app.js +48 -0
  48. package/dist/install-ui/prompt.js +24 -0
  49. package/dist/launcher/classify.js +116 -0
  50. package/dist/launcher/env.js +53 -0
  51. package/dist/launcher/live-install.js +50 -0
  52. package/dist/launcher/output-redaction.js +77 -0
  53. package/dist/launcher/preflight-prompt.js +139 -0
  54. package/dist/launcher/resolve-real-binary.js +73 -0
  55. package/dist/launcher/run.js +417 -0
  56. package/dist/policy/evaluate.js +128 -0
  57. package/dist/presentation/mode.js +52 -0
  58. package/dist/presentation/theme.js +29 -0
  59. package/dist/proxy/buffer-budget.js +64 -0
  60. package/dist/proxy/ca.js +126 -0
  61. package/dist/proxy/classify-host.js +26 -0
  62. package/dist/proxy/enforcement.js +102 -0
  63. package/dist/proxy/metadata-map.js +336 -0
  64. package/dist/proxy/server.js +909 -0
  65. package/dist/proxy/upstream-proxy.js +102 -0
  66. package/dist/proxy/worker.js +39 -0
  67. package/dist/publish-set/collect.js +51 -0
  68. package/dist/publish-set/no-exec-shell.js +19 -0
  69. package/dist/publish-set/npm.js +109 -0
  70. package/dist/publish-set/pack.js +36 -0
  71. package/dist/publish-set/pypi.js +59 -0
  72. package/dist/runtime/cli.js +17 -0
  73. package/dist/runtime/first-run.js +60 -0
  74. package/dist/runtime/node-version.js +58 -0
  75. package/dist/runtime/nudges.js +105 -0
  76. package/dist/scan/analyze-worker.js +21 -0
  77. package/dist/scan/collect.js +153 -0
  78. package/dist/scan/command.js +159 -0
  79. package/dist/scan/discovery.js +209 -0
  80. package/dist/scan/render.js +240 -0
  81. package/dist/scan/scanner-report.js +82 -0
  82. package/dist/scan/staged.js +173 -0
  83. package/dist/scan/types.js +1 -0
  84. package/dist/scan-ui/LegacyApp.js +156 -0
  85. package/dist/scan-ui/alt-screen.js +84 -0
  86. package/dist/scan-ui/api-aliases.js +1 -0
  87. package/dist/scan-ui/components/ErrorView.js +23 -0
  88. package/dist/scan-ui/components/InteractiveResultsView.js +1166 -0
  89. package/dist/scan-ui/components/ProgressBar.js +89 -0
  90. package/dist/scan-ui/components/ProjectSelector.js +62 -0
  91. package/dist/scan-ui/components/ScoreHeader.js +20 -0
  92. package/dist/scan-ui/components/SetupBanner.js +13 -0
  93. package/dist/scan-ui/components/Spinner.js +4 -0
  94. package/dist/scan-ui/format-helpers.js +40 -0
  95. package/dist/scan-ui/hooks/useExpandAnimation.js +40 -0
  96. package/dist/scan-ui/hooks/useScan.js +113 -0
  97. package/dist/scan-ui/hooks/useTerminalSize.js +24 -0
  98. package/dist/scan-ui/launch.js +27 -0
  99. package/dist/scan-ui/logo.js +91 -0
  100. package/dist/scan-ui/shims.js +30 -0
  101. package/dist/security/sanitize.js +28 -0
  102. package/dist/service/state.js +837 -0
  103. package/dist/service/trust-store.js +234 -0
  104. package/dist/service/worker.js +88 -0
  105. package/dist/setup/git-hook.js +244 -0
  106. package/dist/setup/optional-support.js +58 -0
  107. package/dist/setup/plan.js +899 -0
  108. package/dist/state/cleanup-registry.js +60 -0
  109. package/dist/state/index.js +5 -0
  110. package/dist/state/locks.js +161 -0
  111. package/dist/state/paths.js +24 -0
  112. package/dist/state/sessions.js +170 -0
  113. package/dist/state/store.js +50 -0
  114. package/dist/telemetry/events.js +40 -0
  115. package/dist/util/git.js +20 -0
  116. package/dist/util/tty-prompt.js +43 -0
  117. package/dist/verify/local.js +400 -0
  118. package/dist/verify/package-check.js +240 -0
  119. package/dist/verify/preflight.js +698 -0
  120. package/dist/verify/render.js +184 -0
  121. package/dist/verify/types.js +1 -0
  122. package/package.json +33 -50
  123. package/dist/index.mjs +0 -54141
  124. package/dist/postinstall.mjs +0 -731
  125. package/dist/python-hook/dg_pip_hook.pth +0 -1
  126. package/dist/python-hook/dg_pip_hook.py +0 -130
@@ -0,0 +1,102 @@
1
+ import { connect } from "node:net";
2
+ import { redactSecrets } from "../launcher/output-redaction.js";
3
+ export function selectUpstreamProxy(target, env) {
4
+ const explicit = env.DG_UPSTREAM_PROXY;
5
+ const inherited = target.protocol === "https:"
6
+ ? env.HTTPS_PROXY ?? env.https_proxy ?? env.ALL_PROXY ?? env.all_proxy
7
+ : env.HTTP_PROXY ?? env.http_proxy ?? env.ALL_PROXY ?? env.all_proxy;
8
+ const raw = explicit ?? inherited;
9
+ if (!raw || (!explicit && matchesNoProxy(target.hostname, target.port, env.NO_PROXY ?? env.no_proxy))) {
10
+ return null;
11
+ }
12
+ const url = new URL(raw);
13
+ if (url.protocol !== "http:") {
14
+ throw new Error("only HTTP upstream proxies are supported for per-invocation proxy chaining");
15
+ }
16
+ const authorizationHeader = url.username || url.password
17
+ ? `Basic ${Buffer.from(`${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`).toString("base64")}`
18
+ : undefined;
19
+ return {
20
+ url,
21
+ ...(authorizationHeader ? { authorizationHeader } : {}),
22
+ redactedUrl: redactSecrets(url.toString())
23
+ };
24
+ }
25
+ export function connectViaUpstreamProxy(target, upstream, extraHeaders = []) {
26
+ return new Promise((resolve, reject) => {
27
+ const socket = connect(Number(upstream.url.port || "80"), upstream.url.hostname);
28
+ const chunks = [];
29
+ const fail = (error) => {
30
+ socket.destroy();
31
+ reject(error);
32
+ };
33
+ socket.once("error", fail);
34
+ socket.once("connect", () => {
35
+ const authority = authorityFor(target);
36
+ const headers = [
37
+ `CONNECT ${authority} HTTP/1.1`,
38
+ `Host: ${authority}`,
39
+ "Proxy-Connection: keep-alive",
40
+ ...(upstream.authorizationHeader ? [`Proxy-Authorization: ${upstream.authorizationHeader}`] : []),
41
+ ...extraHeaders,
42
+ "",
43
+ ""
44
+ ];
45
+ socket.write(headers.join("\r\n"));
46
+ });
47
+ socket.on("data", function onData(chunk) {
48
+ chunks.push(chunk);
49
+ const buffered = Buffer.concat(chunks);
50
+ if (buffered.length > 64 * 1024) {
51
+ socket.off("data", onData);
52
+ socket.destroy();
53
+ reject(new Error("upstream proxy CONNECT response exceeded the 64KiB header limit"));
54
+ return;
55
+ }
56
+ const headerEnd = buffered.indexOf("\r\n\r\n");
57
+ if (headerEnd === -1) {
58
+ return;
59
+ }
60
+ socket.off("data", onData);
61
+ socket.off("error", fail);
62
+ const head = buffered.subarray(headerEnd + 4);
63
+ const statusLine = buffered.subarray(0, headerEnd).toString("latin1").split("\r\n")[0] ?? "";
64
+ if (!/^HTTP\/1\.[01] 2\d\d\b/.test(statusLine)) {
65
+ socket.destroy();
66
+ reject(new Error(`upstream proxy CONNECT failed: ${statusLine}`));
67
+ return;
68
+ }
69
+ if (head.length > 0) {
70
+ socket.unshift(head);
71
+ }
72
+ resolve(socket);
73
+ });
74
+ });
75
+ }
76
+ export function authorityFor(target) {
77
+ const port = target.port || (target.protocol === "https:" ? "443" : "80");
78
+ return `${target.hostname}:${port}`;
79
+ }
80
+ function matchesNoProxy(host, port, rawNoProxy) {
81
+ if (!rawNoProxy) {
82
+ return false;
83
+ }
84
+ const normalizedHost = host.toLowerCase();
85
+ const hostPort = `${normalizedHost}:${port}`;
86
+ for (const rawEntry of rawNoProxy.split(",")) {
87
+ const entry = rawEntry.trim().toLowerCase();
88
+ if (!entry) {
89
+ continue;
90
+ }
91
+ if (entry === "*") {
92
+ return true;
93
+ }
94
+ if (entry === normalizedHost || entry === hostPort) {
95
+ return true;
96
+ }
97
+ if (entry.startsWith(".") && normalizedHost.endsWith(entry)) {
98
+ return true;
99
+ }
100
+ }
101
+ return false;
102
+ }
@@ -0,0 +1,39 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { startProductionHttpProxy } from "./server.js";
3
+ import { parseForceOverrideRequest } from "./enforcement.js";
4
+ const sessionPath = process.argv[2];
5
+ const apiBaseUrl = process.argv[3];
6
+ const classificationJson = process.env.DG_PROXY_CLASSIFICATION;
7
+ if (!sessionPath || !apiBaseUrl || !classificationJson) {
8
+ process.stderr.write("dg proxy worker missing startup arguments\n");
9
+ process.exit(1);
10
+ }
11
+ const session = JSON.parse(readFileSync(sessionPath, "utf8"));
12
+ const classification = JSON.parse(classificationJson);
13
+ const forceOverride = parseForceOverrideRequest(process.env.DG_FORCE_OVERRIDE_REQUEST);
14
+ let handle = null;
15
+ let closed = false;
16
+ async function close() {
17
+ if (closed) {
18
+ return;
19
+ }
20
+ closed = true;
21
+ await handle?.close();
22
+ }
23
+ process.stdin.on("end", () => {
24
+ close().finally(() => process.exit(0));
25
+ });
26
+ process.on("SIGTERM", () => {
27
+ close().finally(() => process.exit(0));
28
+ });
29
+ process.on("SIGINT", () => {
30
+ close().finally(() => process.exit(0));
31
+ });
32
+ handle = await startProductionHttpProxy({
33
+ session,
34
+ apiBaseUrl,
35
+ classification,
36
+ env: process.env,
37
+ ...(forceOverride ? { forceOverride } : {})
38
+ });
39
+ process.stdout.write(`ready ${handle.port}\n`);
@@ -0,0 +1,51 @@
1
+ import { lstatSync, readFileSync, readlinkSync, realpathSync } from "node:fs";
2
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ export function buildAuditFile(root, relPath) {
4
+ const absolute = resolve(root, relPath);
5
+ let stat;
6
+ try {
7
+ stat = lstatSync(absolute);
8
+ }
9
+ catch {
10
+ return null;
11
+ }
12
+ const isSymlink = stat.isSymbolicLink();
13
+ let symlinkEscapes = false;
14
+ if (isSymlink) {
15
+ try {
16
+ const target = readlinkSync(absolute);
17
+ const resolved = isAbsolute(target) ? target : resolve(root, relPath, "..", target);
18
+ const real = realpathSync(resolved);
19
+ const rootReal = realpathSync(root);
20
+ symlinkEscapes = real !== rootReal && !real.startsWith(rootReal + sep);
21
+ }
22
+ catch {
23
+ symlinkEscapes = true;
24
+ }
25
+ }
26
+ if (!isSymlink && !stat.isFile()) {
27
+ return null;
28
+ }
29
+ return {
30
+ path: toDisplay(relPath),
31
+ size: stat.size,
32
+ isSymlink,
33
+ symlinkEscapes,
34
+ mode: stat.mode,
35
+ read: () => {
36
+ try {
37
+ return readFileSync(absolute);
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ };
44
+ }
45
+ export function toDisplay(relPath) {
46
+ const normalized = relative(".", relPath) || relPath;
47
+ return normalized.split(sep).join("/");
48
+ }
49
+ export function joinRel(dir, name) {
50
+ return toDisplay(join(dir, name));
51
+ }
@@ -0,0 +1,19 @@
1
+ import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ export function noExecPackEnv(env, platform = process.platform) {
5
+ const dir = mkdtempSync(join(tmpdir(), "dg-noexec-shell-"));
6
+ const windows = platform === "win32";
7
+ const shellPath = join(dir, windows ? "noop.cmd" : "noop.sh");
8
+ if (windows) {
9
+ writeFileSync(shellPath, "@exit /b 0\r\n", "utf8");
10
+ }
11
+ else {
12
+ writeFileSync(shellPath, "#!/bin/sh\nexit 0\n", "utf8");
13
+ chmodSync(shellPath, 0o755);
14
+ }
15
+ return {
16
+ env: { ...env, npm_config_script_shell: shellPath },
17
+ cleanup: () => rmSync(dir, { recursive: true, force: true })
18
+ };
19
+ }
@@ -0,0 +1,109 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { toDisplay } from "./collect.js";
5
+ import { noExecPackEnv } from "./no-exec-shell.js";
6
+ export function npmExecutable(platform = process.platform) {
7
+ return platform === "win32" ? "npm.cmd" : "npm";
8
+ }
9
+ const DEFAULT_EXCLUDED_DIRS = new Set([
10
+ "node_modules",
11
+ ".git",
12
+ ".svn",
13
+ ".hg",
14
+ "CVS",
15
+ ".venv",
16
+ "venv",
17
+ "__pycache__"
18
+ ]);
19
+ const DEFAULT_EXCLUDED_NAMES = new Set([
20
+ ".npmignore",
21
+ ".gitignore",
22
+ ".DS_Store",
23
+ ".npmrc",
24
+ ".lock-wscript",
25
+ "npm-debug.log",
26
+ "package-lock.json"
27
+ ]);
28
+ export function npmPublishSet(root, env = process.env) {
29
+ const hasAllowlist = npmHasAllowlist(root);
30
+ const packed = npmPackList(root, env);
31
+ if (packed) {
32
+ return { relPaths: packed, source: "npm-pack", hasAllowlist };
33
+ }
34
+ return { relPaths: fallbackWalk(root), source: "fallback", hasAllowlist };
35
+ }
36
+ function npmHasAllowlist(root) {
37
+ if (existsSync(join(root, ".npmignore"))) {
38
+ return true;
39
+ }
40
+ try {
41
+ const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
42
+ return Array.isArray(parsed.files) && parsed.files.length > 0;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ function npmPackList(root, env) {
49
+ const shell = noExecPackEnv(env);
50
+ let result;
51
+ try {
52
+ result = spawnSync(npmExecutable(), ["pack", "--dry-run", "--json", "--ignore-scripts"], {
53
+ cwd: root,
54
+ env: shell.env,
55
+ encoding: "utf8",
56
+ timeout: 60_000,
57
+ maxBuffer: 64 * 1024 * 1024
58
+ });
59
+ }
60
+ finally {
61
+ shell.cleanup();
62
+ }
63
+ if (result.status !== 0 || !result.stdout) {
64
+ return null;
65
+ }
66
+ try {
67
+ const parsed = JSON.parse(result.stdout);
68
+ const entry = parsed[0];
69
+ if (!entry || !Array.isArray(entry.files)) {
70
+ return null;
71
+ }
72
+ const paths = entry.files.map((file) => file.path).filter((path) => typeof path === "string");
73
+ return paths.length > 0 ? paths.map(toDisplay) : null;
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function fallbackWalk(root) {
80
+ const out = [];
81
+ walk(root, "", 0, out);
82
+ return out.sort((left, right) => left.localeCompare(right));
83
+ function walk(absolute, rel, depth, acc) {
84
+ if (depth > 24) {
85
+ return;
86
+ }
87
+ let entries;
88
+ try {
89
+ entries = readdirSync(absolute, { withFileTypes: true });
90
+ }
91
+ catch {
92
+ return;
93
+ }
94
+ for (const entry of entries) {
95
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
96
+ if (entry.isDirectory()) {
97
+ if (!DEFAULT_EXCLUDED_DIRS.has(entry.name)) {
98
+ walk(join(absolute, entry.name), childRel, depth + 1, acc);
99
+ }
100
+ continue;
101
+ }
102
+ if (entry.isFile() || entry.isSymbolicLink()) {
103
+ if (!DEFAULT_EXCLUDED_NAMES.has(entry.name)) {
104
+ acc.push(toDisplay(childRel));
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,36 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { npmExecutable } from "./npm.js";
7
+ import { noExecPackEnv } from "./no-exec-shell.js";
8
+ export function packNpmArtifact(root, env = process.env) {
9
+ const dest = mkdtempSync(join(tmpdir(), "dg-audit-pack-"));
10
+ const shell = noExecPackEnv(env);
11
+ try {
12
+ const result = spawnSync(npmExecutable(), ["pack", "--ignore-scripts", "--pack-destination", dest], {
13
+ cwd: root,
14
+ env: shell.env,
15
+ encoding: "utf8",
16
+ timeout: 120_000,
17
+ maxBuffer: 8 * 1024 * 1024
18
+ });
19
+ if (result.status !== 0) {
20
+ return { error: result.stderr ? result.stderr.trim().split("\n").pop() ?? "npm pack failed" : "npm pack failed" };
21
+ }
22
+ const tgz = readdirSync(dest).find((name) => name.endsWith(".tgz"));
23
+ if (!tgz) {
24
+ return { error: "npm pack produced no tarball" };
25
+ }
26
+ const bytes = readFileSync(join(dest, tgz));
27
+ return { bytes, sha256: createHash("sha256").update(bytes).digest("hex") };
28
+ }
29
+ catch (error) {
30
+ return { error: error instanceof Error ? error.message : "pack failed" };
31
+ }
32
+ finally {
33
+ shell.cleanup();
34
+ rmSync(dest, { recursive: true, force: true });
35
+ }
36
+ }
@@ -0,0 +1,59 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { toDisplay } from "./collect.js";
4
+ const EXCLUDED_DIRS = new Set([
5
+ ".git",
6
+ ".svn",
7
+ ".hg",
8
+ ".venv",
9
+ "venv",
10
+ "__pycache__",
11
+ "build",
12
+ "dist",
13
+ ".tox",
14
+ ".mypy_cache",
15
+ ".pytest_cache",
16
+ ".eggs"
17
+ ]);
18
+ export function pypiPublishSet(root) {
19
+ const hasAllowlist = pypiHasAllowlist(root);
20
+ const out = [];
21
+ walk(root, "", 0, out);
22
+ return { relPaths: out.sort((left, right) => left.localeCompare(right)), source: "fallback", hasAllowlist };
23
+ function walk(absolute, rel, depth, acc) {
24
+ if (depth > 24) {
25
+ return;
26
+ }
27
+ let entries;
28
+ try {
29
+ entries = readdirSync(absolute, { withFileTypes: true });
30
+ }
31
+ catch {
32
+ return;
33
+ }
34
+ for (const entry of entries) {
35
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
36
+ if (entry.isDirectory()) {
37
+ if (!EXCLUDED_DIRS.has(entry.name) && !entry.name.endsWith(".egg-info")) {
38
+ walk(join(absolute, entry.name), childRel, depth + 1, acc);
39
+ }
40
+ continue;
41
+ }
42
+ if (entry.isFile() || entry.isSymbolicLink()) {
43
+ acc.push(toDisplay(childRel));
44
+ }
45
+ }
46
+ }
47
+ }
48
+ function pypiHasAllowlist(root) {
49
+ if (existsSync(join(root, "MANIFEST.in"))) {
50
+ return true;
51
+ }
52
+ try {
53
+ const pyproject = readFileSync(join(root, "pyproject.toml"), "utf8");
54
+ return /\[tool\.(setuptools|hatch|flit|poetry)[^\]]*\]/u.test(pyproject) && /include|packages|sdist/u.test(pyproject);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { routeCommand } from "../commands/router.js";
3
+ if (process.env.NODE_ENV === "test" && process.env.DG_TEST_RUNTIME_SENTINEL) {
4
+ writeFileSync(process.env.DG_TEST_RUNTIME_SENTINEL, "loaded\n", "utf8");
5
+ }
6
+ export function runCli(args) {
7
+ return routeCommand(args);
8
+ }
9
+ export function writeCliResult(result) {
10
+ if (result.stdout) {
11
+ process.stdout.write(result.stdout);
12
+ }
13
+ if (result.stderr) {
14
+ process.stderr.write(result.stderr);
15
+ }
16
+ process.exitCode = result.exitCode;
17
+ }
@@ -0,0 +1,60 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { isCiEnv } from "../presentation/mode.js";
4
+ import { createTheme } from "../presentation/theme.js";
5
+ import { resolveDgPaths } from "../state/index.js";
6
+ const SKIP_COMMANDS = new Set([
7
+ "help",
8
+ "--help",
9
+ "-h",
10
+ "--help-all",
11
+ "version",
12
+ "--version",
13
+ "-v",
14
+ "completion",
15
+ "login",
16
+ "logout",
17
+ "update",
18
+ "upgrade",
19
+ "uninstall"
20
+ ]);
21
+ export function firstRunMarkerPath(env = process.env) {
22
+ return join(resolveDgPaths(env).stateDir, "first-run-shown");
23
+ }
24
+ export function maybeShowFirstRun(args, options = {}) {
25
+ const env = options.env ?? process.env;
26
+ const stderr = options.stderr ?? process.stderr;
27
+ const command = args[0] ?? "";
28
+ if (!stderr.isTTY || isCiEnv(env)) {
29
+ return false;
30
+ }
31
+ if (SKIP_COMMANDS.has(command) || args.some((arg) => arg === "--json" || arg === "--sarif" || arg === "--quiet")) {
32
+ return false;
33
+ }
34
+ const marker = firstRunMarkerPath(env);
35
+ if (existsSync(marker)) {
36
+ return false;
37
+ }
38
+ const theme = createTheme(true);
39
+ const command_ = (text) => theme.paint("accent", text);
40
+ const lines = [
41
+ "",
42
+ ` ${theme.paint("pass", "✓")} Dependency Guardian is ready.`,
43
+ "",
44
+ ` ${theme.paint("muted", "dg npm install / dg pip install scan packages before they run.")}`,
45
+ ` ${theme.paint("muted", "Run")} ${command_("dg setup")} ${theme.paint("muted", "once to protect bare npm/pip installs too.")}`,
46
+ "",
47
+ ` Next: ${command_("dg login")} to connect your account.`,
48
+ "",
49
+ ""
50
+ ];
51
+ stderr.write(lines.join("\n"));
52
+ try {
53
+ mkdirSync(dirname(marker), { recursive: true, mode: 0o700 });
54
+ writeFileSync(marker, `${new Date().toISOString()}\n`, { encoding: "utf8", mode: 0o600 });
55
+ }
56
+ catch {
57
+ return true;
58
+ }
59
+ return true;
60
+ }
@@ -0,0 +1,58 @@
1
+ const MINIMUM_NODE = Object.freeze({
2
+ major: 22,
3
+ minor: 14,
4
+ patch: 0
5
+ });
6
+ export function parseNodeVersion(version) {
7
+ const normalized = version.trim().replace(/^v/, "");
8
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-.+)?$/.exec(normalized);
9
+ if (!match) {
10
+ return null;
11
+ }
12
+ return {
13
+ major: Number(match[1]),
14
+ minor: Number(match[2]),
15
+ patch: Number(match[3])
16
+ };
17
+ }
18
+ export function isSupportedNode(version) {
19
+ const parsed = parseNodeVersion(version);
20
+ if (!parsed) {
21
+ return false;
22
+ }
23
+ if (parsed.major > MINIMUM_NODE.major) {
24
+ return true;
25
+ }
26
+ if (parsed.major < MINIMUM_NODE.major) {
27
+ return false;
28
+ }
29
+ if (parsed.minor > MINIMUM_NODE.minor) {
30
+ return true;
31
+ }
32
+ if (parsed.minor < MINIMUM_NODE.minor) {
33
+ return false;
34
+ }
35
+ return parsed.patch >= MINIMUM_NODE.patch;
36
+ }
37
+ export function assertSupportedNode(version) {
38
+ if (isSupportedNode(version)) {
39
+ return;
40
+ }
41
+ throw new Error(`dg requires Node.js >=22.14.0. Current runtime is ${version}. Upgrade Node before running dg.`);
42
+ }
43
+ export function currentNodeVersion() {
44
+ if (process.env.NODE_ENV === "test" && process.env.DG_TEST_NODE_VERSION) {
45
+ return process.env.DG_TEST_NODE_VERSION;
46
+ }
47
+ return process.version;
48
+ }
49
+ export function assertCurrentNode() {
50
+ try {
51
+ assertSupportedNode(currentNodeVersion());
52
+ }
53
+ catch (error) {
54
+ const message = error instanceof Error ? error.message : String(error);
55
+ process.stderr.write(`${message}\n`);
56
+ process.exit(1);
57
+ }
58
+ }
@@ -0,0 +1,105 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { authStatus } from "../auth/store.js";
4
+ import { compareVersions, readLatestVersion } from "../commands/update.js";
5
+ import { dgVersion } from "../commands/version.js";
6
+ import { isCiEnv } from "../presentation/mode.js";
7
+ import { createTheme } from "../presentation/theme.js";
8
+ import { resolveDgPaths } from "../state/index.js";
9
+ const UPDATE_THROTTLE_MS = 24 * 60 * 60 * 1000;
10
+ const LOGIN_THROTTLE_MS = 7 * 24 * 60 * 60 * 1000;
11
+ const LATEST_LOOKUP_TIMEOUT_MS = 1500;
12
+ const SKIP_COMMANDS = new Set([
13
+ "help",
14
+ "--help",
15
+ "-h",
16
+ "--help-all",
17
+ "version",
18
+ "--version",
19
+ "-v",
20
+ "completion",
21
+ "login",
22
+ "logout",
23
+ "update",
24
+ "upgrade",
25
+ "uninstall",
26
+ "scan",
27
+ "licenses"
28
+ ]);
29
+ export function nudgeStatePath(env = process.env) {
30
+ return join(resolveDgPaths(env).stateDir, "nudges.json");
31
+ }
32
+ export function maybeShowNudges(args, options = {}) {
33
+ const env = options.env ?? process.env;
34
+ const stderr = options.stderr ?? process.stderr;
35
+ const now = options.now ?? new Date();
36
+ const command = args[0] ?? "";
37
+ if (!stderr.isTTY || isCiEnv(env)) {
38
+ return;
39
+ }
40
+ if (SKIP_COMMANDS.has(command) || args.some((arg) => arg === "--json" || arg === "--sarif" || arg === "--quiet")) {
41
+ return;
42
+ }
43
+ const statePath = nudgeStatePath(env);
44
+ const state = readState(statePath);
45
+ const theme = createTheme(true);
46
+ const lines = [];
47
+ let stateChanged = false;
48
+ if (due(state.updateCheckedAt, UPDATE_THROTTLE_MS, now)) {
49
+ state.updateCheckedAt = now.toISOString();
50
+ stateChanged = true;
51
+ const latest = readLatestVersion(LATEST_LOOKUP_TIMEOUT_MS);
52
+ if (latest) {
53
+ state.updateLatest = latest;
54
+ if (compareVersions(latest, dgVersion()) > 0) {
55
+ lines.push(`${theme.paint("warn", "⚠")} ${theme.paint("muted", `Update available: ${dgVersion()} → ${latest}. Run`)} ${theme.paint("accent", "dg update")}${theme.paint("muted", ".")}`);
56
+ }
57
+ }
58
+ }
59
+ if (due(state.loginNudgedAt, LOGIN_THROTTLE_MS, now) && !isAuthenticated(env)) {
60
+ state.loginNudgedAt = now.toISOString();
61
+ stateChanged = true;
62
+ lines.push(`${theme.paint("muted", "Run")} ${theme.paint("accent", "dg login")} ${theme.paint("muted", "to connect your account.")}`);
63
+ }
64
+ if (lines.length > 0) {
65
+ stderr.write(`\n${lines.join("\n")}\n`);
66
+ }
67
+ if (stateChanged) {
68
+ writeState(statePath, state);
69
+ }
70
+ }
71
+ function isAuthenticated(env) {
72
+ try {
73
+ return authStatus(env).authenticated;
74
+ }
75
+ catch {
76
+ return true;
77
+ }
78
+ }
79
+ function due(last, throttleMs, now) {
80
+ if (!last) {
81
+ return true;
82
+ }
83
+ const parsed = Date.parse(last);
84
+ return !Number.isFinite(parsed) || now.getTime() - parsed >= throttleMs;
85
+ }
86
+ function readState(path) {
87
+ try {
88
+ if (!existsSync(path)) {
89
+ return {};
90
+ }
91
+ return JSON.parse(readFileSync(path, "utf8"));
92
+ }
93
+ catch {
94
+ return {};
95
+ }
96
+ }
97
+ function writeState(path, state) {
98
+ try {
99
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
100
+ writeFileSync(path, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
101
+ }
102
+ catch {
103
+ return;
104
+ }
105
+ }
@@ -0,0 +1,21 @@
1
+ import { analyzePackages, mergeAnalyzeResponses } from "../api/analyze.js";
2
+ async function main() {
3
+ const raw = process.argv[2];
4
+ if (!raw) {
5
+ process.stderr.write("analyze-worker: missing input payload\n");
6
+ process.exit(2);
7
+ }
8
+ const groups = JSON.parse(raw);
9
+ const responses = [];
10
+ for (const group of groups) {
11
+ if (group.packages.length === 0) {
12
+ continue;
13
+ }
14
+ responses.push(await analyzePackages(group.packages, { ecosystem: group.ecosystem }));
15
+ }
16
+ process.stdout.write(JSON.stringify(mergeAnalyzeResponses(responses)));
17
+ }
18
+ main().catch((error) => {
19
+ process.stderr.write(`analyze-worker: ${error instanceof Error ? error.message : "unknown error"}\n`);
20
+ process.exit(1);
21
+ });