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

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 (61) hide show
  1. package/CHANGELOG.md +93 -1
  2. package/README.md +88 -25
  3. package/dist/bin/cli.js +50 -1
  4. package/dist/commands/audit/fetcher.d.ts +2 -6
  5. package/dist/commands/audit/fetcher.js +2 -79
  6. package/dist/commands/audit/mapper.d.ts +8 -1
  7. package/dist/commands/audit/mapper.js +106 -10
  8. package/dist/commands/audit/parser.js +36 -2
  9. package/dist/commands/audit/runner.js +179 -15
  10. package/dist/commands/audit/sources/github.d.ts +2 -0
  11. package/dist/commands/audit/sources/github.js +125 -0
  12. package/dist/commands/audit/sources/index.d.ts +6 -0
  13. package/dist/commands/audit/sources/index.js +92 -0
  14. package/dist/commands/audit/sources/osv.d.ts +2 -0
  15. package/dist/commands/audit/sources/osv.js +131 -0
  16. package/dist/commands/audit/sources/types.d.ts +21 -0
  17. package/dist/commands/audit/sources/types.js +1 -0
  18. package/dist/commands/audit/targets.d.ts +20 -0
  19. package/dist/commands/audit/targets.js +314 -0
  20. package/dist/commands/changelog/fetcher.d.ts +9 -0
  21. package/dist/commands/changelog/fetcher.js +130 -0
  22. package/dist/commands/licenses/parser.d.ts +2 -0
  23. package/dist/commands/licenses/parser.js +116 -0
  24. package/dist/commands/licenses/runner.d.ts +9 -0
  25. package/dist/commands/licenses/runner.js +163 -0
  26. package/dist/commands/licenses/sbom.d.ts +10 -0
  27. package/dist/commands/licenses/sbom.js +70 -0
  28. package/dist/commands/resolve/graph/builder.d.ts +20 -0
  29. package/dist/commands/resolve/graph/builder.js +183 -0
  30. package/dist/commands/resolve/graph/conflict.d.ts +20 -0
  31. package/dist/commands/resolve/graph/conflict.js +52 -0
  32. package/dist/commands/resolve/graph/resolver.d.ts +17 -0
  33. package/dist/commands/resolve/graph/resolver.js +71 -0
  34. package/dist/commands/resolve/parser.d.ts +2 -0
  35. package/dist/commands/resolve/parser.js +89 -0
  36. package/dist/commands/resolve/runner.d.ts +13 -0
  37. package/dist/commands/resolve/runner.js +136 -0
  38. package/dist/commands/snapshot/parser.d.ts +2 -0
  39. package/dist/commands/snapshot/parser.js +80 -0
  40. package/dist/commands/snapshot/runner.d.ts +11 -0
  41. package/dist/commands/snapshot/runner.js +115 -0
  42. package/dist/commands/snapshot/store.d.ts +35 -0
  43. package/dist/commands/snapshot/store.js +158 -0
  44. package/dist/commands/unused/matcher.d.ts +22 -0
  45. package/dist/commands/unused/matcher.js +95 -0
  46. package/dist/commands/unused/parser.d.ts +2 -0
  47. package/dist/commands/unused/parser.js +95 -0
  48. package/dist/commands/unused/runner.d.ts +11 -0
  49. package/dist/commands/unused/runner.js +113 -0
  50. package/dist/commands/unused/scanner.d.ts +18 -0
  51. package/dist/commands/unused/scanner.js +129 -0
  52. package/dist/core/impact.d.ts +36 -0
  53. package/dist/core/impact.js +82 -0
  54. package/dist/core/options.d.ts +13 -1
  55. package/dist/core/options.js +35 -13
  56. package/dist/types/index.d.ts +187 -1
  57. package/dist/ui/tui.d.ts +6 -0
  58. package/dist/ui/tui.js +50 -0
  59. package/dist/utils/semver.d.ts +18 -0
  60. package/dist/utils/semver.js +88 -3
  61. package/package.json +8 -1
@@ -0,0 +1,21 @@
1
+ import type { AuditOptions, AuditSourceStatus, AuditSourceName, CveAdvisory } from "../../../types/index.js";
2
+ import type { AuditTarget } from "../targets.js";
3
+ export interface AuditSourceTargetResult {
4
+ advisories: CveAdvisory[];
5
+ ok: boolean;
6
+ error?: string;
7
+ }
8
+ export interface AuditSourceFetchResult {
9
+ advisories: CveAdvisory[];
10
+ warnings: string[];
11
+ health: AuditSourceStatus;
12
+ }
13
+ export interface AuditSourceAggregateResult {
14
+ advisories: CveAdvisory[];
15
+ warnings: string[];
16
+ sourceHealth: AuditSourceStatus[];
17
+ }
18
+ export interface AuditSourceAdapter {
19
+ name: AuditSourceName;
20
+ fetch(targets: AuditTarget[], options: Pick<AuditOptions, "concurrency" | "registryTimeoutMs">): Promise<AuditSourceFetchResult>;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { PackageDependency } from "../../types/index.js";
2
+ export interface AuditTarget {
3
+ name: string;
4
+ version: string;
5
+ packageDir: string;
6
+ manifestRange: string;
7
+ resolution: "lockfile" | "manifest";
8
+ lockfilePath?: string;
9
+ }
10
+ export interface AuditTargetResolution {
11
+ targets: AuditTarget[];
12
+ warnings: string[];
13
+ resolution: {
14
+ lockfile: number;
15
+ manifest: number;
16
+ unresolved: number;
17
+ };
18
+ }
19
+ export declare function extractAuditVersion(range: string): string | null;
20
+ export declare function resolveAuditTargets(rootCwd: string, packageDirs: string[], depsByDir: Map<string, PackageDependency[]>): Promise<AuditTargetResolution>;
@@ -0,0 +1,314 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ const LOCKFILE_PRIORITY = [
4
+ "package-lock.json",
5
+ "npm-shrinkwrap.json",
6
+ "pnpm-lock.yaml",
7
+ "bun.lock",
8
+ ];
9
+ const packageLockCache = new Map();
10
+ const pnpmLockCache = new Map();
11
+ const bunLockCache = new Map();
12
+ export function extractAuditVersion(range) {
13
+ const trimmed = range.trim();
14
+ const match = trimmed.match(/^(?:\^|~|>=|<=|>|<|=)?\s*(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/);
15
+ return match?.[1] ?? null;
16
+ }
17
+ export async function resolveAuditTargets(rootCwd, packageDirs, depsByDir) {
18
+ const warnings = [];
19
+ const targets = new Map();
20
+ const resolution = {
21
+ lockfile: 0,
22
+ manifest: 0,
23
+ unresolved: 0,
24
+ };
25
+ for (const dir of packageDirs) {
26
+ const deps = depsByDir.get(dir) ?? [];
27
+ for (const dep of deps) {
28
+ const resolved = await resolveDependencyVersion(rootCwd, dir, dep);
29
+ if (!resolved) {
30
+ resolution.unresolved += 1;
31
+ continue;
32
+ }
33
+ const key = `${resolved.name}@${resolved.version}`;
34
+ targets.set(key, resolved);
35
+ if (resolved.resolution === "lockfile") {
36
+ resolution.lockfile += 1;
37
+ }
38
+ else {
39
+ resolution.manifest += 1;
40
+ }
41
+ }
42
+ }
43
+ if (resolution.unresolved > 0) {
44
+ warnings.push(`Skipped ${resolution.unresolved} dependency range${resolution.unresolved === 1 ? "" : "s"} that could not be resolved from a lockfile or concrete manifest version.`);
45
+ }
46
+ return {
47
+ targets: [...targets.values()],
48
+ warnings,
49
+ resolution,
50
+ };
51
+ }
52
+ async function resolveDependencyVersion(rootCwd, packageDir, dep) {
53
+ const lockfiles = await findNearestLockfiles(rootCwd, packageDir);
54
+ for (const lockfilePath of lockfiles) {
55
+ const fileName = path.basename(lockfilePath);
56
+ const version = fileName === "pnpm-lock.yaml"
57
+ ? await resolveFromPnpmLock(lockfilePath, packageDir, dep.name)
58
+ : fileName === "bun.lock"
59
+ ? await resolveFromBunLock(lockfilePath, packageDir, dep.name)
60
+ : await resolveFromPackageLock(lockfilePath, packageDir, dep.name);
61
+ if (version) {
62
+ return {
63
+ name: dep.name,
64
+ version,
65
+ packageDir,
66
+ manifestRange: dep.range,
67
+ resolution: "lockfile",
68
+ lockfilePath,
69
+ };
70
+ }
71
+ }
72
+ const manifestVersion = extractAuditVersion(dep.range);
73
+ if (!manifestVersion)
74
+ return null;
75
+ return {
76
+ name: dep.name,
77
+ version: manifestVersion,
78
+ packageDir,
79
+ manifestRange: dep.range,
80
+ resolution: "manifest",
81
+ };
82
+ }
83
+ async function findNearestLockfiles(rootCwd, startDir) {
84
+ const found = [];
85
+ let current = startDir;
86
+ while (true) {
87
+ for (const fileName of LOCKFILE_PRIORITY) {
88
+ const candidate = path.join(current, fileName);
89
+ try {
90
+ await fs.access(candidate);
91
+ found.push(candidate);
92
+ }
93
+ catch {
94
+ // ignore missing
95
+ }
96
+ }
97
+ if (current === rootCwd)
98
+ break;
99
+ const parent = path.dirname(current);
100
+ if (parent === current)
101
+ break;
102
+ current = parent;
103
+ }
104
+ return found;
105
+ }
106
+ async function resolveFromPackageLock(lockfilePath, packageDir, packageName) {
107
+ const parsed = await readPackageLock(lockfilePath);
108
+ const rootDir = path.dirname(lockfilePath);
109
+ const relDir = normalizeRelativePath(rootDir, packageDir);
110
+ const candidatePaths = relDir
111
+ ? [`${relDir}/node_modules/${packageName}`, `node_modules/${packageName}`]
112
+ : [`node_modules/${packageName}`];
113
+ for (const key of candidatePaths) {
114
+ const version = parsed.packages?.[key]?.version;
115
+ if (version)
116
+ return version;
117
+ }
118
+ if (!relDir) {
119
+ return parsed.dependencies?.[packageName]?.version ?? null;
120
+ }
121
+ return parsed.dependencies?.[packageName]?.version ?? null;
122
+ }
123
+ async function resolveFromPnpmLock(lockfilePath, packageDir, packageName) {
124
+ const parsed = await readPnpmLock(lockfilePath);
125
+ const rootDir = path.dirname(lockfilePath);
126
+ const relDir = normalizeRelativePath(rootDir, packageDir) || ".";
127
+ const importers = [relDir, "."];
128
+ for (const importerKey of importers) {
129
+ const importer = parsed.importers.get(importerKey);
130
+ const version = importer?.get(packageName);
131
+ if (version)
132
+ return version;
133
+ }
134
+ return null;
135
+ }
136
+ async function resolveFromBunLock(lockfilePath, packageDir, packageName) {
137
+ const parsed = await readBunLock(lockfilePath);
138
+ const rootDir = path.dirname(lockfilePath);
139
+ const relDir = normalizeRelativePath(rootDir, packageDir);
140
+ const workspaceKeys = [relDir, ""];
141
+ for (const workspaceKey of workspaceKeys) {
142
+ const workspace = parsed.workspaces.get(workspaceKey);
143
+ const version = workspace?.get(packageName);
144
+ if (version)
145
+ return version;
146
+ }
147
+ return null;
148
+ }
149
+ async function readPackageLock(lockfilePath) {
150
+ let promise = packageLockCache.get(lockfilePath);
151
+ if (!promise) {
152
+ promise = fs
153
+ .readFile(lockfilePath, "utf8")
154
+ .then((content) => JSON.parse(content));
155
+ packageLockCache.set(lockfilePath, promise);
156
+ }
157
+ return await promise;
158
+ }
159
+ async function readPnpmLock(lockfilePath) {
160
+ let promise = pnpmLockCache.get(lockfilePath);
161
+ if (!promise) {
162
+ promise = fs.readFile(lockfilePath, "utf8").then(parsePnpmLock);
163
+ pnpmLockCache.set(lockfilePath, promise);
164
+ }
165
+ return await promise;
166
+ }
167
+ async function readBunLock(lockfilePath) {
168
+ let promise = bunLockCache.get(lockfilePath);
169
+ if (!promise) {
170
+ promise = fs.readFile(lockfilePath, "utf8").then(parseBunLock);
171
+ bunLockCache.set(lockfilePath, promise);
172
+ }
173
+ return await promise;
174
+ }
175
+ function parsePnpmLock(content) {
176
+ const importers = new Map();
177
+ const lines = content.split(/\r?\n/);
178
+ let inImporters = false;
179
+ let currentImporter = null;
180
+ let inDependencySection = false;
181
+ let currentPackageName = null;
182
+ for (const rawLine of lines) {
183
+ const indent = rawLine.match(/^ */)?.[0].length ?? 0;
184
+ const trimmed = rawLine.trim();
185
+ if (!trimmed || trimmed.startsWith("#"))
186
+ continue;
187
+ if (indent === 0) {
188
+ inImporters = trimmed === "importers:";
189
+ currentImporter = null;
190
+ inDependencySection = false;
191
+ currentPackageName = null;
192
+ continue;
193
+ }
194
+ if (!inImporters)
195
+ continue;
196
+ if (indent === 2 && trimmed.endsWith(":")) {
197
+ currentImporter = trimYamlKey(trimmed.slice(0, -1));
198
+ importers.set(currentImporter, new Map());
199
+ inDependencySection = false;
200
+ currentPackageName = null;
201
+ continue;
202
+ }
203
+ if (!currentImporter)
204
+ continue;
205
+ if (indent === 4 && trimmed.endsWith(":")) {
206
+ const key = trimYamlKey(trimmed.slice(0, -1));
207
+ inDependencySection =
208
+ key === "dependencies" ||
209
+ key === "devDependencies" ||
210
+ key === "optionalDependencies";
211
+ currentPackageName = null;
212
+ continue;
213
+ }
214
+ if (!inDependencySection)
215
+ continue;
216
+ if (indent === 6) {
217
+ currentPackageName = null;
218
+ const separator = trimmed.indexOf(":");
219
+ if (separator === -1)
220
+ continue;
221
+ const key = trimYamlKey(trimmed.slice(0, separator));
222
+ const value = trimmed.slice(separator + 1).trim();
223
+ if (!value) {
224
+ currentPackageName = key;
225
+ continue;
226
+ }
227
+ const version = normalizePnpmVersion(value);
228
+ if (version) {
229
+ importers.get(currentImporter)?.set(key, version);
230
+ }
231
+ continue;
232
+ }
233
+ if (indent === 8 && currentPackageName && trimmed.startsWith("version:")) {
234
+ const version = normalizePnpmVersion(trimmed.slice("version:".length));
235
+ if (version) {
236
+ importers.get(currentImporter)?.set(currentPackageName, version);
237
+ }
238
+ }
239
+ }
240
+ return { importers };
241
+ }
242
+ function parseBunLock(content) {
243
+ const workspaces = new Map();
244
+ const lines = content.split(/\r?\n/);
245
+ let inWorkspaces = false;
246
+ let currentWorkspace = "";
247
+ let currentSection = null;
248
+ for (const rawLine of lines) {
249
+ const indent = rawLine.match(/^ */)?.[0].length ?? 0;
250
+ const trimmed = rawLine.trim();
251
+ if (!trimmed)
252
+ continue;
253
+ if (!inWorkspaces && trimmed === '"workspaces": {') {
254
+ inWorkspaces = true;
255
+ currentWorkspace = "";
256
+ currentSection = null;
257
+ continue;
258
+ }
259
+ if (inWorkspaces && indent <= 2 && trimmed === "},") {
260
+ inWorkspaces = false;
261
+ currentWorkspace = "";
262
+ currentSection = null;
263
+ continue;
264
+ }
265
+ if (indent === 4 && trimmed.endsWith("{")) {
266
+ const keyMatch = trimmed.match(/^"([^"]*)": \{$/);
267
+ if (!keyMatch)
268
+ continue;
269
+ currentWorkspace = keyMatch[1] === "" ? "" : keyMatch[1];
270
+ workspaces.set(currentWorkspace, new Map());
271
+ currentSection = null;
272
+ continue;
273
+ }
274
+ if (!workspaces.has(currentWorkspace))
275
+ continue;
276
+ if (indent === 6 && trimmed.endsWith("{")) {
277
+ const keyMatch = trimmed.match(/^"(dependencies|devDependencies|optionalDependencies)": \{$/);
278
+ const sectionName = keyMatch?.[1];
279
+ currentSection =
280
+ sectionName === "dependencies" ||
281
+ sectionName === "devDependencies" ||
282
+ sectionName === "optionalDependencies"
283
+ ? sectionName
284
+ : null;
285
+ continue;
286
+ }
287
+ if (!currentSection)
288
+ continue;
289
+ if (indent === 8) {
290
+ const depMatch = trimmed.match(/^"([^"]+)": "([^"]+)",?$/);
291
+ if (!depMatch)
292
+ continue;
293
+ const packageName = depMatch[1];
294
+ const version = extractAuditVersion(depMatch[2]);
295
+ if (version) {
296
+ workspaces.get(currentWorkspace)?.set(packageName, version);
297
+ }
298
+ }
299
+ }
300
+ return { workspaces };
301
+ }
302
+ function normalizeRelativePath(rootDir, targetDir) {
303
+ const relative = path.relative(rootDir, targetDir).replace(/\\/g, "/");
304
+ return relative === "" ? "" : relative;
305
+ }
306
+ function normalizePnpmVersion(value) {
307
+ const cleaned = trimYamlKey(value.trim());
308
+ const base = cleaned.split("(")[0] ?? cleaned;
309
+ const match = base.match(/^(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/);
310
+ return match?.[1] ?? null;
311
+ }
312
+ function trimYamlKey(value) {
313
+ return value.trim().replace(/^['"]|['"]$/g, "");
314
+ }
@@ -0,0 +1,9 @@
1
+ export interface ChangelogEntry {
2
+ content: string;
3
+ fetchedAt: number;
4
+ }
5
+ /**
6
+ * Fetches the changelog or release notes for a given package and repository URL.
7
+ * Uses SQLite caching to avoid API rate limits.
8
+ */
9
+ export declare function fetchChangelog(packageName: string, repositoryUrl?: string): Promise<string | null>;
@@ -0,0 +1,130 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
5
+ class ChangelogCache {
6
+ db = null;
7
+ dbPath;
8
+ constructor() {
9
+ const basePath = path.join(os.homedir(), ".cache", "rainy-updates");
10
+ this.dbPath = path.join(basePath, "cache.db");
11
+ }
12
+ async init() {
13
+ if (this.db)
14
+ return;
15
+ try {
16
+ if (typeof Bun !== "undefined") {
17
+ await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
18
+ const mod = await import("bun:sqlite");
19
+ this.db = new mod.Database(this.dbPath, { create: true });
20
+ this.db.exec(`
21
+ CREATE TABLE IF NOT EXISTS changelogs (
22
+ package TEXT PRIMARY KEY,
23
+ content TEXT,
24
+ fetched_at INTEGER NOT NULL
25
+ );
26
+ `);
27
+ }
28
+ }
29
+ catch (e) {
30
+ // Fail silently if sqlite isn't available
31
+ this.db = null;
32
+ }
33
+ }
34
+ async get(packageName) {
35
+ if (!this.db)
36
+ return null;
37
+ try {
38
+ const row = this.db
39
+ .prepare("SELECT content, fetched_at FROM changelogs WHERE package = ?")
40
+ .get(packageName);
41
+ if (!row)
42
+ return null;
43
+ const isExpired = Date.now() - row.fetched_at > CACHE_TTL_MS;
44
+ if (isExpired)
45
+ return null;
46
+ return row.content;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ async set(packageName, content) {
53
+ if (!this.db)
54
+ return;
55
+ try {
56
+ this.db
57
+ .prepare("INSERT OR REPLACE INTO changelogs (package, content, fetched_at) VALUES (?, ?, ?)")
58
+ .run(packageName, content, Date.now());
59
+ }
60
+ catch {
61
+ // Ignore cache write errors
62
+ }
63
+ }
64
+ }
65
+ const cache = new ChangelogCache();
66
+ /**
67
+ * Parses a repository URL into a GitHub owner and repo.
68
+ */
69
+ function parseGitHubUrl(url) {
70
+ const match = url.match(/github\.com[/:]([^/]+)\/([^/.]+)(?:\.git)?/);
71
+ if (match && match[1] && match[2]) {
72
+ return { owner: match[1], repo: match[2] };
73
+ }
74
+ return null;
75
+ }
76
+ /**
77
+ * Fetches the changelog or release notes for a given package and repository URL.
78
+ * Uses SQLite caching to avoid API rate limits.
79
+ */
80
+ export async function fetchChangelog(packageName, repositoryUrl) {
81
+ if (!repositoryUrl)
82
+ return null;
83
+ await cache.init();
84
+ // 1. Check Cache
85
+ const cached = await cache.get(packageName);
86
+ if (cached)
87
+ return cached;
88
+ const githubInfo = parseGitHubUrl(repositoryUrl);
89
+ if (!githubInfo)
90
+ return null;
91
+ const { owner, repo } = githubInfo;
92
+ try {
93
+ // 2. Fetch from GitHub API
94
+ // Try releases first, fallback to CHANGELOG.md file
95
+ const headers = {
96
+ "User-Agent": "rainy-updates-cli",
97
+ Accept: "application/vnd.github.v3+json",
98
+ };
99
+ let content = "";
100
+ // Attempt to get the latest release notes
101
+ const releasesRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases/latest`, { headers });
102
+ if (releasesRes.ok) {
103
+ const release = await releasesRes.json();
104
+ if (release.body) {
105
+ content = `# Release ${release.name || release.tag_name}\n\n${release.body}`;
106
+ }
107
+ }
108
+ if (!content) {
109
+ // Fallback: try to fetch CHANGELOG.md from the root
110
+ const contentsRes = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/CHANGELOG.md`, { headers });
111
+ if (contentsRes.ok) {
112
+ const fileContent = await contentsRes.json();
113
+ if (fileContent.content && fileContent.encoding === "base64") {
114
+ content = Buffer.from(fileContent.content, "base64").toString("utf-8");
115
+ }
116
+ }
117
+ }
118
+ if (content) {
119
+ // 3. Cache the fetched content
120
+ await cache.set(packageName, content);
121
+ return content;
122
+ }
123
+ // Nothing found, cache empty string to prevent spamming
124
+ await cache.set(packageName, "");
125
+ return null;
126
+ }
127
+ catch (err) {
128
+ return null;
129
+ }
130
+ }
@@ -0,0 +1,2 @@
1
+ import type { LicenseOptions } from "../../types/index.js";
2
+ export declare function parseLicensesArgs(args: string[]): LicenseOptions;
@@ -0,0 +1,116 @@
1
+ export function parseLicensesArgs(args) {
2
+ const options = {
3
+ cwd: process.cwd(),
4
+ workspace: false,
5
+ allow: undefined,
6
+ deny: undefined,
7
+ sbomFile: undefined,
8
+ jsonFile: undefined,
9
+ diffMode: false,
10
+ concurrency: 12,
11
+ registryTimeoutMs: 10_000,
12
+ cacheTtlSeconds: 3600,
13
+ };
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 === "--diff") {
29
+ options.diffMode = true;
30
+ continue;
31
+ }
32
+ if (current === "--allow" && next) {
33
+ options.allow = next
34
+ .split(",")
35
+ .map((s) => s.trim())
36
+ .filter(Boolean);
37
+ i++;
38
+ continue;
39
+ }
40
+ if (current === "--allow")
41
+ throw new Error("Missing value for --allow");
42
+ if (current === "--deny" && next) {
43
+ options.deny = next
44
+ .split(",")
45
+ .map((s) => s.trim())
46
+ .filter(Boolean);
47
+ i++;
48
+ continue;
49
+ }
50
+ if (current === "--deny")
51
+ throw new Error("Missing value for --deny");
52
+ if (current === "--sbom" && next) {
53
+ options.sbomFile = next;
54
+ i++;
55
+ continue;
56
+ }
57
+ if (current === "--sbom")
58
+ throw new Error("Missing value for --sbom");
59
+ if (current === "--json-file" && next) {
60
+ options.jsonFile = next;
61
+ i++;
62
+ continue;
63
+ }
64
+ if (current === "--json-file")
65
+ throw new Error("Missing value for --json-file");
66
+ if (current === "--concurrency" && next) {
67
+ const n = Number(next);
68
+ if (!Number.isInteger(n) || n <= 0)
69
+ throw new Error("--concurrency must be a positive integer");
70
+ options.concurrency = n;
71
+ i++;
72
+ continue;
73
+ }
74
+ if (current === "--concurrency")
75
+ throw new Error("Missing value for --concurrency");
76
+ if (current === "--timeout" && next) {
77
+ const ms = Number(next);
78
+ if (!Number.isFinite(ms) || ms <= 0)
79
+ throw new Error("--timeout must be a positive number");
80
+ options.registryTimeoutMs = ms;
81
+ i++;
82
+ continue;
83
+ }
84
+ if (current === "--timeout")
85
+ throw new Error("Missing value for --timeout");
86
+ if (current === "--help" || current === "-h") {
87
+ process.stdout.write(LICENSES_HELP);
88
+ process.exit(0);
89
+ }
90
+ if (current.startsWith("-"))
91
+ throw new Error(`Unknown option: ${current}`);
92
+ }
93
+ return options;
94
+ }
95
+ const LICENSES_HELP = `
96
+ rup licenses — Scan dependency licenses and generate SPDX SBOM
97
+
98
+ Usage:
99
+ rup licenses [options]
100
+
101
+ Options:
102
+ --allow <spdx,...> Allow only these SPDX identifiers (e.g. MIT,Apache-2.0)
103
+ --deny <spdx,...> Deny these SPDX identifiers (e.g. GPL-3.0)
104
+ --sbom <path> Write SPDX 2.3 SBOM JSON to file
105
+ --json-file <path> Write JSON report to file
106
+ --diff Show only packages with a different license than last scan
107
+ --workspace Scan all workspace packages
108
+ --timeout <ms> Registry request timeout (default: 10000)
109
+ --concurrency <n> Parallel registry requests (default: 12)
110
+ --cwd <path> Working directory (default: cwd)
111
+ --help Show this help
112
+
113
+ Exit codes:
114
+ 0 No violations
115
+ 1 License violations detected
116
+ `.trimStart();
@@ -0,0 +1,9 @@
1
+ import type { LicenseOptions, LicenseResult } from "../../types/index.js";
2
+ /**
3
+ * Entry point for `rup licenses`. Lazy-loaded by cli.ts.
4
+ *
5
+ * Fetches the SPDX license field from each dependency's packument,
6
+ * checks it against --allow/--deny lists, and optionally generates
7
+ * an SPDX 2.3 SBOM JSON document.
8
+ */
9
+ export declare function runLicenses(options: LicenseOptions): Promise<LicenseResult>;