depvitals 0.1.2 → 0.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.
Files changed (60) hide show
  1. package/README.md +37 -55
  2. package/dist/cli/entry.d.ts +59 -0
  3. package/dist/cli/entry.d.ts.map +1 -0
  4. package/dist/cli/entry.js +95 -0
  5. package/dist/cli/entry.js.map +1 -0
  6. package/dist/cli/index.js +14 -18
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/cli/notifier/eligibility.d.ts +65 -0
  9. package/dist/cli/notifier/eligibility.d.ts.map +1 -0
  10. package/dist/cli/notifier/eligibility.js +123 -0
  11. package/dist/cli/notifier/eligibility.js.map +1 -0
  12. package/dist/cli/notifier/registryLookup.d.ts +111 -0
  13. package/dist/cli/notifier/registryLookup.d.ts.map +1 -0
  14. package/dist/cli/notifier/registryLookup.js +211 -0
  15. package/dist/cli/notifier/registryLookup.js.map +1 -0
  16. package/dist/cli/notifier/updateCache.d.ts +101 -0
  17. package/dist/cli/notifier/updateCache.d.ts.map +1 -0
  18. package/dist/cli/notifier/updateCache.js +352 -0
  19. package/dist/cli/notifier/updateCache.js.map +1 -0
  20. package/dist/cli/notifier/updateCheck.d.ts +88 -0
  21. package/dist/cli/notifier/updateCheck.d.ts.map +1 -0
  22. package/dist/cli/notifier/updateCheck.js +166 -0
  23. package/dist/cli/notifier/updateCheck.js.map +1 -0
  24. package/dist/cli/notifier/versionCompare.d.ts +48 -0
  25. package/dist/cli/notifier/versionCompare.d.ts.map +1 -0
  26. package/dist/cli/notifier/versionCompare.js +89 -0
  27. package/dist/cli/notifier/versionCompare.js.map +1 -0
  28. package/dist/cli/runCli.d.ts +2 -0
  29. package/dist/cli/runCli.d.ts.map +1 -1
  30. package/dist/cli/runCli.js +30 -3
  31. package/dist/cli/runCli.js.map +1 -1
  32. package/dist/core/analyzeProject.d.ts +44 -6
  33. package/dist/core/analyzeProject.d.ts.map +1 -1
  34. package/dist/core/analyzeProject.js +110 -11
  35. package/dist/core/analyzeProject.js.map +1 -1
  36. package/dist/detectors/eslintManifestConfig.d.ts +111 -0
  37. package/dist/detectors/eslintManifestConfig.d.ts.map +1 -0
  38. package/dist/detectors/eslintManifestConfig.js +348 -0
  39. package/dist/detectors/eslintManifestConfig.js.map +1 -0
  40. package/dist/detectors/eslintRcJsonConfig.d.ts +85 -0
  41. package/dist/detectors/eslintRcJsonConfig.d.ts.map +1 -0
  42. package/dist/detectors/eslintRcJsonConfig.js +188 -0
  43. package/dist/detectors/eslintRcJsonConfig.js.map +1 -0
  44. package/dist/detectors/jestManifestConfig.d.ts +76 -0
  45. package/dist/detectors/jestManifestConfig.d.ts.map +1 -0
  46. package/dist/detectors/jestManifestConfig.js +292 -0
  47. package/dist/detectors/jestManifestConfig.js.map +1 -0
  48. package/dist/reporters/terminalReporter.js +27 -1
  49. package/dist/reporters/terminalReporter.js.map +1 -1
  50. package/dist/scanner/moduleSpecifiers.d.ts +9 -1
  51. package/dist/scanner/moduleSpecifiers.d.ts.map +1 -1
  52. package/dist/scanner/moduleSpecifiers.js +2352 -173
  53. package/dist/scanner/moduleSpecifiers.js.map +1 -1
  54. package/dist/scanner/projectManifest.d.ts +14 -0
  55. package/dist/scanner/projectManifest.d.ts.map +1 -1
  56. package/dist/scanner/projectManifest.js +40 -0
  57. package/dist/scanner/projectManifest.js.map +1 -1
  58. package/dist/scanner/scriptUsage.js +35 -3
  59. package/dist/scanner/scriptUsage.js.map +1 -1
  60. package/package.json +7 -7
package/README.md CHANGED
@@ -1,88 +1,70 @@
1
1
  # DepVitals
2
2
 
3
- > A simple, lightweight dependency health checker for Node.js projects.
3
+ DepVitals finds unused and undeclared npm dependencies in single-package JavaScript and TypeScript projects. It is a zero-config Node.js CLI with a custom static scanner and no runtime dependencies.
4
4
 
5
- DepVitals scans a Node.js project to identify declared dependencies that appear unused and imported packages that are not declared in `package.json`.
5
+ [Depcheck](https://github.com/depcheck/depcheck)-inspired, DepVitals focuses on dependency declarations and supported usage evidence. Use it to review dependencies during maintenance or before a release; review each finding before changing your manifest.
6
6
 
7
- Inspired by [depcheck](https://github.com/depcheck/depcheck), DepVitals stays deliberately small. It performs lightweight static analysis locally, without making network requests, modifying the analyzed project, or executing its code.
7
+ Documentation on `main` can be ahead of the [published npm package](https://www.npmjs.com/package/depvitals). Check the [changelog](https://github.com/MatiasRicardi/depvitals/blob/main/CHANGELOG.md) for release-specific behavior.
8
8
 
9
- ## Usage
9
+ ## Quick start
10
10
 
11
- ```bash
11
+ Requires **Node.js 22.13.0 or newer**. From a directory containing `package.json`:
12
+
13
+ ```sh
12
14
  npx depvitals
13
- npx depvitals ./path/to/project
14
- npx depvitals --help
15
- npx depvitals --version
16
15
  ```
17
16
 
18
- Run it from a project directory to scan the current directory, or provide a path to analyze another project.
17
+ Scan an explicit project path, or request standalone help/version:
19
18
 
20
- **Requirements:** Node.js `>=22.13.0`.
21
-
22
- **Version note:** Dependency scanning was introduced in v0.1.0, and v0.1.2 expanded the script executable allowlist. The earlier v0.0.1 release contains only the initial CLI bootstrap.
23
-
24
- **Exit status:**
25
-
26
- ```text
27
- 0 = successful clean scan, help, or version
28
- 1 = dependency findings or an operational/CLI error
19
+ ```sh
20
+ npx depvitals ./path/to/project
21
+ npx depvitals --help
22
+ npx depvitals --version
29
23
  ```
30
24
 
31
- Scan results, including dependency findings, are printed to `stdout`. Argument errors and operational errors are printed to `stderr`.
32
-
33
- ## What it detects
25
+ These commands run the npm release. For development-branch behavior, build and run the local CLI as described in [Contributing](https://github.com/MatiasRicardi/depvitals/blob/main/CONTRIBUTING.md).
34
26
 
35
- - **Unused dependencies** — packages declared in `dependencies` for which no supported source file or `package.json` script provides usage evidence.
36
- - **Unused devDependencies** — the same analysis applied to packages declared in `devDependencies`.
37
- - **Missing dependencies** — external packages referenced by supported source imports but not declared in the project's `package.json`.
27
+ ## Understand the results
38
28
 
39
- DepVitals does not inspect `node_modules`, so **"missing" means "not declared", not "not installed"**. An installed but undeclared package can still be reported as missing, and package installation state is never validated.
29
+ | Finding | Meaning |
30
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
31
+ | Unused dependencies | Entries in `dependencies` with no supported source, script or configuration usage evidence. |
32
+ | Unused devDependencies | The same check for `devDependencies`. |
33
+ | Missing dependencies | Packages referenced by supported source imports/requires but absent from every dependency declaration group. |
40
34
 
41
- Similarly, a dependency is reported as unused only when no supported usage evidence is found. Indirect or unsupported usage may still exist.
35
+ **Missing means undeclared, not uninstalled.** DepVitals does not inspect `node_modules`. **Unused is not proof of safe removal**: indirect or unsupported usage may exist. Peer and optional declarations suppress missing findings and are never reported as unused.
42
36
 
43
- `peerDependencies` and `optionalDependencies` are considered when checking whether imported packages are declared, but are never reported as unused.
44
-
45
- **Example output:**
37
+ A scan can report:
46
38
 
47
39
  ```text
48
40
  DepVitals
49
41
 
50
- Unused dependencies (2)
51
- dotenv
42
+ Unused dependencies (1)
52
43
  lodash
53
44
 
54
45
  Unused devDependencies (1)
55
- eslint
46
+ echo-only
56
47
 
57
48
  Missing dependencies (1)
58
- zod
49
+ undeclared-pkg
59
50
  ```
60
51
 
61
- ## Supported in v0.1.0
52
+ Reports go to stdout; operational errors go to stderr. Exit status is **0** for a clean scan, help or version, and **1** for findings or errors. An eligible update notice may additionally use stderr without changing the status.
53
+
54
+ ## Coverage and fit
55
+
56
+ DepVitals recognizes literal ESM/CommonJS imports, exports and requires in JavaScript, TypeScript, JSX/TSX and declaration files. It normalizes scoped/subpath imports and excludes Node builtins and internal paths.
57
+
58
+ Supported `package.json` commands and bounded ESLint/Jest configuration hints can prevent unused findings for declared packages. Only source evidence creates missing findings. See the [technical reference](https://github.com/MatiasRicardi/depvitals/blob/main/docs/reference.md) for supported patterns, precedence and exclusions.
62
59
 
63
- - **JavaScript and TypeScript:** `.js`, `.jsx`, `.ts`, `.tsx`, `.mjs`, `.cjs`, `.mts`, `.cts`, including `.d.ts`-style declaration files.
64
- - **Static ESM imports and exports:** regular imports, side-effect imports, `import type`, and `export ... from`.
65
- - **Literal dynamic imports:** expressions such as `import("express")`.
66
- - **Literal CommonJS requires:** expressions such as `require("express")`.
67
- - **Basic `package.json` script awareness:** a declared dependency invoked by a supported script command counts as used, for example `eslint` in `"lint": "eslint ."`. Script usage can prevent unused findings but does not generate missing-dependency findings.
68
- - **Known executable names:** since v0.1.2, a static allowlist of 60 known executable commands resolves to 46 declared packages, so a real CLI executable credits the package that declares it, for example `tsserver` → `typescript`, `ng` → `@angular/cli`, `vue-cli-service` → `@vue/cli-service`, `depcruise` → `dependency-cruiser`, `sls` → `serverless`, and `gulp` → `gulp-cli`. Each alias was verified against published `bin` metadata beforehand: nothing is auto-discovered from `bin` fields or from `node_modules`. 32 of them are collision-sensitive, meaning a same-named declared package wins and the mapped provider gets no evidence.
69
- - **Node.js builtins:** built-in modules, with or without the `node:` prefix, are excluded from missing-dependency findings.
70
- - **Scoped packages and subpaths:** package specifiers are normalized to their package root. For example, `@scope/ui/button` counts as `@scope/ui`, and `lodash/chunk` counts as `lodash`.
71
- - **Ignored directories:** generated and vendor trees are skipped, including `node_modules`, `dist`, `build`, `coverage`, `out`, `.next`, `.nuxt`, `.cache`, `.turbo`, and `.git`.
72
- - **Deterministic output:** findings are grouped into sections in a fixed order, with package names sorted alphabetically within each section.
60
+ ## Limits and update check
73
61
 
74
- Detection uses a lightweight static lexical scanner rather than a full JavaScript/TypeScript parser. Comments, ordinary strings, template text, and regular expressions that merely resemble imports are not counted as dependency usage.
62
+ Analysis covers one project root, without workspace aggregation, alias/tsconfig resolution or blanket `@types` inference. The scanner is not a full compiler or resolver. Dynamic references can be missed; generated/vendor directories are excluded using fixed rules. There is no JSON output, public analyzer API, per-package explanation or automatic fix.
75
63
 
76
- ## Current limitations
64
+ Core analysis is offline and never executes or modifies the analyzed project. After a completed interactive scan, an optional, bounded update check can request public metadata for **DepVitals itself** and cache it outside the project for about 24 hours. It uploads no source or dependency list and adds no telemetry. CI, non-TTY, help/version and failed scans skip the check. Set `DEPVITALS_NO_UPDATE_CHECK=1` to disable it; the [reference](https://github.com/MatiasRicardi/depvitals/blob/main/docs/reference.md#update-notice-and-privacy) shows POSIX and PowerShell examples.
77
65
 
78
- - **Single-project analysis:** only one `package.json` is analyzed. Workspaces, monorepos, and parent-manifest lookup are not supported.
79
- - **No configuration-aware resolution:** framework-specific configurations, plugins, TypeScript `paths`, and bundler aliases are not resolved.
80
- - **Static specifiers only:** non-literal dynamic `import()`, template-literal specifiers, `require.resolve()`, and property calls such as `obj.require()` are not recognized as usage evidence.
81
- - **Limited script detection:** script usage relies on supported command patterns, and only the executables in the static allowlist above are translated to their declaring package, and only when the executable appears in a supported direct command position. Every other executable name keeps exact-name matching, mappings are never applied to runner arguments such as `npx tsc` or `pnpm exec tsc`, and ambiguous provider families such as `tailwindcss`, `playwright`, and `webpack` are deliberately unmapped, so only an exactly declared same-name package is credited.
82
- - **Fixed directory exclusions:** source discovery uses the built-in ignored-directory list above. `.gitignore` and custom ignore rules are not interpreted.
83
- - **Conservative error handling:** unreadable source files or source content the lexical scanner cannot safely finish scanning, such as unterminated strings, comments, templates, or regular expressions, abort the scan rather than producing partial results.
84
- - **No remediation features yet:** automatic fixes, custom configuration files, and per-dependency explanations are not available.
66
+ ## More
85
67
 
86
- ## License
68
+ [Technical reference](https://github.com/MatiasRicardi/depvitals/blob/main/docs/reference.md) · [Contributing](https://github.com/MatiasRicardi/depvitals/blob/main/CONTRIBUTING.md) · [Changelog](https://github.com/MatiasRicardi/depvitals/blob/main/CHANGELOG.md) · [npm](https://www.npmjs.com/package/depvitals) · [Repository](https://github.com/MatiasRicardi/depvitals)
87
69
 
88
- MIT
70
+ [MIT license](https://github.com/MatiasRicardi/depvitals/blob/main/LICENSE).
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Process-facing CLI orchestration (Step 67).
3
+ *
4
+ * `runCliEntry` is the layer between the pure {@link runCli} orchestration and the real process
5
+ * streams. It owns exactly three things:
6
+ *
7
+ * 1. render the deterministic result: stdout first, then stderr, byte for byte as `runCli` produced
8
+ * them - the report and the operational diagnostics are untouched by this module;
9
+ * 2. run the *optional* update check **after** that output, and append its notice to stderr only;
10
+ * 3. hand back the original exit code.
11
+ *
12
+ * The ordering above is the contract: a scan always finishes printing before the notifier may write,
13
+ * and the notifier can never rewrite, delay or contradict that output. It also cannot change the exit
14
+ * status, because the returned code comes from `runCli` alone and the notice is only appended text.
15
+ *
16
+ * Nothing here reads `process`: the streams, the environment, the working directory and the notifier
17
+ * are all injected, so the sequencing is testable without spawning anything and without network
18
+ * access. The thin executable adapter in `index.ts` supplies the real process objects and assigns
19
+ * `process.exitCode` **after** this function resolves, which is what keeps the optional asynchronous
20
+ * work from being truncated (and `process.exit()` is never called at all).
21
+ *
22
+ * The command kind handed to the notifier is deliberately only a literal string: no project path,
23
+ * manifest content or dependency name ever crosses into notifier code.
24
+ */
25
+ import { type UpdateNoticeCollector, type UpdateNoticeRequest } from './notifier/updateCheck.js';
26
+ import { type CliOptions } from './runCli.js';
27
+ /**
28
+ * The minimal stream view this layer needs: a writable text sink plus its interactivity flag.
29
+ * `process.stdout` and `process.stderr` satisfy it, and a test double satisfies it too, which keeps
30
+ * the TTY gate injectable without ever faking a real terminal.
31
+ */
32
+ export type StreamWriter = {
33
+ readonly isTTY?: boolean | undefined;
34
+ write(chunk: string): unknown;
35
+ };
36
+ /** Everything the entry layer needs from the process, injected. */
37
+ export type CliEntryOptions = CliOptions & {
38
+ /** The environment map used for the notifier eligibility gates, e.g. `process.env`. */
39
+ readonly env: UpdateNoticeRequest['env'];
40
+ /** The standard output stream. */
41
+ readonly stdout: StreamWriter;
42
+ /** The standard error stream. */
43
+ readonly stderr: StreamWriter;
44
+ /**
45
+ * Notifier seam. Defaults to the production {@link collectUpdateNotice}; tests inject a stub so
46
+ * they stay deterministic and offline while still asserting the real sequencing and channels.
47
+ */
48
+ readonly updateNotice?: UpdateNoticeCollector;
49
+ };
50
+ /**
51
+ * Render one CLI invocation and return the exit code the caller must assign to `process.exitCode`.
52
+ *
53
+ * @param args - The raw arguments after `node` and the script path.
54
+ * @param options - Version, working directory, environment, the two output streams and the
55
+ * optional notifier seam.
56
+ * @returns The unchanged exit code of the scan: 0 for clean/help/version, 1 for findings/errors.
57
+ */
58
+ export declare function runCliEntry(args: readonly string[], options: CliEntryOptions): Promise<number>;
59
+ //# sourceMappingURL=entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.d.ts","sourceRoot":"","sources":["../../src/cli/entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACzB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EAAE,KAAK,UAAU,EAA0B,MAAM,aAAa,CAAC;AAEtE;;;;GAIG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACrC,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC/B,CAAC;AAEF,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG;IACzC,uFAAuF;IACvF,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACzC,kCAAkC;IAClC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,iCAAiC;IACjC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC;CAC/C,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,MAAM,CAAC,CAmBjB"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Process-facing CLI orchestration (Step 67).
3
+ *
4
+ * `runCliEntry` is the layer between the pure {@link runCli} orchestration and the real process
5
+ * streams. It owns exactly three things:
6
+ *
7
+ * 1. render the deterministic result: stdout first, then stderr, byte for byte as `runCli` produced
8
+ * them - the report and the operational diagnostics are untouched by this module;
9
+ * 2. run the *optional* update check **after** that output, and append its notice to stderr only;
10
+ * 3. hand back the original exit code.
11
+ *
12
+ * The ordering above is the contract: a scan always finishes printing before the notifier may write,
13
+ * and the notifier can never rewrite, delay or contradict that output. It also cannot change the exit
14
+ * status, because the returned code comes from `runCli` alone and the notice is only appended text.
15
+ *
16
+ * Nothing here reads `process`: the streams, the environment, the working directory and the notifier
17
+ * are all injected, so the sequencing is testable without spawning anything and without network
18
+ * access. The thin executable adapter in `index.ts` supplies the real process objects and assigns
19
+ * `process.exitCode` **after** this function resolves, which is what keeps the optional asynchronous
20
+ * work from being truncated (and `process.exit()` is never called at all).
21
+ *
22
+ * The command kind handed to the notifier is deliberately only a literal string: no project path,
23
+ * manifest content or dependency name ever crosses into notifier code.
24
+ */
25
+ import { collectUpdateNotice, } from './notifier/updateCheck.js';
26
+ import { parseCliArgs } from './parseCliArgs.js';
27
+ import { runCli } from './runCli.js';
28
+ /**
29
+ * Render one CLI invocation and return the exit code the caller must assign to `process.exitCode`.
30
+ *
31
+ * @param args - The raw arguments after `node` and the script path.
32
+ * @param options - Version, working directory, environment, the two output streams and the
33
+ * optional notifier seam.
34
+ * @returns The unchanged exit code of the scan: 0 for clean/help/version, 1 for findings/errors.
35
+ */
36
+ export async function runCliEntry(args, options) {
37
+ const result = await runCli(args, { version: options.version, cwd: options.cwd });
38
+ // The deterministic output goes out before anything optional happens.
39
+ if (result.stdout.length > 0) {
40
+ options.stdout.write(result.stdout);
41
+ }
42
+ if (result.stderr.length > 0) {
43
+ options.stderr.write(result.stderr);
44
+ }
45
+ const notice = await collectNotice(args, result, options);
46
+ if (notice !== undefined && notice.length > 0) {
47
+ options.stderr.write(notice);
48
+ }
49
+ return result.exitCode;
50
+ }
51
+ /**
52
+ * Ask the notifier for its notice, if this invocation has one.
53
+ *
54
+ * The call is wrapped so that a notifier failing in any way - a thrown error, a rejected promise, a
55
+ * malformed return value - behaves exactly like "no notice": the scan's own status and diagnostics
56
+ * stand on their own. An unexpected type is discarded rather than printed.
57
+ */
58
+ async function collectNotice(args, result, options) {
59
+ try {
60
+ const collect = options.updateNotice ?? collectUpdateNotice;
61
+ const notice = await collect({
62
+ commandKind: commandKindOf(args, options.cwd, result),
63
+ currentVersion: options.version,
64
+ env: options.env,
65
+ stdoutTTY: options.stdout.isTTY === true,
66
+ stderrTTY: options.stderr.isTTY === true,
67
+ });
68
+ return typeof notice === 'string' ? notice : undefined;
69
+ }
70
+ catch {
71
+ // An optional notification never becomes a failure of its own.
72
+ return undefined;
73
+ }
74
+ }
75
+ /**
76
+ * Classify the invocation in the vocabulary the eligibility predicate expects.
77
+ *
78
+ * The argument parser is the single source of truth for `help`, `version` and usage errors, so it is
79
+ * consulted again here (a pure, deterministic function of the same arguments and the same working
80
+ * directory) instead of widening {@link CliResult}, whose three keys are pinned by the contract gate.
81
+ * A parsed `scan` is then split by what the scan produced: {@link runCli} renders a report whenever
82
+ * the scan completed - clean or with findings - and leaves stdout empty for an operational failure,
83
+ * so an empty report means the scan never finished and nothing may be suggested to the user.
84
+ *
85
+ * Only the literal kind crosses the boundary; the parsed project path stays here and is never passed
86
+ * into notifier code.
87
+ */
88
+ function commandKindOf(args, cwd, result) {
89
+ const command = parseCliArgs(args, cwd);
90
+ if (command.kind !== 'scan') {
91
+ return command.kind;
92
+ }
93
+ return result.stdout.length === 0 ? 'scan-failure' : 'scan';
94
+ }
95
+ //# sourceMappingURL=entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entry.js","sourceRoot":"","sources":["../../src/cli/entry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EACL,mBAAmB,GAGpB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAmC,MAAM,EAAE,MAAM,aAAa,CAAC;AA2BtE;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,IAAuB,EACvB,OAAwB;IAExB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IAElF,sEAAsE;IACtE,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1D,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC9C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,OAAO,MAAM,CAAC,QAAQ,CAAC;AACzB,CAAC;AAED;;;;;;GAMG;AACH,KAAK,UAAU,aAAa,CAC1B,IAAuB,EACvB,MAAiB,EACjB,OAAwB;IAExB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,OAAO,CAAC,YAAY,IAAI,mBAAmB,CAAC;QAE5D,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;YAC3B,WAAW,EAAE,aAAa,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC;YACrD,cAAc,EAAE,OAAO,CAAC,OAAO;YAC/B,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI;YACxC,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI;SACzC,CAAC,CAAC;QAEH,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;QAC/D,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,SAAS,aAAa,CACpB,IAAuB,EACvB,GAAW,EACX,MAAiB;IAEjB,MAAM,OAAO,GAAG,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAExC,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC,IAAI,CAAC;IACtB,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9D,CAAC"}
package/dist/cli/index.js CHANGED
@@ -1,31 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from 'node:fs';
3
- import { runCli } from './runCli.js';
3
+ import { runCliEntry } from './entry.js';
4
4
  /**
5
- * Thin executable adapter for the `v0.1.0` CLI.
5
+ * Thin executable adapter for the CLI.
6
6
  *
7
7
  * Its only responsibilities are:
8
8
  * 1. resolve the package version from the single source of truth (`package.json`);
9
- * 2. collect `process.argv`, `process.cwd()` and the version;
10
- * 3. await the pure orchestration in `runCli`;
11
- * 4. render the returned stdout/stderr;
12
- * 5. assign `process.exitCode` (never call `process.exit()`).
9
+ * 2. hand `process.argv`, `process.cwd()`, `process.env` and the two output streams to
10
+ * `runCliEntry`, which prints the report, then optionally appends the update notice to stderr;
11
+ * 3. assign `process.exitCode` from the returned status (never call `process.exit()`, so the
12
+ * optional update check is never truncated and can never change the status).
13
13
  *
14
- * All argument handling, scanning orchestration and error mapping live in `runCli`, so this file
15
- * only wires process I/O to that orchestration. The relative path works both from the TypeScript
16
- * source (`src/cli/index.ts`) and from the built output (`dist/cli/index.js`), because both resolve
17
- * to the repository root `package.json`.
14
+ * All argument handling, scanning orchestration, error mapping and the notifier sequencing live in
15
+ * `runCli`/`runCliEntry`, so this file only wires process objects to them. The relative path works
16
+ * both from the TypeScript source (`src/cli/index.ts`) and from the built output
17
+ * (`dist/cli/index.js`), because both resolve to the repository root `package.json`.
18
18
  */
19
19
  const version = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
20
- const result = await runCli(process.argv.slice(2), {
20
+ process.exitCode = await runCliEntry(process.argv.slice(2), {
21
21
  version: version.version,
22
22
  cwd: process.cwd(),
23
+ env: process.env,
24
+ stdout: process.stdout,
25
+ stderr: process.stderr,
23
26
  });
24
- if (result.stdout.length > 0) {
25
- process.stdout.write(result.stdout);
26
- }
27
- if (result.stderr.length > 0) {
28
- process.stderr.write(result.stderr);
29
- }
30
- process.exitCode = result.exitCode;
31
27
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,YAAY,CAAC,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAC9C,CAAC;AAEzB,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACjD,OAAO,EAAE,OAAO,CAAC,OAAO;IACxB,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;CACnB,CAAC,CAAC;AAEH,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC;AAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;IAC7B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AACtC,CAAC;AAED,OAAO,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;;;;;;;;;GAcG;AACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CACxB,YAAY,CAAC,IAAI,GAAG,CAAC,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAC9C,CAAC;AAEzB,OAAO,CAAC,QAAQ,GAAG,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC1D,OAAO,EAAE,OAAO,CAAC,OAAO;IACxB,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;IAClB,GAAG,EAAE,OAAO,CAAC,GAAG;IAChB,MAAM,EAAE,OAAO,CAAC,MAAM;IACtB,MAAM,EAAE,OAAO,CAAC,MAAM;CACvB,CAAC,CAAC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pure eligibility predicate for the optional CLI update check (Step 64).
3
+ *
4
+ * The update-notifier contract allows a *newer DepVitals version* notice to be printed to stderr
5
+ * only after a normal, completed scan in an interactive terminal. Everything before that network
6
+ * convenience is decided here, so the gates are auditable in one place:
7
+ *
8
+ * 1. a completed normal scan invocation (clean **or** with findings) - not help, not version,
9
+ * not an argument error, not a failed scan;
10
+ * 2. both stdout and stderr are interactive TTYs (a pipe or redirection on either stream skips);
11
+ * 3. no reliable CI flag;
12
+ * 4. no explicit `DEPVITALS_NO_UPDATE_CHECK=1` opt-out.
13
+ *
14
+ * An ineligible invocation must trigger **no** registry lookup, **no** cache read and **no**
15
+ * notice, so this module deliberately has no imports: it touches no filesystem, no network, no
16
+ * cache, no clock, no output stream and no ambient global state. Every input is passed in by the
17
+ * caller (an environment map, two booleans and the command kind), which keeps the decision
18
+ * deterministic, offline and unit-testable without any real global state.
19
+ *
20
+ * The decision is safe by default: anything unknown or unrecognized resolves to `false`, and no
21
+ * input can make this function throw.
22
+ */
23
+ /**
24
+ * The environment map to consult, shaped exactly like the ambient environment the CLI entry point
25
+ * already holds. Injecting it (instead of reading globals) is what makes every gate testable with
26
+ * arbitrary, hostile or partially known values.
27
+ */
28
+ export type NotifierEnv = Readonly<Record<string, string | undefined>>;
29
+ /**
30
+ * What the CLI is about to do, expressed in the same vocabulary as the argument parser plus the
31
+ * one outcome the parser cannot know:
32
+ *
33
+ * - `scan` - a normal scan invocation that completed (clean or with findings; findings alone set
34
+ * exit code 1 and stay eligible);
35
+ * - `help` / `version` - standalone meta options that never scan;
36
+ * - `error` - an argument error from {@link parseCliArgs}, i.e. nothing was scanned;
37
+ * - `scan-failure` - the scan started but failed operationally, so no report was produced.
38
+ */
39
+ export type NotifierCommandKind = 'scan' | 'help' | 'version' | 'error' | 'scan-failure';
40
+ /** Everything the decision depends on, with no implicit global reads. */
41
+ export type NotifierEligibilityInput = {
42
+ /** Environment map to consult, e.g. the caller's own environment object. */
43
+ readonly env: NotifierEnv;
44
+ /**
45
+ * Whether the standard output stream is an interactive terminal. Node reports a pipe or a
46
+ * redirection as non-interactive (its `isTTY` flag is absent), and any falsy value here is
47
+ * ineligible, so the caller only has to pass the stream's own flag.
48
+ */
49
+ readonly stdoutTTY: boolean;
50
+ /** Whether the error output stream is an interactive terminal, with the same rule as stdout. */
51
+ readonly stderrTTY: boolean;
52
+ /** Which command the CLI completed. Only a completed scan can ever be eligible. */
53
+ readonly commandKind: NotifierCommandKind;
54
+ };
55
+ /**
56
+ * Decide whether a human interactive invocation may attempt the optional update check.
57
+ *
58
+ * Returns `true` only when every gate in the contract's eligibility order passes. `false` means
59
+ * the caller must not touch the network, must not read the cache and must not print a notice.
60
+ *
61
+ * @param input - The injected environment map, the two stream TTY flags and the command kind.
62
+ * @returns `true` only for an interactive, non-CI, non-opted-out completed normal scan.
63
+ */
64
+ export declare function isEligibleUpdateCheck(input: NotifierEligibilityInput): boolean;
65
+ //# sourceMappingURL=eligibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eligibility.d.ts","sourceRoot":"","sources":["../../../src/cli/notifier/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEvE;;;;;;;;;GASG;AACH,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,cAAc,CAAC;AAEzF,yEAAyE;AACzE,MAAM,MAAM,wBAAwB,GAAG;IACrC,4EAA4E;IAC5E,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAC;IAC1B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,mFAAmF;IACnF,QAAQ,CAAC,WAAW,EAAE,mBAAmB,CAAC;CAC3C,CAAC;AA0FF;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,wBAAwB,GAAG,OAAO,CAe9E"}
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Pure eligibility predicate for the optional CLI update check (Step 64).
3
+ *
4
+ * The update-notifier contract allows a *newer DepVitals version* notice to be printed to stderr
5
+ * only after a normal, completed scan in an interactive terminal. Everything before that network
6
+ * convenience is decided here, so the gates are auditable in one place:
7
+ *
8
+ * 1. a completed normal scan invocation (clean **or** with findings) - not help, not version,
9
+ * not an argument error, not a failed scan;
10
+ * 2. both stdout and stderr are interactive TTYs (a pipe or redirection on either stream skips);
11
+ * 3. no reliable CI flag;
12
+ * 4. no explicit `DEPVITALS_NO_UPDATE_CHECK=1` opt-out.
13
+ *
14
+ * An ineligible invocation must trigger **no** registry lookup, **no** cache read and **no**
15
+ * notice, so this module deliberately has no imports: it touches no filesystem, no network, no
16
+ * cache, no clock, no output stream and no ambient global state. Every input is passed in by the
17
+ * caller (an environment map, two booleans and the command kind), which keeps the decision
18
+ * deterministic, offline and unit-testable without any real global state.
19
+ *
20
+ * The decision is safe by default: anything unknown or unrecognized resolves to `false`, and no
21
+ * input can make this function throw.
22
+ */
23
+ /** The only supported opt-out variable; see the update-notifier contract. */
24
+ const NO_UPDATE_CHECK_VARIABLE = 'DEPVITALS_NO_UPDATE_CHECK';
25
+ /** The opt-out is honored for exactly this value and for no other. */
26
+ const NO_UPDATE_CHECK_VALUE = '1';
27
+ /**
28
+ * The explicit list of environment variables that reliably mean "this is a non-interactive
29
+ * continuous-integration run". Matching is by exact variable name, never by substring, so
30
+ * unrelated variables such as `MY_CI`, `CIRCLE_CI` or `GITLAB` are ignored.
31
+ */
32
+ const CI_VARIABLES = [
33
+ 'CI',
34
+ 'GITHUB_ACTIONS',
35
+ 'GITLAB_CI',
36
+ 'TF_BUILD',
37
+ 'CIRCLECI',
38
+ 'TRAVIS',
39
+ 'BUILDKITE',
40
+ 'JENKINS_URL',
41
+ 'TEAMCITY_VERSION',
42
+ 'APPVEYOR',
43
+ ];
44
+ /**
45
+ * Values that explicitly cancel a CI flag. Providers write real identifiers into these variables
46
+ * (`CI=TeamCity`, `TF_BUILD=True`), so a value that is present, non-empty and not one of these
47
+ * tokens counts as CI. Skipping on an unrecognized value is the conservative direction: it costs a
48
+ * missing notice, never an unexpected request or an extra line in a CI log.
49
+ */
50
+ const CANCELLED_VALUES = new Set(['', '0', 'false', 'no', 'off']);
51
+ /**
52
+ * Read one variable from the injected environment map.
53
+ */
54
+ function read(env, name) {
55
+ return env[name];
56
+ }
57
+ /**
58
+ * Whether a CI variable is present with a value that is not an explicit cancellation. Absent and
59
+ * empty values are not CI; comparison of cancellation tokens is case-insensitive.
60
+ */
61
+ function isSet(value) {
62
+ if (value === undefined) {
63
+ return false;
64
+ }
65
+ return !CANCELLED_VALUES.has(value.toLowerCase());
66
+ }
67
+ /**
68
+ * Whether any recognized CI variable reports a CI run.
69
+ */
70
+ function isCiRun(env) {
71
+ return CI_VARIABLES.some((name) => isSet(read(env, name)));
72
+ }
73
+ /**
74
+ * Whether the user opted out. `DEPVITALS_NO_UPDATE_CHECK` disables the update check if and only if
75
+ * it equals exactly `"1"`: `0`, ``, `2`, `true`, `01` and padded values do not opt out, and no
76
+ * broader truthy handling is implemented or implied.
77
+ */
78
+ function hasOptedOut(env) {
79
+ return read(env, NO_UPDATE_CHECK_VARIABLE) === NO_UPDATE_CHECK_VALUE;
80
+ }
81
+ /**
82
+ * Whether the invocation is a completed normal scan. `help`, `version`, argument errors and failed
83
+ * scans are excluded; a scan that finished with findings is included, because findings are a
84
+ * successful deterministic result and only change the exit code.
85
+ */
86
+ function isCompletedScan(commandKind) {
87
+ switch (commandKind) {
88
+ case 'scan':
89
+ return true;
90
+ case 'help':
91
+ case 'version':
92
+ case 'error':
93
+ case 'scan-failure':
94
+ return false;
95
+ default:
96
+ // A value outside the declared union can only come from an untyped caller; unknown work is
97
+ // not a completed scan, so the safe default applies here too.
98
+ return false;
99
+ }
100
+ }
101
+ /**
102
+ * Decide whether a human interactive invocation may attempt the optional update check.
103
+ *
104
+ * Returns `true` only when every gate in the contract's eligibility order passes. `false` means
105
+ * the caller must not touch the network, must not read the cache and must not print a notice.
106
+ *
107
+ * @param input - The injected environment map, the two stream TTY flags and the command kind.
108
+ * @returns `true` only for an interactive, non-CI, non-opted-out completed normal scan.
109
+ */
110
+ export function isEligibleUpdateCheck(input) {
111
+ if (!isCompletedScan(input.commandKind)) {
112
+ return false;
113
+ }
114
+ // Both streams must be interactive; a pipe or redirection on either one suppresses the check.
115
+ if (!input.stdoutTTY || !input.stderrTTY) {
116
+ return false;
117
+ }
118
+ if (isCiRun(input.env)) {
119
+ return false;
120
+ }
121
+ return !hasOptedOut(input.env);
122
+ }
123
+ //# sourceMappingURL=eligibility.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eligibility.js","sourceRoot":"","sources":["../../../src/cli/notifier/eligibility.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAqCH,6EAA6E;AAC7E,MAAM,wBAAwB,GAAG,2BAA2B,CAAC;AAE7D,sEAAsE;AACtE,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAElC;;;;GAIG;AACH,MAAM,YAAY,GAAsB;IACtC,IAAI;IACJ,gBAAgB;IAChB,WAAW;IACX,UAAU;IACV,UAAU;IACV,QAAQ;IACR,WAAW;IACX,aAAa;IACb,kBAAkB;IAClB,UAAU;CACX,CAAC;AAEF;;;;;GAKG;AACH,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AAEvF;;GAEG;AACH,SAAS,IAAI,CAAC,GAAgB,EAAE,IAAY;IAC1C,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC;AACnB,CAAC;AAED;;;GAGG;AACH,SAAS,KAAK,CAAC,KAAyB;IACtC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;AACpD,CAAC;AAED;;GAEG;AACH,SAAS,OAAO,CAAC,GAAgB;IAC/B,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,GAAgB;IACnC,OAAO,IAAI,CAAC,GAAG,EAAE,wBAAwB,CAAC,KAAK,qBAAqB,CAAC;AACvE,CAAC;AAED;;;;GAIG;AACH,SAAS,eAAe,CAAC,WAAgC;IACvD,QAAQ,WAAW,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,OAAO,IAAI,CAAC;QACd,KAAK,MAAM,CAAC;QACZ,KAAK,SAAS,CAAC;QACf,KAAK,OAAO,CAAC;QACb,KAAK,cAAc;YACjB,OAAO,KAAK,CAAC;QACf;YACE,2FAA2F;YAC3F,8DAA8D;YAC9D,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAA+B;IACnE,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;QACxC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,8FAA8F;IAC9F,IAAI,CAAC,KAAK,CAAC,SAAS,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,CAAC;QACzC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC"}