@rainy-updates/cli 0.5.1 → 0.5.2-rc.1

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 (42) hide show
  1. package/CHANGELOG.md +68 -1
  2. package/README.md +84 -25
  3. package/dist/bin/cli.js +30 -0
  4. package/dist/commands/audit/mapper.js +1 -1
  5. package/dist/commands/licenses/parser.d.ts +2 -0
  6. package/dist/commands/licenses/parser.js +116 -0
  7. package/dist/commands/licenses/runner.d.ts +9 -0
  8. package/dist/commands/licenses/runner.js +163 -0
  9. package/dist/commands/licenses/sbom.d.ts +10 -0
  10. package/dist/commands/licenses/sbom.js +70 -0
  11. package/dist/commands/resolve/graph/builder.d.ts +20 -0
  12. package/dist/commands/resolve/graph/builder.js +183 -0
  13. package/dist/commands/resolve/graph/conflict.d.ts +20 -0
  14. package/dist/commands/resolve/graph/conflict.js +52 -0
  15. package/dist/commands/resolve/graph/resolver.d.ts +17 -0
  16. package/dist/commands/resolve/graph/resolver.js +71 -0
  17. package/dist/commands/resolve/parser.d.ts +2 -0
  18. package/dist/commands/resolve/parser.js +89 -0
  19. package/dist/commands/resolve/runner.d.ts +13 -0
  20. package/dist/commands/resolve/runner.js +136 -0
  21. package/dist/commands/snapshot/parser.d.ts +2 -0
  22. package/dist/commands/snapshot/parser.js +80 -0
  23. package/dist/commands/snapshot/runner.d.ts +11 -0
  24. package/dist/commands/snapshot/runner.js +115 -0
  25. package/dist/commands/snapshot/store.d.ts +35 -0
  26. package/dist/commands/snapshot/store.js +158 -0
  27. package/dist/commands/unused/matcher.d.ts +22 -0
  28. package/dist/commands/unused/matcher.js +95 -0
  29. package/dist/commands/unused/parser.d.ts +2 -0
  30. package/dist/commands/unused/parser.js +95 -0
  31. package/dist/commands/unused/runner.d.ts +11 -0
  32. package/dist/commands/unused/runner.js +113 -0
  33. package/dist/commands/unused/scanner.d.ts +18 -0
  34. package/dist/commands/unused/scanner.js +129 -0
  35. package/dist/core/impact.d.ts +36 -0
  36. package/dist/core/impact.js +82 -0
  37. package/dist/core/options.d.ts +13 -1
  38. package/dist/core/options.js +35 -13
  39. package/dist/types/index.d.ts +153 -0
  40. package/dist/utils/semver.d.ts +18 -0
  41. package/dist/utils/semver.js +88 -3
  42. package/package.json +1 -1
@@ -0,0 +1,2 @@
1
+ import type { SnapshotOptions } from "../../types/index.js";
2
+ export declare function parseSnapshotArgs(args: string[]): SnapshotOptions;
@@ -0,0 +1,80 @@
1
+ const VALID_ACTIONS = ["save", "list", "restore", "diff"];
2
+ export function parseSnapshotArgs(args) {
3
+ const options = {
4
+ cwd: process.cwd(),
5
+ workspace: false,
6
+ action: "list",
7
+ label: undefined,
8
+ snapshotId: undefined,
9
+ storeFile: undefined,
10
+ };
11
+ // First positional arg is the action
12
+ let positionalIndex = 0;
13
+ const positionals = [];
14
+ for (let i = 0; i < args.length; i++) {
15
+ const current = args[i];
16
+ const next = args[i + 1];
17
+ if (current === "--cwd" && next) {
18
+ options.cwd = next;
19
+ i++;
20
+ continue;
21
+ }
22
+ if (current === "--cwd")
23
+ throw new Error("Missing value for --cwd");
24
+ if (current === "--workspace") {
25
+ options.workspace = true;
26
+ continue;
27
+ }
28
+ if (current === "--label" && next) {
29
+ options.label = next;
30
+ i++;
31
+ continue;
32
+ }
33
+ if (current === "--label")
34
+ throw new Error("Missing value for --label");
35
+ if (current === "--store" && next) {
36
+ options.storeFile = next;
37
+ i++;
38
+ continue;
39
+ }
40
+ if (current === "--store")
41
+ throw new Error("Missing value for --store");
42
+ if (current === "--help" || current === "-h") {
43
+ process.stdout.write(SNAPSHOT_HELP);
44
+ process.exit(0);
45
+ }
46
+ if (current.startsWith("-"))
47
+ throw new Error(`Unknown option: ${current}`);
48
+ // Positional arguments
49
+ positionals.push(current);
50
+ }
51
+ // positionals[0] = action, positionals[1] = id/label for restore/diff
52
+ if (positionals.length >= 1) {
53
+ const action = positionals[0];
54
+ if (!VALID_ACTIONS.includes(action)) {
55
+ throw new Error(`Unknown snapshot action: "${positionals[0]}". Valid: ${VALID_ACTIONS.join(", ")}`);
56
+ }
57
+ options.action = action;
58
+ }
59
+ if (positionals.length >= 2) {
60
+ // For restore/diff the second positional is the id/label
61
+ options.snapshotId = positionals[1];
62
+ }
63
+ return options;
64
+ }
65
+ const SNAPSHOT_HELP = `
66
+ rup snapshot — Save, list, restore, and diff dependency state snapshots
67
+
68
+ Usage:
69
+ rup snapshot save [--label <name>] Save current dependency state
70
+ rup snapshot list List all saved snapshots
71
+ rup snapshot restore <id|label> Restore package.json files from a snapshot
72
+ rup snapshot diff <id|label> Show changes since a snapshot
73
+
74
+ Options:
75
+ --label <name> Human-readable label for the snapshot
76
+ --store <path> Custom snapshot store file (default: .rup-snapshots.json)
77
+ --workspace Include all workspace packages
78
+ --cwd <path> Working directory (default: cwd)
79
+ --help Show this help
80
+ `.trimStart();
@@ -0,0 +1,11 @@
1
+ import type { SnapshotOptions, SnapshotResult } from "../../types/index.js";
2
+ /**
3
+ * Entry point for `rup snapshot`. Lazy-loaded by cli.ts.
4
+ *
5
+ * Actions:
6
+ * save — Capture package.json + lockfile state → store
7
+ * list — Print all saved snapshots
8
+ * restore — Restore a snapshot by id or label
9
+ * diff — Show dependency changes since a snapshot
10
+ */
11
+ export declare function runSnapshot(options: SnapshotOptions): Promise<SnapshotResult>;
@@ -0,0 +1,115 @@
1
+ import process from "node:process";
2
+ import { discoverPackageDirs } from "../../workspace/discover.js";
3
+ import { SnapshotStore, captureState, restoreState, diffManifests, } from "./store.js";
4
+ /**
5
+ * Entry point for `rup snapshot`. Lazy-loaded by cli.ts.
6
+ *
7
+ * Actions:
8
+ * save — Capture package.json + lockfile state → store
9
+ * list — Print all saved snapshots
10
+ * restore — Restore a snapshot by id or label
11
+ * diff — Show dependency changes since a snapshot
12
+ */
13
+ export async function runSnapshot(options) {
14
+ const result = {
15
+ action: options.action,
16
+ errors: [],
17
+ warnings: [],
18
+ };
19
+ const packageDirs = await discoverPackageDirs(options.cwd, options.workspace);
20
+ const store = new SnapshotStore(options.cwd, options.storeFile);
21
+ switch (options.action) {
22
+ // ─ save ──────────────────────────────────────────────────────────────────
23
+ case "save": {
24
+ const label = options.label ??
25
+ `snap-${new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19)}`;
26
+ const { manifests, lockfileHashes } = await captureState(packageDirs);
27
+ const entry = await store.saveSnapshot(manifests, lockfileHashes, label);
28
+ result.snapshotId = entry.id;
29
+ result.label = entry.label;
30
+ process.stdout.write(`✔ Snapshot saved: ${entry.label} (${entry.id})\n`);
31
+ break;
32
+ }
33
+ // ─ list ──────────────────────────────────────────────────────────────────
34
+ case "list": {
35
+ const entries = await store.listSnapshots();
36
+ result.entries = entries.map((e) => ({
37
+ id: e.id,
38
+ label: e.label,
39
+ createdAt: new Date(e.createdAt).toISOString(),
40
+ }));
41
+ if (entries.length === 0) {
42
+ process.stdout.write("No snapshots saved yet. Use `rup snapshot save` to create one.\n");
43
+ }
44
+ else {
45
+ process.stdout.write(`\n${entries.length} snapshot(s):\n\n`);
46
+ process.stdout.write(" " + "ID".padEnd(30) + "Label".padEnd(30) + "Created\n");
47
+ process.stdout.write(" " + "─".repeat(75) + "\n");
48
+ for (const e of entries) {
49
+ process.stdout.write(" " +
50
+ e.id.padEnd(30) +
51
+ e.label.padEnd(30) +
52
+ new Date(e.createdAt).toLocaleString() +
53
+ "\n");
54
+ }
55
+ process.stdout.write("\n");
56
+ }
57
+ break;
58
+ }
59
+ // ─ restore ───────────────────────────────────────────────────────────────
60
+ case "restore": {
61
+ const idOrLabel = options.snapshotId ?? options.label;
62
+ if (!idOrLabel) {
63
+ result.errors.push("Restore requires a snapshot ID or label. Usage: rup snapshot restore <id|label>");
64
+ break;
65
+ }
66
+ const entry = await store.findSnapshot(idOrLabel);
67
+ if (!entry) {
68
+ result.errors.push(`Snapshot not found: ${idOrLabel}. Use \`rup snapshot list\` to view saved snapshots.`);
69
+ break;
70
+ }
71
+ await restoreState(entry);
72
+ result.snapshotId = entry.id;
73
+ result.label = entry.label;
74
+ const count = Object.keys(entry.manifests).length;
75
+ process.stdout.write(`✔ Restored ${count} package.json file(s) from snapshot "${entry.label}" (${entry.id})\n`);
76
+ process.stdout.write(" Re-run your package manager install to apply.\n");
77
+ break;
78
+ }
79
+ // ─ diff ──────────────────────────────────────────────────────────────────
80
+ case "diff": {
81
+ const idOrLabel = options.snapshotId ?? options.label;
82
+ if (!idOrLabel) {
83
+ result.errors.push("Diff requires a snapshot ID or label. Usage: rup snapshot diff <id|label>");
84
+ break;
85
+ }
86
+ const entry = await store.findSnapshot(idOrLabel);
87
+ if (!entry) {
88
+ result.errors.push(`Snapshot not found: ${idOrLabel}`);
89
+ break;
90
+ }
91
+ const { manifests: currentManifests } = await captureState(packageDirs);
92
+ const changes = diffManifests(entry.manifests, currentManifests);
93
+ result.diff = changes;
94
+ if (changes.length === 0) {
95
+ process.stdout.write(`✔ No dependency changes since snapshot "${entry.label}"\n`);
96
+ }
97
+ else {
98
+ process.stdout.write(`\nDependency changes since snapshot "${entry.label}":\n\n`);
99
+ process.stdout.write(" " + "Package".padEnd(35) + "Before".padEnd(20) + "After\n");
100
+ process.stdout.write(" " + "─".repeat(65) + "\n");
101
+ for (const c of changes) {
102
+ process.stdout.write(" " + c.name.padEnd(35) + c.from.padEnd(20) + c.to + "\n");
103
+ }
104
+ process.stdout.write("\n");
105
+ }
106
+ break;
107
+ }
108
+ }
109
+ if (result.errors.length > 0) {
110
+ for (const err of result.errors) {
111
+ process.stderr.write(`[snapshot] ✖ ${err}\n`);
112
+ }
113
+ }
114
+ return result;
115
+ }
@@ -0,0 +1,35 @@
1
+ import type { SnapshotEntry } from "../../types/index.js";
2
+ /**
3
+ * Lightweight SQLite-free snapshot store (uses a JSON file in the project root).
4
+ *
5
+ * Design goals:
6
+ * - No extra runtime dependencies (SQLite bindings vary by runtime)
7
+ * - Human-readable store file (git-committable if desired)
8
+ * - Atomic writes via tmp-rename to prevent corruption
9
+ * - Fast: entire store fits in memory for typical use (< 50 snapshots)
10
+ */
11
+ export declare class SnapshotStore {
12
+ private readonly storePath;
13
+ private entries;
14
+ private loaded;
15
+ constructor(cwd: string, storeFile?: string);
16
+ load(): Promise<void>;
17
+ save(): Promise<void>;
18
+ saveSnapshot(manifests: Record<string, string>, lockfileHashes: Record<string, string>, label: string): Promise<SnapshotEntry>;
19
+ listSnapshots(): Promise<SnapshotEntry[]>;
20
+ findSnapshot(idOrLabel: string): Promise<SnapshotEntry | null>;
21
+ deleteSnapshot(idOrLabel: string): Promise<boolean>;
22
+ }
23
+ /** Captures current package.json and lockfile state for a set of directories. */
24
+ export declare function captureState(packageDirs: string[]): Promise<{
25
+ manifests: Record<string, string>;
26
+ lockfileHashes: Record<string, string>;
27
+ }>;
28
+ /** Restores package.json files from a snapshot's manifest map. */
29
+ export declare function restoreState(entry: SnapshotEntry): Promise<void>;
30
+ /** Computes a diff of dependency versions between two manifest snapshots. */
31
+ export declare function diffManifests(before: Record<string, string>, after: Record<string, string>): Array<{
32
+ name: string;
33
+ from: string;
34
+ to: string;
35
+ }>;
@@ -0,0 +1,158 @@
1
+ import { createHash } from "node:crypto";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ const DEFAULT_STORE_NAME = ".rup-snapshots.json";
5
+ /**
6
+ * Lightweight SQLite-free snapshot store (uses a JSON file in the project root).
7
+ *
8
+ * Design goals:
9
+ * - No extra runtime dependencies (SQLite bindings vary by runtime)
10
+ * - Human-readable store file (git-committable if desired)
11
+ * - Atomic writes via tmp-rename to prevent corruption
12
+ * - Fast: entire store fits in memory for typical use (< 50 snapshots)
13
+ */
14
+ export class SnapshotStore {
15
+ storePath;
16
+ entries = [];
17
+ loaded = false;
18
+ constructor(cwd, storeFile) {
19
+ this.storePath = storeFile
20
+ ? path.isAbsolute(storeFile)
21
+ ? storeFile
22
+ : path.join(cwd, storeFile)
23
+ : path.join(cwd, DEFAULT_STORE_NAME);
24
+ }
25
+ async load() {
26
+ if (this.loaded)
27
+ return;
28
+ try {
29
+ const raw = await fs.readFile(this.storePath, "utf8");
30
+ const parsed = JSON.parse(raw);
31
+ if (Array.isArray(parsed)) {
32
+ this.entries = parsed;
33
+ }
34
+ }
35
+ catch {
36
+ this.entries = [];
37
+ }
38
+ this.loaded = true;
39
+ }
40
+ async save() {
41
+ const tmp = this.storePath + ".tmp";
42
+ await fs.writeFile(tmp, JSON.stringify(this.entries, null, 2) + "\n", "utf8");
43
+ await fs.rename(tmp, this.storePath);
44
+ }
45
+ async saveSnapshot(manifests, lockfileHashes, label) {
46
+ await this.load();
47
+ const id = `snap-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
48
+ const entry = {
49
+ id,
50
+ label,
51
+ createdAt: Date.now(),
52
+ manifests,
53
+ lockfileHashes,
54
+ };
55
+ this.entries.push(entry);
56
+ await this.save();
57
+ return entry;
58
+ }
59
+ async listSnapshots() {
60
+ await this.load();
61
+ return [...this.entries].sort((a, b) => b.createdAt - a.createdAt);
62
+ }
63
+ async findSnapshot(idOrLabel) {
64
+ await this.load();
65
+ return (this.entries.find((e) => e.id === idOrLabel || e.label === idOrLabel) ??
66
+ null);
67
+ }
68
+ async deleteSnapshot(idOrLabel) {
69
+ await this.load();
70
+ const before = this.entries.length;
71
+ this.entries = this.entries.filter((e) => e.id !== idOrLabel && e.label !== idOrLabel);
72
+ if (this.entries.length < before) {
73
+ await this.save();
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+ }
79
+ /** Captures current package.json and lockfile state for a set of directories. */
80
+ export async function captureState(packageDirs) {
81
+ const manifests = {};
82
+ const lockfileHashes = {};
83
+ const LOCKFILES = [
84
+ "package-lock.json",
85
+ "pnpm-lock.yaml",
86
+ "yarn.lock",
87
+ "bun.lockb",
88
+ ];
89
+ await Promise.all(packageDirs.map(async (dir) => {
90
+ // Read package.json
91
+ try {
92
+ const content = await fs.readFile(path.join(dir, "package.json"), "utf8");
93
+ manifests[dir] = content;
94
+ }
95
+ catch {
96
+ // No package.json — skip
97
+ }
98
+ // Hash the first lockfile found
99
+ for (const lf of LOCKFILES) {
100
+ try {
101
+ const content = await fs.readFile(path.join(dir, lf));
102
+ lockfileHashes[dir] = createHash("sha256")
103
+ .update(content)
104
+ .digest("hex");
105
+ break;
106
+ }
107
+ catch {
108
+ // Try next
109
+ }
110
+ }
111
+ }));
112
+ return { manifests, lockfileHashes };
113
+ }
114
+ /** Restores package.json files from a snapshot's manifest map. */
115
+ export async function restoreState(entry) {
116
+ await Promise.all(Object.entries(entry.manifests).map(async ([dir, content]) => {
117
+ const manifestPath = path.join(dir, "package.json");
118
+ const tmp = manifestPath + ".tmp";
119
+ await fs.writeFile(tmp, content, "utf8");
120
+ await fs.rename(tmp, manifestPath);
121
+ }));
122
+ }
123
+ /** Computes a diff of dependency versions between two manifest snapshots. */
124
+ export function diffManifests(before, after) {
125
+ const changes = [];
126
+ for (const [dir, afterJson] of Object.entries(after)) {
127
+ const beforeJson = before[dir];
128
+ if (!beforeJson)
129
+ continue;
130
+ let beforeManifest;
131
+ let afterManifest;
132
+ try {
133
+ beforeManifest = JSON.parse(beforeJson);
134
+ afterManifest = JSON.parse(afterJson);
135
+ }
136
+ catch {
137
+ continue;
138
+ }
139
+ const fields = [
140
+ "dependencies",
141
+ "devDependencies",
142
+ "optionalDependencies",
143
+ ];
144
+ for (const field of fields) {
145
+ const before = beforeManifest[field] ?? {};
146
+ const after = afterManifest[field] ?? {};
147
+ const allNames = new Set([...Object.keys(before), ...Object.keys(after)]);
148
+ for (const name of allNames) {
149
+ const fromVer = before[name] ?? "(removed)";
150
+ const toVer = after[name] ?? "(removed)";
151
+ if (fromVer !== toVer) {
152
+ changes.push({ name, from: fromVer, to: toVer });
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return changes;
158
+ }
@@ -0,0 +1,22 @@
1
+ import type { PackageManifest } from "../../types/index.js";
2
+ import type { UnusedDependency } from "../../types/index.js";
3
+ /**
4
+ * Cross-references declared package.json dependencies against imported package names
5
+ * to surface two classes of problems:
6
+ *
7
+ * - "declared-not-imported": in package.json but never imported in source
8
+ * - "imported-not-declared": imported in source but absent from package.json
9
+ */
10
+ export interface MatchOptions {
11
+ includeDevDependencies: boolean;
12
+ }
13
+ export declare function matchDependencies(manifest: PackageManifest, importedPackages: Set<string>, packageDir: string, options: MatchOptions): {
14
+ unused: UnusedDependency[];
15
+ missing: UnusedDependency[];
16
+ };
17
+ /**
18
+ * Applies the unused/missing result of `matchDependencies` to a package.json
19
+ * manifest in-memory, removing `unused` entries. Returns the modified manifest
20
+ * as a formatted JSON string ready to write back to disk.
21
+ */
22
+ export declare function removeUnusedFromManifest(manifestJson: string, unused: UnusedDependency[]): string;
@@ -0,0 +1,95 @@
1
+ export function matchDependencies(manifest, importedPackages, packageDir, options) {
2
+ const declared = buildDeclaredMap(manifest, options);
3
+ const unused = [];
4
+ const missing = [];
5
+ // Find declared deps not seen in source imports
6
+ for (const [name, field] of declared) {
7
+ if (!importedPackages.has(name)) {
8
+ unused.push({
9
+ name,
10
+ kind: "declared-not-imported",
11
+ declaredIn: field,
12
+ });
13
+ }
14
+ }
15
+ // Find imports not declared in package.json
16
+ for (const importedName of importedPackages) {
17
+ if (!declared.has(importedName)) {
18
+ missing.push({
19
+ name: importedName,
20
+ kind: "imported-not-declared",
21
+ importedFrom: packageDir,
22
+ });
23
+ }
24
+ }
25
+ return { unused, missing };
26
+ }
27
+ /**
28
+ * Build a map of { packageName → fieldName } from package.json.
29
+ * Only includes the fields requested (e.g. skip devDependencies when
30
+ * includeDevDependencies is false).
31
+ */
32
+ function buildDeclaredMap(manifest, options) {
33
+ const result = new Map();
34
+ const fields = [
35
+ [
36
+ manifest.dependencies,
37
+ "dependencies",
38
+ ],
39
+ [
40
+ manifest.optionalDependencies,
41
+ "optionalDependencies",
42
+ ],
43
+ ];
44
+ if (options.includeDevDependencies) {
45
+ fields.push([
46
+ manifest.devDependencies,
47
+ "devDependencies",
48
+ ]);
49
+ }
50
+ // peerDependencies are intentionally excluded — they are rarely directly imported
51
+ // and are a separate concern handled by `rup resolve`.
52
+ for (const [deps, fieldName] of fields) {
53
+ if (!deps)
54
+ continue;
55
+ for (const name of Object.keys(deps)) {
56
+ if (!result.has(name)) {
57
+ result.set(name, fieldName);
58
+ }
59
+ }
60
+ }
61
+ return result;
62
+ }
63
+ /**
64
+ * Applies the unused/missing result of `matchDependencies` to a package.json
65
+ * manifest in-memory, removing `unused` entries. Returns the modified manifest
66
+ * as a formatted JSON string ready to write back to disk.
67
+ */
68
+ export function removeUnusedFromManifest(manifestJson, unused) {
69
+ if (unused.length === 0)
70
+ return manifestJson;
71
+ let parsed;
72
+ try {
73
+ parsed = JSON.parse(manifestJson);
74
+ }
75
+ catch {
76
+ return manifestJson;
77
+ }
78
+ const unusedNames = new Set(unused.map((u) => u.name));
79
+ const fields = [
80
+ "dependencies",
81
+ "devDependencies",
82
+ "optionalDependencies",
83
+ ];
84
+ for (const field of fields) {
85
+ const deps = parsed[field];
86
+ if (!deps || typeof deps !== "object")
87
+ continue;
88
+ for (const name of Object.keys(deps)) {
89
+ if (unusedNames.has(name)) {
90
+ delete deps[name];
91
+ }
92
+ }
93
+ }
94
+ return JSON.stringify(parsed, null, 2) + "\n";
95
+ }
@@ -0,0 +1,2 @@
1
+ import type { UnusedOptions } from "../../types/index.js";
2
+ export declare function parseUnusedArgs(args: string[]): UnusedOptions;
@@ -0,0 +1,95 @@
1
+ const DEFAULT_SRC_DIRS = ["src"];
2
+ export function parseUnusedArgs(args) {
3
+ const options = {
4
+ cwd: process.cwd(),
5
+ workspace: false,
6
+ srcDirs: DEFAULT_SRC_DIRS,
7
+ includeDevDependencies: true,
8
+ fix: false,
9
+ dryRun: false,
10
+ jsonFile: undefined,
11
+ concurrency: 16,
12
+ };
13
+ for (let i = 0; i < args.length; i++) {
14
+ const current = args[i];
15
+ const next = args[i + 1];
16
+ if (current === "--cwd" && next) {
17
+ options.cwd = next;
18
+ i++;
19
+ continue;
20
+ }
21
+ if (current === "--cwd")
22
+ throw new Error("Missing value for --cwd");
23
+ if (current === "--workspace") {
24
+ options.workspace = true;
25
+ continue;
26
+ }
27
+ if (current === "--src" && next) {
28
+ options.srcDirs = next
29
+ .split(",")
30
+ .map((s) => s.trim())
31
+ .filter(Boolean);
32
+ i++;
33
+ continue;
34
+ }
35
+ if (current === "--src")
36
+ throw new Error("Missing value for --src");
37
+ if (current === "--no-dev") {
38
+ options.includeDevDependencies = false;
39
+ continue;
40
+ }
41
+ if (current === "--fix") {
42
+ options.fix = true;
43
+ continue;
44
+ }
45
+ if (current === "--dry-run") {
46
+ options.dryRun = true;
47
+ continue;
48
+ }
49
+ if (current === "--json-file" && next) {
50
+ options.jsonFile = next;
51
+ i++;
52
+ continue;
53
+ }
54
+ if (current === "--json-file")
55
+ throw new Error("Missing value for --json-file");
56
+ if (current === "--concurrency" && next) {
57
+ const parsed = Number(next);
58
+ if (!Number.isInteger(parsed) || parsed <= 0)
59
+ throw new Error("--concurrency must be a positive integer");
60
+ options.concurrency = parsed;
61
+ i++;
62
+ continue;
63
+ }
64
+ if (current === "--concurrency")
65
+ throw new Error("Missing value for --concurrency");
66
+ if (current === "--help" || current === "-h") {
67
+ process.stdout.write(UNUSED_HELP);
68
+ process.exit(0);
69
+ }
70
+ if (current.startsWith("-"))
71
+ throw new Error(`Unknown option: ${current}`);
72
+ }
73
+ return options;
74
+ }
75
+ const UNUSED_HELP = `
76
+ rup unused — Detect unused and missing npm dependencies
77
+
78
+ Usage:
79
+ rup unused [options]
80
+
81
+ Options:
82
+ --src <dirs> Comma-separated source directories to scan (default: src)
83
+ --workspace Scan all workspace packages
84
+ --no-dev Exclude devDependencies from unused detection
85
+ --fix Remove unused dependencies from package.json
86
+ --dry-run Preview changes without writing
87
+ --json-file <path> Write JSON report to file
88
+ --cwd <path> Working directory (default: cwd)
89
+ --concurrency <n> Parallel file scanning concurrency (default: 16)
90
+ --help Show this help
91
+
92
+ Exit codes:
93
+ 0 No unused dependencies found
94
+ 1 Unused or missing dependencies detected
95
+ `.trimStart();
@@ -0,0 +1,11 @@
1
+ import type { UnusedOptions, UnusedResult } from "../../types/index.js";
2
+ /**
3
+ * Entry point for `rup unused`. Lazy-loaded by cli.ts.
4
+ *
5
+ * Strategy:
6
+ * 1. Collect all source directories to scan
7
+ * 2. Scan them in parallel for imported package names
8
+ * 3. Cross-reference against package.json declarations
9
+ * 4. Optionally apply --fix (remove unused from package.json)
10
+ */
11
+ export declare function runUnused(options: UnusedOptions): Promise<UnusedResult>;