@westbayberry/dg 1.0.6 → 1.0.8

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 (3) hide show
  1. package/README.md +59 -44
  2. package/dist/index.mjs +357 -298
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @westbayberry/dg
2
2
 
3
- Supply chain security scanner for npm dependencies. Detects malicious packages, typosquatting, dependency confusion, and 20+ attack patterns before they reach production.
3
+ Supply chain security scanner for npm and Python dependencies. Detects malicious packages, typosquatting, dependency confusion, and 20+ attack patterns before they reach production.
4
4
 
5
5
  ## Install
6
6
 
@@ -8,44 +8,42 @@ Supply chain security scanner for npm dependencies. Detects malicious packages,
8
8
  npm install -g @westbayberry/dg
9
9
  ```
10
10
 
11
- Or run without installing:
12
-
13
- ```bash
14
- npx @westbayberry/dg scan
15
- ```
16
-
17
11
  ## Quick Start
18
12
 
19
- 1. Get your API key at [westbayberry.com/dashboard](https://westbayberry.com/dashboard)
20
- 2. Run a scan:
21
-
22
13
  ```bash
23
- export DG_API_KEY=dg_live_your_key_here
14
+ dg login
24
15
  dg scan
25
16
  ```
26
17
 
27
- That's it. The CLI auto-detects changed packages by diffing your lockfile against the base branch.
18
+ The CLI auto-discovers projects in your directory tree npm lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml) and Python dependency files (requirements.txt, Pipfile.lock, poetry.lock). If multiple projects are found, you pick which ones to scan.
28
19
 
29
- ## Usage
20
+ ## Commands
30
21
 
31
22
  ```
32
- dg scan [options]
23
+ dg scan [options] Scan dependencies (auto-discovers npm + Python projects)
24
+ dg npm install <pkg> Scan packages before installing
25
+ dg login Authenticate with your WestBayBerry account
26
+ dg logout Remove saved credentials
27
+ dg hook install Install git pre-commit hook to scan lockfile changes
28
+ dg hook uninstall Remove the pre-commit hook
29
+ dg update Check for and install the latest version
30
+ dg wrap Show instructions to alias npm to dg
33
31
  ```
34
32
 
35
- ### Options
36
-
37
- | Flag | Env Var | Default | Description |
38
- |------|---------|---------|-------------|
39
- | `--api-key <key>` | `DG_API_KEY` | *required* | Your API key |
40
- | `--mode <mode>` | `DG_MODE` | `warn` | `block` / `warn` / `off` |
41
- | `--block-threshold <n>` | | `70` | Score threshold for blocking |
42
- | `--warn-threshold <n>` | | `60` | Score threshold for warnings |
43
- | `--max-packages <n>` | | `200` | Max packages per scan |
44
- | `--allowlist <pkgs>` | `DG_ALLOWLIST` | | Comma-separated packages to skip |
45
- | `--json` | | | Output JSON for CI parsing |
46
- | `--scan-all` | | | Scan all packages, not just changed |
47
- | `--base-lockfile <path>` | | | Explicit base lockfile for diff |
48
- | `--api-url <url>` | `DG_API_URL` | `https://api.westbayberry.com` | API endpoint |
33
+ ### Scan Options
34
+
35
+ | Flag | Default | Description |
36
+ |------|---------|-------------|
37
+ | `--mode <mode>` | `warn` | `block` / `warn` / `off` |
38
+ | `--block-threshold <n>` | `70` | Score threshold for blocking |
39
+ | `--warn-threshold <n>` | `60` | Score threshold for warnings |
40
+ | `--max-packages <n>` | `200` | Max packages per scan |
41
+ | `--allowlist <pkgs>` | | Comma-separated packages to skip |
42
+ | `--json` | | Output JSON for CI parsing |
43
+ | `--scan-all` | | Scan all packages, not just changed |
44
+ | `--base-lockfile <path>` | | Explicit base lockfile for diff |
45
+ | `--workspace <dir>` | | Scan a specific workspace subdirectory |
46
+ | `--debug` | | Show diagnostic output |
49
47
 
50
48
  ### Exit Codes
51
49
 
@@ -54,37 +52,54 @@ dg scan [options]
54
52
  | `0` | Pass | Continue |
55
53
  | `1` | Warning | Advisory — review recommended |
56
54
  | `2` | Block | Fail the pipeline |
55
+ | `3` | Error | Internal error |
56
+
57
+ ## CI Setup
57
58
 
58
- ## CI Examples
59
+ Authenticate first, then add the scan to your pipeline:
59
60
 
60
- ### GitHub Actions CI
61
+ ### GitHub Actions
61
62
 
62
63
  ```yaml
63
64
  - name: Scan dependencies
64
- run: npx @westbayberry/dg scan --mode block
65
- env:
66
- DG_API_KEY: ${{ secrets.DG_API_KEY }}
65
+ run: |
66
+ npx @westbayberry/dg login
67
+ npx @westbayberry/dg scan --mode block --json
67
68
  ```
68
69
 
69
- ### GitLab CI
70
+ ### Any CI
70
71
 
71
- ```yaml
72
- dependency-scan:
73
- script:
74
- - npx @westbayberry/dg scan --mode block
75
- variables:
76
- DG_API_KEY: $DG_API_KEY
72
+ ```bash
73
+ npx @westbayberry/dg login
74
+ npx @westbayberry/dg scan --mode block --json
77
75
  ```
78
76
 
79
- ### Any CI
77
+ ## Git Hook
78
+
79
+ Block commits that introduce risky dependencies:
80
80
 
81
81
  ```bash
82
- export DG_API_KEY="$DG_API_KEY"
83
- npx @westbayberry/dg scan --mode block --json
82
+ dg hook install
83
+ ```
84
+
85
+ This installs a pre-commit hook that runs `dg scan --mode block` whenever a lockfile change is staged. Use `dg hook uninstall` to remove it.
86
+
87
+ ## npm Wrapper
88
+
89
+ Scan packages before installing:
90
+
91
+ ```bash
92
+ dg npm install express lodash
93
+ ```
94
+
95
+ Use `--dg-force` to bypass a block. Or alias npm globally:
96
+
97
+ ```bash
98
+ echo 'alias npm="dg npm"' >> ~/.zshrc
84
99
  ```
85
100
 
86
101
  ## Links
87
102
 
88
- - [Dashboard & API Keys](https://westbayberry.com/dashboard)
103
+ - [Dashboard](https://westbayberry.com/dashboard)
89
104
  - [Documentation](https://westbayberry.com/docs)
90
105
  - [Pricing](https://westbayberry.com/pricing)
package/dist/index.mjs CHANGED
@@ -39,6 +39,194 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
39
39
  mod
40
40
  ));
41
41
 
42
+ // src/config.ts
43
+ var config_exports = {};
44
+ __export(config_exports, {
45
+ USAGE: () => USAGE,
46
+ getVersion: () => getVersion,
47
+ parseConfig: () => parseConfig
48
+ });
49
+ import { parseArgs } from "node:util";
50
+ import { readFileSync, existsSync } from "node:fs";
51
+ import { join } from "node:path";
52
+ import { homedir } from "node:os";
53
+ function loadDgrc() {
54
+ const candidates = [
55
+ join(process.cwd(), ".dgrc.json"),
56
+ join(homedir(), ".dgrc.json")
57
+ ];
58
+ for (const filepath of candidates) {
59
+ if (existsSync(filepath)) {
60
+ try {
61
+ return JSON.parse(readFileSync(filepath, "utf-8"));
62
+ } catch {
63
+ process.stderr.write(`Warning: Failed to parse ${filepath}, ignoring.
64
+ `);
65
+ }
66
+ }
67
+ }
68
+ return {};
69
+ }
70
+ function getVersion() {
71
+ try {
72
+ const pkg = JSON.parse(
73
+ readFileSync(join(__dirname, "..", "package.json"), "utf-8")
74
+ );
75
+ return pkg.version ?? "1.0.0";
76
+ } catch {
77
+ return "1.0.0";
78
+ }
79
+ }
80
+ function parseConfig(argv) {
81
+ const { values, positionals } = parseArgs({
82
+ args: argv.slice(2),
83
+ options: {
84
+ "api-url": { type: "string" },
85
+ mode: { type: "string" },
86
+ "block-threshold": { type: "string" },
87
+ "warn-threshold": { type: "string" },
88
+ "max-packages": { type: "string" },
89
+ allowlist: { type: "string" },
90
+ json: { type: "boolean", default: false },
91
+ "scan-all": { type: "boolean", default: false },
92
+ "base-lockfile": { type: "string" },
93
+ workspace: { type: "string", short: "w" },
94
+ debug: { type: "boolean", default: false },
95
+ "no-config": { type: "boolean", default: false },
96
+ help: { type: "boolean", default: false },
97
+ version: { type: "boolean", default: false }
98
+ },
99
+ allowPositionals: true,
100
+ strict: false
101
+ });
102
+ if (values.help) {
103
+ process.stdout.write(USAGE);
104
+ process.exit(0);
105
+ }
106
+ if (values.version) {
107
+ process.stdout.write(`dependency-guardian v${getVersion()}
108
+ `);
109
+ process.exit(0);
110
+ }
111
+ const command = positionals[0] ?? "scan";
112
+ const noConfig = values["no-config"];
113
+ const dgrc = noConfig ? {} : loadDgrc();
114
+ const apiKey = dgrc.apiKey ?? "";
115
+ if (!apiKey) {
116
+ process.stderr.write(
117
+ "Error: Not logged in. Run `dg login` to authenticate.\n"
118
+ );
119
+ process.exit(1);
120
+ }
121
+ const modeRaw = values.mode ?? process.env.DG_MODE ?? dgrc.mode ?? "warn";
122
+ if (!["block", "warn", "off"].includes(modeRaw)) {
123
+ process.stderr.write(
124
+ `Error: Invalid mode "${modeRaw}". Must be block, warn, or off.
125
+ `
126
+ );
127
+ process.exit(1);
128
+ }
129
+ const allowlistRaw = values.allowlist ?? process.env.DG_ALLOWLIST ?? "";
130
+ const blockThreshold = Number(values["block-threshold"] ?? dgrc.blockThreshold ?? "70");
131
+ const warnThreshold = Number(values["warn-threshold"] ?? dgrc.warnThreshold ?? "60");
132
+ const maxPackages = Number(values["max-packages"] ?? dgrc.maxPackages ?? "200");
133
+ const debug = values.debug || process.env.DG_DEBUG === "1";
134
+ if (isNaN(blockThreshold) || blockThreshold < 0 || blockThreshold > 100) {
135
+ process.stderr.write("Error: --block-threshold must be a number between 0 and 100\n");
136
+ process.exit(1);
137
+ }
138
+ if (isNaN(warnThreshold) || warnThreshold < 0 || warnThreshold > 100) {
139
+ process.stderr.write("Error: --warn-threshold must be a number between 0 and 100\n");
140
+ process.exit(1);
141
+ }
142
+ if (isNaN(maxPackages) || maxPackages < 1 || maxPackages > 1e4) {
143
+ process.stderr.write("Error: --max-packages must be a number between 1 and 10000\n");
144
+ process.exit(1);
145
+ }
146
+ return {
147
+ apiKey,
148
+ apiUrl: values["api-url"] ?? process.env.DG_API_URL ?? dgrc.apiUrl ?? "https://api.westbayberry.com",
149
+ mode: modeRaw,
150
+ blockThreshold,
151
+ warnThreshold,
152
+ maxPackages,
153
+ allowlist: allowlistRaw ? allowlistRaw.split(",").map((s) => s.trim()).filter(Boolean) : dgrc.allowlist ?? [],
154
+ json: values.json,
155
+ scanAll: values["scan-all"],
156
+ baseLockfile: values["base-lockfile"] ?? null,
157
+ workspace: values.workspace ?? process.env.DG_WORKSPACE ?? null,
158
+ command,
159
+ debug
160
+ };
161
+ }
162
+ var USAGE;
163
+ var init_config = __esm({
164
+ "src/config.ts"() {
165
+ "use strict";
166
+ USAGE = `
167
+ Dependency Guardian \u2014 Supply chain security scanner
168
+
169
+ Usage:
170
+ dependency-guardian scan [options]
171
+ dg scan [options]
172
+ dg npm install <pkg> [npm-flags]
173
+ dg wrap
174
+
175
+ Commands:
176
+ scan Scan dependencies (auto-discovers npm + Python projects)
177
+ npm Wrap npm commands \u2014 scans packages before installing
178
+ hook install Install git pre-commit hook to scan lockfile changes
179
+ hook uninstall Remove the pre-commit hook
180
+ update Check for and install the latest version
181
+ login Authenticate with your WestBayBerry account
182
+ logout Remove saved credentials
183
+ wrap Show instructions to alias npm to dg
184
+
185
+ Options:
186
+ --api-url <url> API base URL (default: https://api.westbayberry.com)
187
+ --mode <mode> block | warn | off (default: warn)
188
+ --block-threshold <n> Score threshold for blocking (default: 70)
189
+ --warn-threshold <n> Score threshold for warnings (default: 60)
190
+ --max-packages <n> Max packages per scan (default: 200)
191
+ --allowlist <pkgs> Comma-separated package names to skip
192
+ --json Output JSON for CI parsing
193
+ --scan-all Scan all packages, not just changed
194
+ --base-lockfile <path> Path to base lockfile for explicit diff
195
+ --workspace <dir> Scan a specific workspace subdirectory
196
+ --debug Show diagnostic output (discovery, batches, timing)
197
+ --no-config Skip loading .dgrc.json config file
198
+ --help Show this help message
199
+ --version Show version number
200
+
201
+ Config File:
202
+ Place a .dgrc.json in your project root or home directory.
203
+ Precedence: CLI flags > env vars > .dgrc.json > defaults
204
+
205
+ Environment Variables:
206
+ DG_API_URL API base URL
207
+ DG_MODE Mode (block/warn/off)
208
+ DG_ALLOWLIST Comma-separated allowlist
209
+ DG_DEBUG Enable debug output (set to 1)
210
+ DG_WORKSPACE Workspace subdirectory to scan
211
+
212
+ Exit Codes:
213
+ 0 pass \u2014 No risks detected
214
+ 1 warn \u2014 Risks detected (advisory)
215
+ 2 block \u2014 High-risk packages detected
216
+ 3 error \u2014 Internal error (API failure, config error)
217
+
218
+ Examples:
219
+ dg scan
220
+ dg scan --json
221
+ dg scan --scan-all --mode block
222
+ dg scan --base-lockfile ./main-lockfile.json
223
+ dg npm install express lodash
224
+ dg npm install @scope/pkg@^2.0.0
225
+ dg npm install risky-pkg --dg-force
226
+ `.trimStart();
227
+ }
228
+ });
229
+
42
230
  // src/npm-wrapper.ts
43
231
  import { spawn } from "node:child_process";
44
232
  import { readFileSync as readFileSync2, existsSync as existsSync2 } from "node:fs";
@@ -3106,11 +3294,11 @@ var require_react_development = __commonJS({
3106
3294
  }
3107
3295
  return dispatcher.useContext(Context);
3108
3296
  }
3109
- function useState6(initialState) {
3297
+ function useState5(initialState) {
3110
3298
  var dispatcher = resolveDispatcher();
3111
3299
  return dispatcher.useState(initialState);
3112
3300
  }
3113
- function useReducer4(reducer4, initialArg, init) {
3301
+ function useReducer5(reducer4, initialArg, init) {
3114
3302
  var dispatcher = resolveDispatcher();
3115
3303
  return dispatcher.useReducer(reducer4, initialArg, init);
3116
3304
  }
@@ -3130,7 +3318,7 @@ var require_react_development = __commonJS({
3130
3318
  var dispatcher = resolveDispatcher();
3131
3319
  return dispatcher.useLayoutEffect(create2, deps);
3132
3320
  }
3133
- function useCallback4(callback, deps) {
3321
+ function useCallback5(callback, deps) {
3134
3322
  var dispatcher = resolveDispatcher();
3135
3323
  return dispatcher.useCallback(callback, deps);
3136
3324
  }
@@ -3897,7 +4085,7 @@ var require_react_development = __commonJS({
3897
4085
  exports.memo = memo;
3898
4086
  exports.startTransition = startTransition;
3899
4087
  exports.unstable_act = act;
3900
- exports.useCallback = useCallback4;
4088
+ exports.useCallback = useCallback5;
3901
4089
  exports.useContext = useContext7;
3902
4090
  exports.useDebugValue = useDebugValue;
3903
4091
  exports.useDeferredValue = useDeferredValue;
@@ -3907,9 +4095,9 @@ var require_react_development = __commonJS({
3907
4095
  exports.useInsertionEffect = useInsertionEffect;
3908
4096
  exports.useLayoutEffect = useLayoutEffect2;
3909
4097
  exports.useMemo = useMemo4;
3910
- exports.useReducer = useReducer4;
4098
+ exports.useReducer = useReducer5;
3911
4099
  exports.useRef = useRef6;
3912
- exports.useState = useState6;
4100
+ exports.useState = useState5;
3913
4101
  exports.useSyncExternalStore = useSyncExternalStore;
3914
4102
  exports.useTransition = useTransition;
3915
4103
  exports.version = ReactVersion;
@@ -35768,9 +35956,6 @@ var init_build2 = __esm({
35768
35956
  // src/auth.ts
35769
35957
  var auth_exports = {};
35770
35958
  __export(auth_exports, {
35771
- MAX_POLL_ATTEMPTS: () => MAX_POLL_ATTEMPTS,
35772
- POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
35773
- WEB_BASE: () => WEB_BASE,
35774
35959
  clearCredentials: () => clearCredentials,
35775
35960
  createAuthSession: () => createAuthSession,
35776
35961
  getStoredApiKey: () => getStoredApiKey,
@@ -35801,7 +35986,6 @@ async function createAuthSession() {
35801
35986
  const json = await res.json();
35802
35987
  return {
35803
35988
  sessionId: json.session_id,
35804
- userCode: json.user_code,
35805
35989
  verifyUrl: json.verify_url,
35806
35990
  expiresIn: json.expires_in
35807
35991
  };
@@ -35889,24 +36073,25 @@ function openBrowser(url) {
35889
36073
  exec2(cmd, () => {
35890
36074
  });
35891
36075
  }
35892
- var WEB_BASE, POLL_INTERVAL_MS, MAX_POLL_ATTEMPTS, CONFIG_FILE;
36076
+ var WEB_BASE, CONFIG_FILE;
35893
36077
  var init_auth = __esm({
35894
36078
  "src/auth.ts"() {
35895
36079
  "use strict";
35896
36080
  WEB_BASE = "https://westbayberry.com";
35897
- POLL_INTERVAL_MS = 2e3;
35898
- MAX_POLL_ATTEMPTS = 150;
35899
36081
  CONFIG_FILE = ".dgrc.json";
35900
36082
  }
35901
36083
  });
35902
36084
 
35903
36085
  // src/ui/hooks/useLogin.ts
35904
- function reducer(_state, action) {
36086
+ function reducer(state, action) {
35905
36087
  switch (action.type) {
35906
36088
  case "ALREADY_LOGGED_IN":
35907
36089
  return { phase: "already_logged_in", apiKey: action.apiKey };
35908
- case "SESSION_CREATED":
35909
- return { phase: "waiting", userCode: action.userCode, verifyUrl: action.verifyUrl };
36090
+ case "SESSION_READY":
36091
+ return { phase: "ready", verifyUrl: action.verifyUrl };
36092
+ case "BROWSER_OPENED":
36093
+ if (state.phase !== "ready") return state;
36094
+ return { phase: "waiting", verifyUrl: state.verifyUrl };
35910
36095
  case "AUTH_COMPLETE":
35911
36096
  return { phase: "success", email: action.email };
35912
36097
  case "AUTH_EXPIRED":
@@ -35918,10 +36103,11 @@ function reducer(_state, action) {
35918
36103
  function useLogin() {
35919
36104
  const [state, dispatch] = (0, import_react22.useReducer)(reducer, { phase: "creating" });
35920
36105
  const started = (0, import_react22.useRef)(false);
36106
+ const sessionRef = (0, import_react22.useRef)(null);
36107
+ const cancelledRef = (0, import_react22.useRef)(false);
35921
36108
  (0, import_react22.useEffect)(() => {
35922
36109
  if (started.current) return;
35923
36110
  started.current = true;
35924
- let cancelled = false;
35925
36111
  (async () => {
35926
36112
  try {
35927
36113
  const existing = getStoredApiKey();
@@ -35939,16 +36125,30 @@ function useLogin() {
35939
36125
  });
35940
36126
  return;
35941
36127
  }
36128
+ sessionRef.current = session;
36129
+ dispatch({ type: "SESSION_READY", verifyUrl: session.verifyUrl });
36130
+ } catch (err) {
35942
36131
  dispatch({
35943
- type: "SESSION_CREATED",
35944
- userCode: session.userCode,
35945
- verifyUrl: session.verifyUrl
36132
+ type: "ERROR",
36133
+ message: err instanceof Error ? err.message : String(err)
35946
36134
  });
35947
- openBrowser(session.verifyUrl);
35948
- for (let i = 0; i < MAX_POLL_ATTEMPTS2; i++) {
35949
- if (cancelled) return;
35950
- await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS2));
35951
- if (cancelled) return;
36135
+ }
36136
+ })();
36137
+ return () => {
36138
+ cancelledRef.current = true;
36139
+ };
36140
+ }, []);
36141
+ const openAndPoll = (0, import_react22.useCallback)(() => {
36142
+ const session = sessionRef.current;
36143
+ if (!session) return;
36144
+ dispatch({ type: "BROWSER_OPENED" });
36145
+ openBrowser(session.verifyUrl);
36146
+ (async () => {
36147
+ try {
36148
+ for (let i = 0; i < MAX_POLL_ATTEMPTS; i++) {
36149
+ if (cancelledRef.current) return;
36150
+ await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
36151
+ if (cancelledRef.current) return;
35952
36152
  try {
35953
36153
  const result = await pollAuthSession(session.sessionId);
35954
36154
  if (result.status === "complete" && result.apiKey) {
@@ -35971,20 +36171,17 @@ function useLogin() {
35971
36171
  });
35972
36172
  }
35973
36173
  })();
35974
- return () => {
35975
- cancelled = true;
35976
- };
35977
36174
  }, []);
35978
- return state;
36175
+ return { state, openAndPoll };
35979
36176
  }
35980
- var import_react22, POLL_INTERVAL_MS2, MAX_POLL_ATTEMPTS2;
36177
+ var import_react22, POLL_INTERVAL_MS, MAX_POLL_ATTEMPTS;
35981
36178
  var init_useLogin = __esm({
35982
36179
  "src/ui/hooks/useLogin.ts"() {
35983
36180
  "use strict";
35984
36181
  import_react22 = __toESM(require_react());
35985
36182
  init_auth();
35986
- POLL_INTERVAL_MS2 = 2e3;
35987
- MAX_POLL_ATTEMPTS2 = 150;
36183
+ POLL_INTERVAL_MS = 2e3;
36184
+ MAX_POLL_ATTEMPTS = 150;
35988
36185
  }
35989
36186
  });
35990
36187
 
@@ -38623,7 +38820,7 @@ var init_LoginApp = __esm({
38623
38820
  await init_Spinner();
38624
38821
  import_jsx_runtime2 = __toESM(require_jsx_runtime());
38625
38822
  LoginApp = () => {
38626
- const state = useLogin();
38823
+ const { state, openAndPoll } = useLogin();
38627
38824
  const { exit } = use_app_default();
38628
38825
  (0, import_react24.useEffect)(() => {
38629
38826
  if (state.phase === "success") {
@@ -38642,6 +38839,11 @@ var init_LoginApp = __esm({
38642
38839
  return () => clearTimeout(timer);
38643
38840
  }
38644
38841
  }, [state, exit]);
38842
+ use_input_default((_input, key) => {
38843
+ if (state.phase === "ready" && key.return) {
38844
+ openAndPoll();
38845
+ }
38846
+ });
38645
38847
  switch (state.phase) {
38646
38848
  case "creating":
38647
38849
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner2, { label: "Creating login session..." });
@@ -38654,19 +38856,19 @@ var init_LoginApp = __esm({
38654
38856
  " first to re-authenticate."
38655
38857
  ] })
38656
38858
  ] });
38657
- case "waiting":
38859
+ case "ready":
38658
38860
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "column", paddingLeft: 1, children: [
38659
38861
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }),
38660
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { bold: true, children: "Your verification code:" }),
38862
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: "Authenticate your account at:" }),
38863
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", children: state.verifyUrl }),
38661
38864
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }),
38662
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Text, { bold: true, color: "green", children: [
38663
- " ",
38664
- state.userCode
38665
- ] }),
38865
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: "Press ENTER to open in the browser..." })
38866
+ ] });
38867
+ case "waiting":
38868
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(Box_default, { flexDirection: "column", paddingLeft: 1, children: [
38666
38869
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }),
38667
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: "Opening browser to authenticate..." }),
38668
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, children: "If it didn't open, visit:" }),
38669
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { dimColor: true, color: "cyan", children: state.verifyUrl }),
38870
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: "Authenticate your account at:" }),
38871
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { color: "cyan", children: state.verifyUrl }),
38670
38872
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Text, { children: " " }),
38671
38873
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(Spinner2, { label: "Waiting for authorization..." })
38672
38874
  ] });
@@ -39758,11 +39960,22 @@ async function runStaticLogin() {
39758
39960
  process.exit(1);
39759
39961
  }
39760
39962
  process.stderr.write(`
39761
- Your verification code: ${import_chalk4.default.bold.green(session.userCode)}
39963
+ Authenticate your account at:
39762
39964
  `);
39763
- process.stderr.write(` Visit: ${import_chalk4.default.cyan(session.verifyUrl)}
39965
+ process.stderr.write(` ${import_chalk4.default.cyan(session.verifyUrl)}
39764
39966
 
39765
39967
  `);
39968
+ process.stderr.write(import_chalk4.default.dim(" Press ENTER to open in the browser...\n"));
39969
+ await new Promise((resolve2) => {
39970
+ const onData = () => {
39971
+ process.stdin.removeListener("data", onData);
39972
+ if (process.stdin.unref) process.stdin.unref();
39973
+ resolve2();
39974
+ };
39975
+ process.stdin.setEncoding("utf-8");
39976
+ process.stdin.resume();
39977
+ process.stdin.on("data", onData);
39978
+ });
39766
39979
  try {
39767
39980
  openBrowser2(session.verifyUrl);
39768
39981
  } catch {
@@ -40115,13 +40328,10 @@ function useNpmWrapper(npmArgs, config) {
40115
40328
  }
40116
40329
  } catch (error) {
40117
40330
  const message = error instanceof Error ? error.message : String(error);
40118
- const proceed = true;
40119
- dispatch({ type: "ERROR", message, proceed });
40120
- if (proceed) {
40121
- dispatch({ type: "INSTALLING" });
40122
- const code = await runNpm(parsedRef.current.rawArgs);
40123
- dispatch({ type: "DONE", exitCode: code });
40124
- }
40331
+ dispatch({ type: "ERROR", message, proceed: true });
40332
+ dispatch({ type: "INSTALLING" });
40333
+ const code = await runNpm(parsedRef.current.rawArgs);
40334
+ dispatch({ type: "DONE", exitCode: code });
40125
40335
  }
40126
40336
  })();
40127
40337
  }, [npmArgs, config]);
@@ -40427,7 +40637,7 @@ function getHint(error) {
40427
40637
  if (typeof statusCode !== "number") return null;
40428
40638
  switch (statusCode) {
40429
40639
  case 401:
40430
- return "Check your --api-key";
40640
+ return "Not authenticated. Run `dg login` to sign in.";
40431
40641
  case 429:
40432
40642
  return "Rate limit exceeded. Upgrade at westbayberry.com/pricing";
40433
40643
  case 504:
@@ -40734,6 +40944,8 @@ function reducer3(_state, action) {
40734
40944
  switch (action.type) {
40735
40945
  case "PROJECTS_FOUND":
40736
40946
  return { phase: "selecting", projects: action.projects };
40947
+ case "RESTART_SELECTION":
40948
+ return { phase: "selecting", projects: action.projects };
40737
40949
  case "DISCOVERY_COMPLETE":
40738
40950
  return { phase: "scanning", done: 0, total: action.packages.length, currentBatch: [] };
40739
40951
  case "DISCOVERY_EMPTY":
@@ -40749,6 +40961,7 @@ function reducer3(_state, action) {
40749
40961
  function useScan(config) {
40750
40962
  const [state, dispatch] = (0, import_react27.useReducer)(reducer3, { phase: "discovering" });
40751
40963
  const started = (0, import_react27.useRef)(false);
40964
+ const discoveredProjects = (0, import_react27.useRef)(null);
40752
40965
  (0, import_react27.useEffect)(() => {
40753
40966
  if (started.current) return;
40754
40967
  started.current = true;
@@ -40764,6 +40977,7 @@ function useScan(config) {
40764
40977
  runNpmScan(packages, discovery.skipped.length, config, dispatch);
40765
40978
  } catch {
40766
40979
  const projects = discoverProjects(process.cwd());
40980
+ discoveredProjects.current = projects.length > 1 ? projects : null;
40767
40981
  if (projects.length === 0) {
40768
40982
  dispatch({ type: "DISCOVERY_EMPTY", message: "No dependency files found." });
40769
40983
  return;
@@ -40780,7 +40994,15 @@ function useScan(config) {
40780
40994
  dispatch({ type: "DISCOVERY_COMPLETE", packages: [], skippedCount: 0 });
40781
40995
  scanProjects(projects, config, dispatch);
40782
40996
  }, [config]);
40783
- return { state, scanSelectedProjects };
40997
+ const restartSelection = (0, import_react27.useCallback)(() => {
40998
+ if (!discoveredProjects.current) return;
40999
+ dispatch({ type: "RESTART_SELECTION", projects: discoveredProjects.current });
41000
+ }, []);
41001
+ return {
41002
+ state,
41003
+ scanSelectedProjects,
41004
+ restartSelection: discoveredProjects.current ? restartSelection : null
41005
+ };
40784
41006
  }
40785
41007
  async function runNpmScan(packages, skippedCount, config, dispatch) {
40786
41008
  try {
@@ -41016,7 +41238,17 @@ function affectsLine(group) {
41016
41238
  if (names.length <= 5) return names.join(", ");
41017
41239
  return names.slice(0, 5).join(", ") + ` + ${names.length - 5} more`;
41018
41240
  }
41019
- var import_react29, import_chalk10, import_jsx_runtime10, SEVERITY_LABELS3, SEVERITY_COLORS, EVIDENCE_LIMIT2, FIXED_CHROME, MOUSE_ON, MOUSE_OFF, InteractiveResultsView, T, FindingsSummary, FindingsDetail;
41241
+ function viewReducer(_state, action) {
41242
+ switch (action.type) {
41243
+ case "MOVE":
41244
+ return { ..._state, cursor: action.cursor, viewport: action.viewport };
41245
+ case "EXPAND":
41246
+ return { ..._state, expandedIndex: action.expandedIndex, expandLevel: action.expandLevel, viewport: action.viewport };
41247
+ case "MOVE_EXPAND":
41248
+ return { cursor: action.cursor, expandedIndex: action.expandedIndex, expandLevel: action.expandLevel, viewport: action.viewport };
41249
+ }
41250
+ }
41251
+ var import_react29, import_chalk10, import_jsx_runtime10, SEVERITY_LABELS3, SEVERITY_COLORS, EVIDENCE_LIMIT2, FIXED_CHROME, InteractiveResultsView, T, FindingsSummary, FindingsDetail;
41020
41252
  var init_InteractiveResultsView = __esm({
41021
41253
  async "src/ui/components/InteractiveResultsView.tsx"() {
41022
41254
  "use strict";
@@ -41042,19 +41274,18 @@ var init_InteractiveResultsView = __esm({
41042
41274
  };
41043
41275
  EVIDENCE_LIMIT2 = 2;
41044
41276
  FIXED_CHROME = 16;
41045
- MOUSE_ON = "\x1B[?1000h\x1B[?1003h\x1B[?1006h";
41046
- MOUSE_OFF = "\x1B[?1006l\x1B[?1003l\x1B[?1000l";
41047
41277
  InteractiveResultsView = ({
41048
41278
  result,
41049
41279
  config,
41050
41280
  durationMs,
41051
- onExit
41281
+ onExit,
41282
+ onBack
41052
41283
  }) => {
41053
41284
  (0, import_react29.useEffect)(() => {
41054
41285
  if (!process.stdout.isTTY) return;
41055
- process.stdout.write("\x1B[?1049h" + MOUSE_ON);
41286
+ process.stdout.write("\x1B[?1049h");
41056
41287
  return () => {
41057
- process.stdout.write(MOUSE_OFF + "\x1B[?1049l");
41288
+ process.stdout.write("\x1B[?1049l");
41058
41289
  };
41059
41290
  }, []);
41060
41291
  const flagged = (0, import_react29.useMemo)(
@@ -41067,45 +41298,41 @@ var init_InteractiveResultsView = __esm({
41067
41298
  );
41068
41299
  const total = result.packages.length;
41069
41300
  const groups = (0, import_react29.useMemo)(() => groupPackages3(flagged), [flagged]);
41070
- const [cursorIndex, setCursorIndex] = (0, import_react29.useState)(0);
41071
- const [expandLevel, setExpandLevel] = (0, import_react29.useState)(null);
41072
- const [expandedIndex, setExpandedIndex] = (0, import_react29.useState)(null);
41073
- const [viewportStart, setViewportStart] = (0, import_react29.useState)(0);
41074
- const cursorRef = (0, import_react29.useRef)(cursorIndex);
41075
- const expandLevelRef = (0, import_react29.useRef)(expandLevel);
41076
- const expandedIdxRef = (0, import_react29.useRef)(expandedIndex);
41077
- const viewportRef = (0, import_react29.useRef)(viewportStart);
41078
- cursorRef.current = cursorIndex;
41079
- expandLevelRef.current = expandLevel;
41080
- expandedIdxRef.current = expandedIndex;
41081
- viewportRef.current = viewportStart;
41301
+ const [view, dispatchView] = (0, import_react29.useReducer)(viewReducer, {
41302
+ cursor: 0,
41303
+ expandLevel: null,
41304
+ expandedIndex: null,
41305
+ viewport: 0
41306
+ });
41307
+ const viewRef = (0, import_react29.useRef)(view);
41308
+ viewRef.current = view;
41082
41309
  const { stdout } = use_stdout_default();
41083
41310
  const termCols = stdout?.columns ?? process.stdout.columns ?? 80;
41084
41311
  const termRows = stdout?.rows ?? process.stdout.rows ?? 24;
41085
41312
  const availableRows = Math.max(5, termRows - FIXED_CHROME);
41086
41313
  const innerWidth = Math.max(40, termCols - 6);
41087
41314
  const getLevel = (idx) => {
41088
- return expandedIndex === idx ? expandLevel : null;
41315
+ return view.expandedIndex === idx ? view.expandLevel : null;
41089
41316
  };
41090
41317
  const expandTargetHeight = (0, import_react29.useMemo)(() => {
41091
- if (expandedIndex === null || expandLevel === null) return 0;
41092
- const group = groups[expandedIndex];
41318
+ if (view.expandedIndex === null || view.expandLevel === null) return 0;
41319
+ const group = groups[view.expandedIndex];
41093
41320
  if (!group) return 0;
41094
- if (expandLevel === "summary") return findingsSummaryHeight(group);
41321
+ if (view.expandLevel === "summary") return findingsSummaryHeight(group);
41095
41322
  return findingsDetailHeight(group, result.safeVersions);
41096
- }, [expandedIndex, expandLevel, groups, result.safeVersions]);
41323
+ }, [view.expandedIndex, view.expandLevel, groups, result.safeVersions]);
41097
41324
  const { visibleLines: animVisibleLines } = useExpandAnimation(
41098
41325
  expandTargetHeight,
41099
- expandedIndex !== null
41326
+ view.expandedIndex !== null
41100
41327
  );
41101
41328
  const animatedGroupHeight = (group, level, idx) => {
41102
41329
  if (level === null) return 1;
41103
- if (idx === expandedIndex) return 1 + animVisibleLines;
41330
+ if (idx === view.expandedIndex) return 1 + animVisibleLines;
41104
41331
  return groupRowHeight(group, level, result.safeVersions);
41105
41332
  };
41106
41333
  const visibleEnd = (0, import_react29.useMemo)(() => {
41107
41334
  let consumed = 0;
41108
- let end = viewportStart;
41335
+ let end = view.viewport;
41109
41336
  while (end < groups.length) {
41110
41337
  const level = getLevel(end);
41111
41338
  const h = animatedGroupHeight(groups[end], level, end);
@@ -41113,9 +41340,9 @@ var init_InteractiveResultsView = __esm({
41113
41340
  consumed += h;
41114
41341
  end++;
41115
41342
  }
41116
- if (end === viewportStart && groups.length > 0) end = viewportStart + 1;
41343
+ if (end === view.viewport && groups.length > 0) end = view.viewport + 1;
41117
41344
  return end;
41118
- }, [viewportStart, groups, expandedIndex, expandLevel, animVisibleLines, availableRows, result.safeVersions]);
41345
+ }, [view.viewport, groups, view.expandedIndex, view.expandLevel, animVisibleLines, availableRows, result.safeVersions]);
41119
41346
  const adjustViewport = (cursor, expIdx, expLvl, currentStart) => {
41120
41347
  if (cursor < currentStart) return cursor;
41121
41348
  const getLvl = (i) => expIdx === i ? expLvl : null;
@@ -41140,43 +41367,39 @@ var init_InteractiveResultsView = __esm({
41140
41367
  if (input === "q" || key.return) onExit();
41141
41368
  return;
41142
41369
  }
41143
- const cursor = cursorRef.current;
41144
- const expLvl = expandLevelRef.current;
41145
- const expIdx = expandedIdxRef.current;
41146
- const vpStart = viewportRef.current;
41370
+ const { cursor, expandLevel: expLvl, expandedIndex: expIdx, viewport: vpStart } = viewRef.current;
41147
41371
  if (key.upArrow) {
41148
41372
  const next = Math.max(0, cursor - 1);
41149
41373
  const newVp = adjustViewport(next, expIdx, expLvl, vpStart < next ? vpStart : next);
41150
- setCursorIndex(next);
41151
- setViewportStart(newVp);
41374
+ dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
41152
41375
  } else if (key.downArrow) {
41153
41376
  const next = Math.min(groups.length - 1, cursor + 1);
41154
41377
  const newVp = adjustViewport(next, expIdx, expLvl, vpStart);
41155
- setCursorIndex(next);
41156
- setViewportStart(newVp);
41378
+ dispatchView({ type: "MOVE", cursor: next, viewport: newVp });
41157
41379
  } else if (key.return) {
41158
41380
  let newExpIdx;
41159
41381
  let newExpLvl;
41160
- if (expIdx !== cursor) {
41161
- newExpIdx = cursor;
41162
- newExpLvl = "summary";
41163
- } else if (expLvl === "summary") {
41164
- newExpIdx = cursor;
41165
- newExpLvl = "detail";
41166
- } else {
41382
+ if (expIdx === cursor && expLvl !== null) {
41167
41383
  newExpIdx = null;
41168
41384
  newExpLvl = null;
41385
+ } else {
41386
+ newExpIdx = cursor;
41387
+ newExpLvl = "summary";
41169
41388
  }
41170
- setExpandedIndex(newExpIdx);
41171
- setExpandLevel(newExpLvl);
41172
41389
  const newVp = adjustViewport(cursor, newExpIdx, newExpLvl, vpStart);
41173
- setViewportStart(newVp);
41390
+ dispatchView({ type: "EXPAND", expandedIndex: newExpIdx, expandLevel: newExpLvl, viewport: newVp });
41391
+ } else if (input === "e") {
41392
+ if (expIdx === cursor && expLvl === "detail") return;
41393
+ const newVp = adjustViewport(cursor, cursor, "detail", vpStart);
41394
+ dispatchView({ type: "EXPAND", expandedIndex: cursor, expandLevel: "detail", viewport: newVp });
41395
+ } else if (input === "b" && onBack) {
41396
+ onBack();
41174
41397
  } else if (input === "q") {
41175
41398
  onExit();
41176
41399
  }
41177
41400
  });
41178
- const visibleGroups = groups.slice(viewportStart, visibleEnd);
41179
- const aboveCount = viewportStart;
41401
+ const visibleGroups = groups.slice(view.viewport, visibleEnd);
41402
+ const aboveCount = view.viewport;
41180
41403
  const belowCount = groups.length - visibleEnd;
41181
41404
  const nameCol = Math.max(20, innerWidth - 22);
41182
41405
  return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(Box_default, { flexDirection: "column", children: [
@@ -41215,8 +41438,8 @@ var init_InteractiveResultsView = __esm({
41215
41438
  " more above"
41216
41439
  ] }),
41217
41440
  visibleGroups.map((group, visIdx) => {
41218
- const globalIdx = viewportStart + visIdx;
41219
- const isCursor = globalIdx === cursorIndex;
41441
+ const globalIdx = view.viewport + visIdx;
41442
+ const isCursor = globalIdx === view.cursor;
41220
41443
  const level = getLevel(globalIdx);
41221
41444
  const rep = group.packages[0];
41222
41445
  const { label, color } = actionBadge4(rep.score, config);
@@ -41235,7 +41458,7 @@ var init_InteractiveResultsView = __esm({
41235
41458
  {
41236
41459
  group,
41237
41460
  maxWidth: innerWidth - 8,
41238
- maxLines: globalIdx === expandedIndex ? animVisibleLines : void 0
41461
+ maxLines: globalIdx === view.expandedIndex ? animVisibleLines : void 0
41239
41462
  }
41240
41463
  ),
41241
41464
  level === "detail" && /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
@@ -41244,7 +41467,7 @@ var init_InteractiveResultsView = __esm({
41244
41467
  group,
41245
41468
  safeVersion: result.safeVersions[rep.name],
41246
41469
  maxWidth: innerWidth - 8,
41247
- maxLines: globalIdx === expandedIndex ? animVisibleLines : void 0
41470
+ maxLines: globalIdx === view.expandedIndex ? animVisibleLines : void 0
41248
41471
  }
41249
41472
  )
41250
41473
  ] }, group.key);
@@ -41291,13 +41514,23 @@ var init_InteractiveResultsView = __esm({
41291
41514
  " navigate",
41292
41515
  " ",
41293
41516
  import_chalk10.default.cyan("\u23CE"),
41294
- " expand/collapse",
41517
+ " toggle",
41518
+ " ",
41519
+ import_chalk10.default.cyan("e"),
41520
+ " detail",
41295
41521
  " ",
41522
+ onBack && /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
41523
+ import_chalk10.default.cyan("b"),
41524
+ " back",
41525
+ " "
41526
+ ] }),
41296
41527
  import_chalk10.default.cyan("q"),
41297
41528
  " quit"
41298
41529
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(import_jsx_runtime10.Fragment, { children: [
41299
41530
  "Press ",
41300
41531
  import_chalk10.default.cyan("q"),
41532
+ " or ",
41533
+ import_chalk10.default.cyan("Enter"),
41301
41534
  " to exit"
41302
41535
  ] })
41303
41536
  ] })
@@ -41305,13 +41538,9 @@ var init_InteractiveResultsView = __esm({
41305
41538
  };
41306
41539
  T = {
41307
41540
  branch: import_chalk10.default.dim("\u251C\u2500\u2500"),
41308
- // ├──
41309
41541
  last: import_chalk10.default.dim("\u2514\u2500\u2500"),
41310
- // └──
41311
41542
  pipe: import_chalk10.default.dim("\u2502"),
41312
- // │
41313
41543
  blank: " "
41314
- //
41315
41544
  };
41316
41545
  FindingsSummary = ({ group, maxWidth, maxLines }) => {
41317
41546
  const rep = group.packages[0];
@@ -41477,12 +41706,7 @@ var init_ProjectSelector = __esm({
41477
41706
  return /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Text, { bold: isCursor, inverse: isCursor, children: line }, i);
41478
41707
  }),
41479
41708
  /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Text, { children: "" }),
41480
- /* @__PURE__ */ (0, import_jsx_runtime11.jsxs)(Text, { dimColor: true, children: [
41481
- selected.size,
41482
- " of ",
41483
- projects.length,
41484
- " selected"
41485
- ] })
41709
+ /* @__PURE__ */ (0, import_jsx_runtime11.jsx)(Text, { dimColor: true, children: selected.size === 0 ? "Select at least 1 project to scan" : `${selected.size} of ${projects.length} selected` })
41486
41710
  ] });
41487
41711
  };
41488
41712
  }
@@ -41507,7 +41731,7 @@ var init_App2 = __esm({
41507
41731
  await init_ProjectSelector();
41508
41732
  import_jsx_runtime12 = __toESM(require_jsx_runtime());
41509
41733
  App2 = ({ config }) => {
41510
- const { state, scanSelectedProjects } = useScan(config);
41734
+ const { state, scanSelectedProjects, restartSelection } = useScan(config);
41511
41735
  const { exit } = use_app_default();
41512
41736
  const handleResultsExit = (0, import_react31.useCallback)(() => {
41513
41737
  if (state.phase === "results") {
@@ -41574,7 +41798,8 @@ var init_App2 = __esm({
41574
41798
  result: state.result,
41575
41799
  config,
41576
41800
  durationMs: state.durationMs,
41577
- onExit: handleResultsExit
41801
+ onExit: handleResultsExit,
41802
+ onBack: restartSelection ?? void 0
41578
41803
  }
41579
41804
  );
41580
41805
  case "empty":
@@ -41586,183 +41811,8 @@ var init_App2 = __esm({
41586
41811
  }
41587
41812
  });
41588
41813
 
41589
- // src/config.ts
41590
- import { parseArgs } from "node:util";
41591
- import { readFileSync, existsSync } from "node:fs";
41592
- import { join } from "node:path";
41593
- import { homedir } from "node:os";
41594
- function loadDgrc() {
41595
- const candidates = [
41596
- join(process.cwd(), ".dgrc.json"),
41597
- join(homedir(), ".dgrc.json")
41598
- ];
41599
- for (const filepath of candidates) {
41600
- if (existsSync(filepath)) {
41601
- try {
41602
- return JSON.parse(readFileSync(filepath, "utf-8"));
41603
- } catch {
41604
- process.stderr.write(`Warning: Failed to parse ${filepath}, ignoring.
41605
- `);
41606
- }
41607
- }
41608
- }
41609
- return {};
41610
- }
41611
- var USAGE = `
41612
- Dependency Guardian \u2014 Supply chain security scanner
41613
-
41614
- Usage:
41615
- dependency-guardian scan [options]
41616
- dg scan [options]
41617
- dg npm install <pkg> [npm-flags]
41618
- dg wrap
41619
-
41620
- Commands:
41621
- scan Scan dependencies (auto-discovers npm + Python projects)
41622
- npm Wrap npm commands \u2014 scans packages before installing
41623
- hook install Install git pre-commit hook to scan lockfile changes
41624
- hook uninstall Remove the pre-commit hook
41625
- update Check for and install the latest version
41626
- login Authenticate with your WestBayBerry account
41627
- logout Remove saved credentials
41628
- wrap Show instructions to alias npm to dg
41629
-
41630
- Options:
41631
- --api-url <url> API base URL (default: https://api.westbayberry.com)
41632
- --mode <mode> block | warn | off (default: warn)
41633
- --block-threshold <n> Score threshold for blocking (default: 70)
41634
- --warn-threshold <n> Score threshold for warnings (default: 60)
41635
- --max-packages <n> Max packages per scan (default: 200)
41636
- --allowlist <pkgs> Comma-separated package names to skip
41637
- --json Output JSON for CI parsing
41638
- --scan-all Scan all packages, not just changed
41639
- --base-lockfile <path> Path to base lockfile for explicit diff
41640
- --workspace <dir> Scan a specific workspace subdirectory
41641
- --debug Show diagnostic output (discovery, batches, timing)
41642
- --no-config Skip loading .dgrc.json config file
41643
- --help Show this help message
41644
- --version Show version number
41645
-
41646
- Config File:
41647
- Place a .dgrc.json in your project root or home directory.
41648
- Precedence: CLI flags > env vars > .dgrc.json > defaults
41649
-
41650
- Environment Variables:
41651
- DG_API_URL API base URL
41652
- DG_MODE Mode (block/warn/off)
41653
- DG_ALLOWLIST Comma-separated allowlist
41654
- DG_DEBUG Enable debug output (set to 1)
41655
- DG_WORKSPACE Workspace subdirectory to scan
41656
-
41657
- Exit Codes:
41658
- 0 pass \u2014 No risks detected
41659
- 1 warn \u2014 Risks detected (advisory)
41660
- 2 block \u2014 High-risk packages detected
41661
- 3 error \u2014 Internal error (API failure, config error)
41662
-
41663
- Examples:
41664
- dg scan
41665
- dg scan --json
41666
- dg scan --scan-all --mode block
41667
- dg scan --base-lockfile ./main-lockfile.json
41668
- dg npm install express lodash
41669
- dg npm install @scope/pkg@^2.0.0
41670
- dg npm install risky-pkg --dg-force
41671
- `.trimStart();
41672
- function getVersion() {
41673
- try {
41674
- const pkg = JSON.parse(
41675
- readFileSync(join(__dirname, "..", "package.json"), "utf-8")
41676
- );
41677
- return pkg.version ?? "1.0.0";
41678
- } catch {
41679
- return "1.0.0";
41680
- }
41681
- }
41682
- function parseConfig(argv) {
41683
- const { values, positionals } = parseArgs({
41684
- args: argv.slice(2),
41685
- options: {
41686
- "api-url": { type: "string" },
41687
- mode: { type: "string" },
41688
- "block-threshold": { type: "string" },
41689
- "warn-threshold": { type: "string" },
41690
- "max-packages": { type: "string" },
41691
- allowlist: { type: "string" },
41692
- json: { type: "boolean", default: false },
41693
- "scan-all": { type: "boolean", default: false },
41694
- "base-lockfile": { type: "string" },
41695
- workspace: { type: "string", short: "w" },
41696
- debug: { type: "boolean", default: false },
41697
- "no-config": { type: "boolean", default: false },
41698
- help: { type: "boolean", default: false },
41699
- version: { type: "boolean", default: false }
41700
- },
41701
- allowPositionals: true,
41702
- strict: false
41703
- });
41704
- if (values.help) {
41705
- process.stdout.write(USAGE);
41706
- process.exit(0);
41707
- }
41708
- if (values.version) {
41709
- process.stdout.write(`dependency-guardian v${getVersion()}
41710
- `);
41711
- process.exit(0);
41712
- }
41713
- const command = positionals[0] ?? "scan";
41714
- const noConfig = values["no-config"];
41715
- const dgrc = noConfig ? {} : loadDgrc();
41716
- const apiKey = dgrc.apiKey ?? "";
41717
- if (!apiKey) {
41718
- process.stderr.write(
41719
- "Error: Not logged in. Run `dg login` to authenticate.\n"
41720
- );
41721
- process.exit(1);
41722
- }
41723
- const modeRaw = values.mode ?? process.env.DG_MODE ?? dgrc.mode ?? "warn";
41724
- if (!["block", "warn", "off"].includes(modeRaw)) {
41725
- process.stderr.write(
41726
- `Error: Invalid mode "${modeRaw}". Must be block, warn, or off.
41727
- `
41728
- );
41729
- process.exit(1);
41730
- }
41731
- const allowlistRaw = values.allowlist ?? process.env.DG_ALLOWLIST ?? "";
41732
- const blockThreshold = Number(values["block-threshold"] ?? dgrc.blockThreshold ?? "70");
41733
- const warnThreshold = Number(values["warn-threshold"] ?? dgrc.warnThreshold ?? "60");
41734
- const maxPackages = Number(values["max-packages"] ?? dgrc.maxPackages ?? "200");
41735
- const debug = values.debug || process.env.DG_DEBUG === "1";
41736
- if (isNaN(blockThreshold) || blockThreshold < 0 || blockThreshold > 100) {
41737
- process.stderr.write("Error: --block-threshold must be a number between 0 and 100\n");
41738
- process.exit(1);
41739
- }
41740
- if (isNaN(warnThreshold) || warnThreshold < 0 || warnThreshold > 100) {
41741
- process.stderr.write("Error: --warn-threshold must be a number between 0 and 100\n");
41742
- process.exit(1);
41743
- }
41744
- if (isNaN(maxPackages) || maxPackages < 1 || maxPackages > 1e4) {
41745
- process.stderr.write("Error: --max-packages must be a number between 1 and 10000\n");
41746
- process.exit(1);
41747
- }
41748
- return {
41749
- apiKey,
41750
- apiUrl: values["api-url"] ?? process.env.DG_API_URL ?? dgrc.apiUrl ?? "https://api.westbayberry.com",
41751
- mode: modeRaw,
41752
- blockThreshold,
41753
- warnThreshold,
41754
- maxPackages,
41755
- allowlist: allowlistRaw ? allowlistRaw.split(",").map((s) => s.trim()).filter(Boolean) : dgrc.allowlist ?? [],
41756
- json: values.json,
41757
- scanAll: values["scan-all"],
41758
- baseLockfile: values["base-lockfile"] ?? null,
41759
- workspace: values.workspace ?? process.env.DG_WORKSPACE ?? null,
41760
- command,
41761
- debug
41762
- };
41763
- }
41764
-
41765
41814
  // src/bin.ts
41815
+ init_config();
41766
41816
  init_npm_wrapper();
41767
41817
 
41768
41818
  // src/update-check.ts
@@ -41824,8 +41874,7 @@ function spawnBackgroundUpdate(version) {
41824
41874
  try {
41825
41875
  const child = spawn2("npm", ["install", "-g", `${PKG_NAME}@${version}`], {
41826
41876
  detached: true,
41827
- stdio: "ignore",
41828
- shell: true
41877
+ stdio: "ignore"
41829
41878
  });
41830
41879
  child.unref();
41831
41880
  } catch {
@@ -41887,10 +41936,20 @@ async function runUpdate(currentVersion) {
41887
41936
  }
41888
41937
 
41889
41938
  // src/bin.ts
41890
- var CLI_VERSION = "1.0.4";
41939
+ var CLI_VERSION = getVersion();
41891
41940
  var isInteractive = process.stdout.isTTY === true && !process.env.CI && !process.env.NO_COLOR;
41892
41941
  async function main() {
41893
41942
  const rawCommand = process.argv[2];
41943
+ if (!rawCommand || rawCommand === "--help" || rawCommand === "-h") {
41944
+ const { USAGE: USAGE3 } = await Promise.resolve().then(() => (init_config(), config_exports));
41945
+ process.stdout.write(USAGE3);
41946
+ return;
41947
+ }
41948
+ if (rawCommand === "--version" || rawCommand === "-v") {
41949
+ process.stdout.write(`dependency-guardian v${CLI_VERSION}
41950
+ `);
41951
+ return;
41952
+ }
41894
41953
  if (rawCommand === "wrap") {
41895
41954
  handleWrapCommand();
41896
41955
  return;
@@ -41921,7 +41980,7 @@ async function main() {
41921
41980
  const { clearCredentials: clearCredentials2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
41922
41981
  clearCredentials2();
41923
41982
  const chalk9 = (await Promise.resolve().then(() => __toESM(require_source()))).default;
41924
- process.stderr.write(chalk9.green(" Logged out.") + chalk9.dim(" API key removed from ~/.dgrc.json\n"));
41983
+ process.stderr.write(chalk9.green(" Logged out.\n"));
41925
41984
  return;
41926
41985
  }
41927
41986
  const config = parseConfig(process.argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@westbayberry/dg",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "Supply chain security scanner — scan npm dependencies in any CI or terminal",
5
5
  "bin": {
6
6
  "dependency-guardian": "dist/index.mjs",