@rainy-updates/cli 0.5.7 → 0.6.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.
- package/CHANGELOG.md +81 -0
- package/README.md +90 -31
- package/dist/bin/cli.js +16 -16
- package/dist/bin/dispatch.js +29 -32
- package/dist/bin/help.js +32 -2
- package/dist/cache/cache.js +13 -11
- package/dist/commands/audit/parser.js +2 -2
- package/dist/commands/audit/runner.js +27 -46
- package/dist/commands/audit/targets.js +13 -13
- package/dist/commands/bisect/oracle.js +28 -11
- package/dist/commands/bisect/parser.js +3 -3
- package/dist/commands/bisect/runner.js +15 -8
- package/dist/commands/changelog/fetcher.js +11 -5
- package/dist/commands/dashboard/parser.js +103 -1
- package/dist/commands/dashboard/runner.d.ts +2 -2
- package/dist/commands/dashboard/runner.js +67 -37
- package/dist/commands/doctor/parser.js +9 -4
- package/dist/commands/doctor/runner.js +2 -2
- package/dist/commands/ga/parser.js +4 -4
- package/dist/commands/ga/runner.js +13 -7
- package/dist/commands/health/parser.js +2 -2
- package/dist/commands/licenses/runner.js +4 -4
- package/dist/commands/resolve/runner.js +9 -4
- package/dist/commands/review/parser.js +57 -4
- package/dist/commands/review/runner.js +31 -5
- package/dist/commands/snapshot/runner.js +17 -17
- package/dist/commands/snapshot/store.d.ts +0 -12
- package/dist/commands/snapshot/store.js +26 -38
- package/dist/commands/unused/runner.js +6 -7
- package/dist/commands/unused/scanner.js +17 -20
- package/dist/config/loader.d.ts +2 -2
- package/dist/config/loader.js +2 -5
- package/dist/config/policy.js +20 -11
- package/dist/core/analysis/run-silenced.js +0 -1
- package/dist/core/artifacts.js +6 -5
- package/dist/core/baseline.js +3 -5
- package/dist/core/check.js +2 -2
- package/dist/core/ci.js +52 -1
- package/dist/core/decision-plan.d.ts +14 -0
- package/dist/core/decision-plan.js +107 -0
- package/dist/core/doctor/result.js +8 -5
- package/dist/core/fix-pr-batch.js +38 -28
- package/dist/core/fix-pr.js +27 -24
- package/dist/core/init-ci.js +25 -21
- package/dist/core/options.js +95 -4
- package/dist/core/review-model.js +3 -0
- package/dist/core/summary.js +6 -0
- package/dist/core/upgrade.js +64 -2
- package/dist/core/verification.d.ts +2 -0
- package/dist/core/verification.js +106 -0
- package/dist/core/warm-cache.js +2 -2
- package/dist/output/format.js +15 -0
- package/dist/output/github.js +6 -0
- package/dist/output/sarif.js +12 -12
- package/dist/parsers/package-json.js +2 -4
- package/dist/pm/detect.d.ts +3 -1
- package/dist/pm/detect.js +24 -12
- package/dist/pm/install.d.ts +2 -1
- package/dist/pm/install.js +15 -16
- package/dist/registry/npm.js +34 -76
- package/dist/rup +0 -0
- package/dist/types/index.d.ts +76 -5
- package/dist/ui/tui.d.ts +4 -1
- package/dist/ui/tui.js +5 -4
- package/dist/utils/io.js +5 -6
- package/dist/utils/lockfile.js +24 -19
- package/dist/utils/runtime-paths.d.ts +4 -0
- package/dist/utils/runtime-paths.js +35 -0
- package/dist/utils/runtime.d.ts +7 -0
- package/dist/utils/runtime.js +32 -0
- package/dist/workspace/discover.js +55 -51
- package/package.json +16 -16
- package/dist/ui/dashboard/DashboardTUI.d.ts +0 -6
- package/dist/ui/dashboard/DashboardTUI.js +0 -34
- package/dist/ui/dashboard/components/DetailPanel.d.ts +0 -4
- package/dist/ui/dashboard/components/DetailPanel.js +0 -30
- package/dist/ui/dashboard/components/Footer.d.ts +0 -4
- package/dist/ui/dashboard/components/Footer.js +0 -9
- package/dist/ui/dashboard/components/Header.d.ts +0 -4
- package/dist/ui/dashboard/components/Header.js +0 -12
- package/dist/ui/dashboard/components/Sidebar.d.ts +0 -4
- package/dist/ui/dashboard/components/Sidebar.js +0 -23
- package/dist/ui/dashboard/store.d.ts +0 -34
- package/dist/ui/dashboard/store.js +0 -148
package/dist/cache/cache.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
|
-
import os from "node:os";
|
|
3
1
|
import path from "node:path";
|
|
4
|
-
import
|
|
2
|
+
import { writeFileAtomic } from "../utils/io.js";
|
|
3
|
+
import { getCacheDir } from "../utils/runtime-paths.js";
|
|
4
|
+
import { readEnv } from "../utils/runtime.js";
|
|
5
5
|
class FileCacheStore {
|
|
6
6
|
filePath;
|
|
7
7
|
constructor(filePath) {
|
|
@@ -15,19 +15,19 @@ class FileCacheStore {
|
|
|
15
15
|
return null;
|
|
16
16
|
return {
|
|
17
17
|
...entry,
|
|
18
|
-
availableVersions: Array.isArray(entry.availableVersions)
|
|
18
|
+
availableVersions: Array.isArray(entry.availableVersions)
|
|
19
|
+
? entry.availableVersions
|
|
20
|
+
: [entry.latestVersion],
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
async set(entry) {
|
|
22
24
|
const entries = await this.readEntries();
|
|
23
25
|
entries[this.getKey(entry.packageName, entry.target)] = entry;
|
|
24
|
-
await
|
|
25
|
-
await fs.writeFile(this.filePath, JSON.stringify(entries), "utf8");
|
|
26
|
+
await writeFileAtomic(this.filePath, JSON.stringify(entries));
|
|
26
27
|
}
|
|
27
28
|
async readEntries() {
|
|
28
29
|
try {
|
|
29
|
-
|
|
30
|
-
return JSON.parse(content);
|
|
30
|
+
return (await Bun.file(this.filePath).json());
|
|
31
31
|
}
|
|
32
32
|
catch {
|
|
33
33
|
return {};
|
|
@@ -93,7 +93,9 @@ class SqliteCacheStore {
|
|
|
93
93
|
}
|
|
94
94
|
ensureSchema() {
|
|
95
95
|
try {
|
|
96
|
-
const columns = this.db
|
|
96
|
+
const columns = this.db
|
|
97
|
+
.prepare("PRAGMA table_info(versions);")
|
|
98
|
+
.all();
|
|
97
99
|
const hasAvailableVersions = columns.some((column) => column.name === "available_versions");
|
|
98
100
|
if (!hasAvailableVersions) {
|
|
99
101
|
this.db.exec("ALTER TABLE versions ADD COLUMN available_versions TEXT;");
|
|
@@ -117,8 +119,8 @@ export class VersionCache {
|
|
|
117
119
|
this.fallbackReason = fallbackReason;
|
|
118
120
|
}
|
|
119
121
|
static async create(customPath) {
|
|
120
|
-
const basePath = customPath ??
|
|
121
|
-
if (
|
|
122
|
+
const basePath = customPath ?? getCacheDir();
|
|
123
|
+
if (readEnv("RAINY_UPDATES_CACHE_BACKEND") === "file") {
|
|
122
124
|
const jsonPath = path.join(basePath, "cache.json");
|
|
123
125
|
return new VersionCache(new FileCacheStore(jsonPath), "file", true, "forced via RAINY_UPDATES_CACHE_BACKEND=file");
|
|
124
126
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { getRuntimeCwd } from "../../utils/runtime.js";
|
|
3
3
|
const SEVERITY_LEVELS = ["critical", "high", "medium", "low"];
|
|
4
4
|
const SOURCE_MODES = ["auto", "osv", "github", "all"];
|
|
5
5
|
export function parseSeverity(value) {
|
|
@@ -10,7 +10,7 @@ export function parseSeverity(value) {
|
|
|
10
10
|
}
|
|
11
11
|
export function parseAuditArgs(args) {
|
|
12
12
|
const options = {
|
|
13
|
-
cwd:
|
|
13
|
+
cwd: getRuntimeCwd(),
|
|
14
14
|
workspace: false,
|
|
15
15
|
severity: undefined,
|
|
16
16
|
fix: false,
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { promises as fs } from "node:fs";
|
|
3
|
-
import path from "node:path";
|
|
4
1
|
import { collectDependencies, readManifest, } from "../../parsers/package-json.js";
|
|
2
|
+
import { detectPackageManager as detectProjectPackageManager, resolvePackageManager, } from "../../pm/detect.js";
|
|
5
3
|
import { discoverPackageDirs } from "../../workspace/discover.js";
|
|
6
4
|
import { writeFileAtomic } from "../../utils/io.js";
|
|
7
5
|
import { stableStringify } from "../../utils/stable-json.js";
|
|
6
|
+
import { writeStderr, writeStdout } from "../../utils/runtime.js";
|
|
8
7
|
import { fetchAdvisories } from "./fetcher.js";
|
|
9
8
|
import { resolveAuditTargets } from "./targets.js";
|
|
10
9
|
import { filterBySeverity, buildPatchMap, renderAuditSourceHealth, renderAuditSummary, renderAuditTable, summarizeAdvisories, } from "./mapper.js";
|
|
@@ -55,7 +54,7 @@ export async function runAudit(options) {
|
|
|
55
54
|
return result;
|
|
56
55
|
}
|
|
57
56
|
if (!options.silent) {
|
|
58
|
-
|
|
57
|
+
writeStderr(`[audit] Querying ${describeSourceMode(options.sourceMode)} for ${targetResolution.targets.length} dependency version${targetResolution.targets.length === 1 ? "" : "s"}...\n`);
|
|
59
58
|
}
|
|
60
59
|
const fetched = await fetchAdvisories(targetResolution.targets, {
|
|
61
60
|
concurrency: options.concurrency,
|
|
@@ -81,12 +80,12 @@ export async function runAudit(options) {
|
|
|
81
80
|
result.autoFixable = advisories.filter((a) => a.patchedVersion !== null).length;
|
|
82
81
|
if (!options.silent) {
|
|
83
82
|
if (options.reportFormat === "summary") {
|
|
84
|
-
|
|
83
|
+
writeStdout(renderAuditSummary(result.packages) +
|
|
85
84
|
renderAuditSourceHealth(result.sourceHealth) +
|
|
86
85
|
"\n");
|
|
87
86
|
}
|
|
88
87
|
else if (options.reportFormat === "table" || !options.jsonFile) {
|
|
89
|
-
|
|
88
|
+
writeStdout(renderAuditTable(advisories) +
|
|
90
89
|
renderAuditSourceHealth(result.sourceHealth) +
|
|
91
90
|
"\n");
|
|
92
91
|
}
|
|
@@ -102,7 +101,7 @@ export async function runAudit(options) {
|
|
|
102
101
|
warnings: result.warnings,
|
|
103
102
|
}, 2) + "\n");
|
|
104
103
|
if (!options.silent) {
|
|
105
|
-
|
|
104
|
+
writeStderr(`[audit] JSON report written to ${options.jsonFile}\n`);
|
|
106
105
|
}
|
|
107
106
|
}
|
|
108
107
|
if (options.fix && result.autoFixable > 0) {
|
|
@@ -122,40 +121,41 @@ async function applyFix(advisories, options) {
|
|
|
122
121
|
const patchMap = buildPatchMap(advisories);
|
|
123
122
|
if (patchMap.size === 0)
|
|
124
123
|
return;
|
|
125
|
-
const
|
|
124
|
+
const detected = await detectProjectPackageManager(options.cwd);
|
|
125
|
+
const pm = resolvePackageManager(options.packageManager, detected);
|
|
126
126
|
const installArgs = buildInstallArgs(pm, patchMap);
|
|
127
127
|
const installCmd = `${pm} ${installArgs.join(" ")}`;
|
|
128
128
|
if (options.dryRun) {
|
|
129
129
|
if (!options.silent) {
|
|
130
|
-
|
|
130
|
+
writeStderr(`[audit] --dry-run: would execute:\n ${installCmd}\n`);
|
|
131
131
|
if (options.commit) {
|
|
132
132
|
const msg = buildCommitMessage(patchMap);
|
|
133
|
-
|
|
133
|
+
writeStderr(`[audit] --dry-run: would commit:\n git commit -m "${msg}"\n`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
return;
|
|
137
137
|
}
|
|
138
138
|
if (!options.silent) {
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
writeStderr(`[audit] Applying ${patchMap.size} fix(es)...\n`);
|
|
140
|
+
writeStderr(` → ${installCmd}\n`);
|
|
141
141
|
}
|
|
142
142
|
try {
|
|
143
143
|
await runCommand(pm, installArgs, options.cwd);
|
|
144
144
|
}
|
|
145
145
|
catch (err) {
|
|
146
146
|
if (!options.silent) {
|
|
147
|
-
|
|
147
|
+
writeStderr(`[audit] Install failed: ${String(err)}\n`);
|
|
148
148
|
}
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
151
|
if (!options.silent) {
|
|
152
|
-
|
|
152
|
+
writeStderr(`[audit] ✔ Patches applied successfully.\n`);
|
|
153
153
|
}
|
|
154
154
|
if (options.commit) {
|
|
155
155
|
await commitFix(patchMap, options.cwd, options.silent);
|
|
156
156
|
}
|
|
157
157
|
else if (!options.silent) {
|
|
158
|
-
|
|
158
|
+
writeStderr(`[audit] Tip: run with --commit to automatically commit the changes.\n`);
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
function buildInstallArgs(pm, patchMap) {
|
|
@@ -205,48 +205,29 @@ function buildCommitMessage(patchMap) {
|
|
|
205
205
|
const names = items.map(([n]) => n).join(", ");
|
|
206
206
|
return `fix(security): patch ${items.length} vulnerabilities — ${names} (rup audit)`;
|
|
207
207
|
}
|
|
208
|
-
/** Detects the package manager in use by checking for lockfiles. */
|
|
209
|
-
async function detectPackageManager(cwd, explicit) {
|
|
210
|
-
if (explicit !== "auto")
|
|
211
|
-
return explicit;
|
|
212
|
-
const checks = [
|
|
213
|
-
["bun.lock", "bun"],
|
|
214
|
-
["bun.lockb", "bun"],
|
|
215
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
216
|
-
["yarn.lock", "yarn"],
|
|
217
|
-
];
|
|
218
|
-
for (const [lockfile, pm] of checks) {
|
|
219
|
-
try {
|
|
220
|
-
await fs.access(path.join(cwd, lockfile));
|
|
221
|
-
return pm;
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
// not found, try next
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return "npm"; // default
|
|
228
|
-
}
|
|
229
208
|
/** Spawns a subprocess, pipes stdio live to the terminal. */
|
|
230
209
|
function runCommand(cmd, args, cwd, ignoreErrors = false) {
|
|
231
|
-
return new Promise((resolve, reject) => {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
210
|
+
return new Promise(async (resolve, reject) => {
|
|
211
|
+
try {
|
|
212
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
213
|
+
cwd,
|
|
214
|
+
stdin: "inherit",
|
|
215
|
+
stdout: "inherit",
|
|
216
|
+
stderr: "inherit",
|
|
217
|
+
});
|
|
218
|
+
const code = await proc.exited;
|
|
238
219
|
if (code === 0 || ignoreErrors) {
|
|
239
220
|
resolve();
|
|
240
221
|
}
|
|
241
222
|
else {
|
|
242
223
|
reject(new Error(`${cmd} exited with code ${code}`));
|
|
243
224
|
}
|
|
244
|
-
}
|
|
245
|
-
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
246
227
|
if (ignoreErrors)
|
|
247
228
|
resolve();
|
|
248
229
|
else
|
|
249
230
|
reject(err);
|
|
250
|
-
}
|
|
231
|
+
}
|
|
251
232
|
});
|
|
252
233
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
const LOCKFILE_PRIORITY = [
|
|
4
3
|
"package-lock.json",
|
|
@@ -86,13 +85,9 @@ async function findNearestLockfiles(rootCwd, startDir) {
|
|
|
86
85
|
while (true) {
|
|
87
86
|
for (const fileName of LOCKFILE_PRIORITY) {
|
|
88
87
|
const candidate = path.join(current, fileName);
|
|
89
|
-
|
|
90
|
-
await fs.access(candidate);
|
|
88
|
+
if (await fileExists(candidate)) {
|
|
91
89
|
found.push(candidate);
|
|
92
90
|
}
|
|
93
|
-
catch {
|
|
94
|
-
// ignore missing
|
|
95
|
-
}
|
|
96
91
|
}
|
|
97
92
|
if (current === rootCwd)
|
|
98
93
|
break;
|
|
@@ -115,9 +110,6 @@ async function resolveFromPackageLock(lockfilePath, packageDir, packageName) {
|
|
|
115
110
|
if (version)
|
|
116
111
|
return version;
|
|
117
112
|
}
|
|
118
|
-
if (!relDir) {
|
|
119
|
-
return parsed.dependencies?.[packageName]?.version ?? null;
|
|
120
|
-
}
|
|
121
113
|
return parsed.dependencies?.[packageName]?.version ?? null;
|
|
122
114
|
}
|
|
123
115
|
async function resolveFromPnpmLock(lockfilePath, packageDir, packageName) {
|
|
@@ -149,8 +141,8 @@ async function resolveFromBunLock(lockfilePath, packageDir, packageName) {
|
|
|
149
141
|
async function readPackageLock(lockfilePath) {
|
|
150
142
|
let promise = packageLockCache.get(lockfilePath);
|
|
151
143
|
if (!promise) {
|
|
152
|
-
promise =
|
|
153
|
-
.
|
|
144
|
+
promise = Bun.file(lockfilePath)
|
|
145
|
+
.text()
|
|
154
146
|
.then((content) => JSON.parse(content));
|
|
155
147
|
packageLockCache.set(lockfilePath, promise);
|
|
156
148
|
}
|
|
@@ -159,7 +151,7 @@ async function readPackageLock(lockfilePath) {
|
|
|
159
151
|
async function readPnpmLock(lockfilePath) {
|
|
160
152
|
let promise = pnpmLockCache.get(lockfilePath);
|
|
161
153
|
if (!promise) {
|
|
162
|
-
promise =
|
|
154
|
+
promise = Bun.file(lockfilePath).text().then(parsePnpmLock);
|
|
163
155
|
pnpmLockCache.set(lockfilePath, promise);
|
|
164
156
|
}
|
|
165
157
|
return await promise;
|
|
@@ -167,7 +159,7 @@ async function readPnpmLock(lockfilePath) {
|
|
|
167
159
|
async function readBunLock(lockfilePath) {
|
|
168
160
|
let promise = bunLockCache.get(lockfilePath);
|
|
169
161
|
if (!promise) {
|
|
170
|
-
promise =
|
|
162
|
+
promise = Bun.file(lockfilePath).text().then(parseBunLock);
|
|
171
163
|
bunLockCache.set(lockfilePath, promise);
|
|
172
164
|
}
|
|
173
165
|
return await promise;
|
|
@@ -312,3 +304,11 @@ function normalizePnpmVersion(value) {
|
|
|
312
304
|
function trimYamlKey(value) {
|
|
313
305
|
return value.trim().replace(/^['"]|['"]$/g, "");
|
|
314
306
|
}
|
|
307
|
+
async function fileExists(filePath) {
|
|
308
|
+
try {
|
|
309
|
+
return await Bun.file(filePath).exists();
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { detectPackageManager, resolvePackageManager } from "../../pm/detect.js";
|
|
3
3
|
/**
|
|
4
4
|
* The "oracle" for bisect: installs a specific version of a package
|
|
5
5
|
* into the project's node_modules (via the shell), then runs --cmd.
|
|
@@ -11,7 +11,9 @@ export async function bisectOracle(packageName, version, options) {
|
|
|
11
11
|
process.stderr.write(`[bisect:dry-run] Would test ${packageName}@${version}\n`);
|
|
12
12
|
return "skip";
|
|
13
13
|
}
|
|
14
|
-
const
|
|
14
|
+
const detected = await detectPackageManager(options.cwd);
|
|
15
|
+
const packageManager = resolvePackageManager("auto", detected, "bun");
|
|
16
|
+
const installResult = await runShell(buildInstallCommand(packageManager, packageName, version), options.cwd);
|
|
15
17
|
if (installResult !== 0) {
|
|
16
18
|
process.stderr.write(`[bisect] Failed to install ${packageName}@${version}, skipping.\n`);
|
|
17
19
|
return "skip";
|
|
@@ -22,15 +24,30 @@ export async function bisectOracle(packageName, version, options) {
|
|
|
22
24
|
process.stderr.write(`[bisect] ${packageName}@${version} → ${outcome}\n`);
|
|
23
25
|
return outcome;
|
|
24
26
|
}
|
|
25
|
-
function runShell(command, cwd) {
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
27
|
+
async function runShell(command, cwd) {
|
|
28
|
+
try {
|
|
29
|
+
const shellCmd = process.env.SHELL || "sh";
|
|
30
|
+
const proc = Bun.spawn([shellCmd, "-c", command], {
|
|
29
31
|
cwd: path.resolve(cwd),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
stdout: "pipe",
|
|
33
|
+
stderr: "pipe",
|
|
32
34
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
return await proc.exited;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function buildInstallCommand(packageManager, packageName, version) {
|
|
42
|
+
const spec = `${packageName}@${version}`;
|
|
43
|
+
switch (packageManager) {
|
|
44
|
+
case "bun":
|
|
45
|
+
return `bun add --exact --no-save ${spec}`;
|
|
46
|
+
case "pnpm":
|
|
47
|
+
return `pnpm add --save-exact --no-save ${spec}`;
|
|
48
|
+
case "yarn":
|
|
49
|
+
return `npm install --no-save --save-exact ${spec}`;
|
|
50
|
+
default:
|
|
51
|
+
return `npm install --no-save --save-exact ${spec}`;
|
|
52
|
+
}
|
|
36
53
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import
|
|
2
|
+
import { getRuntimeCwd } from "../../utils/runtime.js";
|
|
3
3
|
export function parseBisectArgs(args) {
|
|
4
4
|
const options = {
|
|
5
|
-
cwd:
|
|
5
|
+
cwd: getRuntimeCwd(),
|
|
6
6
|
packageName: "",
|
|
7
7
|
versionRange: undefined,
|
|
8
|
-
testCommand: "
|
|
8
|
+
testCommand: "",
|
|
9
9
|
concurrency: 4,
|
|
10
10
|
registryTimeoutMs: 8000,
|
|
11
11
|
cacheTtlSeconds: 3600,
|
|
@@ -1,26 +1,33 @@
|
|
|
1
|
+
import { detectPackageManager, resolvePackageManager } from "../../pm/detect.js";
|
|
1
2
|
import { fetchBisectVersions, bisectVersions } from "./engine.js";
|
|
2
3
|
/**
|
|
3
4
|
* Entry point for the `bisect` command. Lazy-loaded by cli.ts.
|
|
4
5
|
* Fully isolated: does NOT import anything from core/options.ts.
|
|
5
6
|
*/
|
|
6
7
|
export async function runBisect(options) {
|
|
7
|
-
|
|
8
|
-
const
|
|
8
|
+
const detected = await detectPackageManager(options.cwd);
|
|
9
|
+
const runtimeOptions = {
|
|
10
|
+
...options,
|
|
11
|
+
testCommand: options.testCommand ||
|
|
12
|
+
`${resolvePackageManager("auto", detected, "bun")} test`,
|
|
13
|
+
};
|
|
14
|
+
process.stderr.write(`\n[bisect] Fetching available versions for ${runtimeOptions.packageName}...\n`);
|
|
15
|
+
const versions = await fetchBisectVersions(runtimeOptions);
|
|
9
16
|
if (versions.length === 0) {
|
|
10
|
-
throw new Error(`No versions found for package "${
|
|
17
|
+
throw new Error(`No versions found for package "${runtimeOptions.packageName}".`);
|
|
11
18
|
}
|
|
12
19
|
process.stderr.write(`[bisect] Found ${versions.length} versions. Starting binary search...\n`);
|
|
13
|
-
if (
|
|
14
|
-
process.stderr.write(`[bisect] Range: ${
|
|
20
|
+
if (runtimeOptions.versionRange) {
|
|
21
|
+
process.stderr.write(`[bisect] Range: ${runtimeOptions.versionRange}\n`);
|
|
15
22
|
}
|
|
16
|
-
const result = await bisectVersions(versions,
|
|
23
|
+
const result = await bisectVersions(versions, runtimeOptions);
|
|
17
24
|
if (result.breakingVersion) {
|
|
18
|
-
process.stdout.write(`\n✖ Break introduced in ${
|
|
25
|
+
process.stdout.write(`\n✖ Break introduced in ${runtimeOptions.packageName}@${result.breakingVersion}\n` +
|
|
19
26
|
` Last good version: ${result.lastGoodVersion ?? "none"}\n` +
|
|
20
27
|
` Tested: ${result.totalVersionsTested} versions in ${result.iterations} iterations\n`);
|
|
21
28
|
}
|
|
22
29
|
else {
|
|
23
|
-
process.stdout.write(`\n✔ No breaking version found for ${
|
|
30
|
+
process.stdout.write(`\n✔ No breaking version found for ${runtimeOptions.packageName} (all versions passed).\n` +
|
|
24
31
|
` Tested: ${result.totalVersionsTested} versions\n`);
|
|
25
32
|
}
|
|
26
33
|
return result;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { getCacheDir } from "../../utils/runtime-paths.js";
|
|
4
4
|
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
5
5
|
class ChangelogCache {
|
|
6
6
|
db = null;
|
|
7
7
|
dbPath;
|
|
8
8
|
constructor() {
|
|
9
|
-
const basePath =
|
|
9
|
+
const basePath = getCacheDir();
|
|
10
10
|
this.dbPath = path.join(basePath, "cache.db");
|
|
11
11
|
}
|
|
12
12
|
async init() {
|
|
@@ -14,7 +14,7 @@ class ChangelogCache {
|
|
|
14
14
|
return;
|
|
15
15
|
try {
|
|
16
16
|
if (typeof Bun !== "undefined") {
|
|
17
|
-
await
|
|
17
|
+
await mkdir(path.dirname(this.dbPath), { recursive: true });
|
|
18
18
|
const mod = await import("bun:sqlite");
|
|
19
19
|
this.db = new mod.Database(this.dbPath, { create: true });
|
|
20
20
|
this.db.exec(`
|
|
@@ -111,7 +111,7 @@ export async function fetchChangelog(packageName, repositoryUrl) {
|
|
|
111
111
|
if (contentsRes.ok) {
|
|
112
112
|
const fileContent = await contentsRes.json();
|
|
113
113
|
if (fileContent.content && fileContent.encoding === "base64") {
|
|
114
|
-
content =
|
|
114
|
+
content = decodeBase64Utf8(fileContent.content);
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
@@ -128,3 +128,9 @@ export async function fetchChangelog(packageName, repositoryUrl) {
|
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
+
function decodeBase64Utf8(value) {
|
|
132
|
+
const normalized = value.replace(/\s+/g, "");
|
|
133
|
+
const binary = atob(normalized);
|
|
134
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
135
|
+
return new TextDecoder().decode(bytes);
|
|
136
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
export function parseDashboardArgs(args) {
|
|
2
3
|
const options = {
|
|
3
4
|
cwd: process.cwd(),
|
|
4
5
|
target: "latest",
|
|
6
|
+
filter: undefined,
|
|
7
|
+
reject: undefined,
|
|
5
8
|
includeKinds: [
|
|
6
9
|
"dependencies",
|
|
7
10
|
"devDependencies",
|
|
@@ -12,19 +15,44 @@ export function parseDashboardArgs(args) {
|
|
|
12
15
|
ci: false,
|
|
13
16
|
format: "table",
|
|
14
17
|
workspace: false,
|
|
18
|
+
jsonFile: undefined,
|
|
19
|
+
githubOutputFile: undefined,
|
|
20
|
+
sarifFile: undefined,
|
|
15
21
|
concurrency: 16,
|
|
16
22
|
registryTimeoutMs: 8000,
|
|
17
23
|
registryRetries: 3,
|
|
18
24
|
offline: false,
|
|
19
25
|
stream: false,
|
|
26
|
+
policyFile: undefined,
|
|
27
|
+
prReportFile: undefined,
|
|
28
|
+
failOn: "none",
|
|
29
|
+
maxUpdates: undefined,
|
|
30
|
+
fixPr: false,
|
|
31
|
+
fixBranch: "chore/rainy-updates",
|
|
32
|
+
fixCommitMessage: undefined,
|
|
33
|
+
fixDryRun: false,
|
|
34
|
+
fixPrNoCheckout: false,
|
|
35
|
+
fixPrBatchSize: undefined,
|
|
36
|
+
noPrReport: false,
|
|
20
37
|
logLevel: "info",
|
|
21
38
|
groupBy: "none",
|
|
39
|
+
groupMax: undefined,
|
|
40
|
+
cooldownDays: undefined,
|
|
41
|
+
prLimit: undefined,
|
|
22
42
|
onlyChanged: false,
|
|
23
43
|
ciProfile: "minimal",
|
|
24
44
|
lockfileMode: "preserve",
|
|
25
45
|
interactive: true,
|
|
26
46
|
showImpact: false,
|
|
27
47
|
showHomepage: true,
|
|
48
|
+
mode: "review",
|
|
49
|
+
focus: "all",
|
|
50
|
+
applySelected: false,
|
|
51
|
+
decisionPlanFile: undefined,
|
|
52
|
+
verify: "none",
|
|
53
|
+
testCommand: undefined,
|
|
54
|
+
verificationReportFile: undefined,
|
|
55
|
+
ciGate: "check",
|
|
28
56
|
};
|
|
29
57
|
for (let i = 0; i < args.length; i++) {
|
|
30
58
|
const arg = args[i];
|
|
@@ -44,16 +72,90 @@ export function parseDashboardArgs(args) {
|
|
|
44
72
|
if (arg === "--view") {
|
|
45
73
|
throw new Error("Missing value for --view");
|
|
46
74
|
}
|
|
75
|
+
if (arg === "--mode" && nextArg) {
|
|
76
|
+
if (nextArg === "check" ||
|
|
77
|
+
nextArg === "review" ||
|
|
78
|
+
nextArg === "upgrade") {
|
|
79
|
+
options.mode = nextArg;
|
|
80
|
+
i++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
throw new Error(`Invalid --mode: ${nextArg}`);
|
|
84
|
+
}
|
|
85
|
+
if (arg === "--mode") {
|
|
86
|
+
throw new Error("Missing value for --mode");
|
|
87
|
+
}
|
|
88
|
+
if (arg === "--focus" && nextArg) {
|
|
89
|
+
if (nextArg === "all" ||
|
|
90
|
+
nextArg === "security" ||
|
|
91
|
+
nextArg === "risk" ||
|
|
92
|
+
nextArg === "major" ||
|
|
93
|
+
nextArg === "blocked" ||
|
|
94
|
+
nextArg === "workspace") {
|
|
95
|
+
options.focus = nextArg;
|
|
96
|
+
i++;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`Invalid --focus: ${nextArg}`);
|
|
100
|
+
}
|
|
101
|
+
if (arg === "--focus") {
|
|
102
|
+
throw new Error("Missing value for --focus");
|
|
103
|
+
}
|
|
104
|
+
if (arg === "--apply-selected") {
|
|
105
|
+
options.applySelected = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (arg === "--verify" && nextArg) {
|
|
109
|
+
if (nextArg === "none" ||
|
|
110
|
+
nextArg === "install" ||
|
|
111
|
+
nextArg === "test" ||
|
|
112
|
+
nextArg === "install,test") {
|
|
113
|
+
options.verify = nextArg;
|
|
114
|
+
i++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Invalid --verify: ${nextArg}`);
|
|
118
|
+
}
|
|
119
|
+
if (arg === "--verify") {
|
|
120
|
+
throw new Error("Missing value for --verify");
|
|
121
|
+
}
|
|
122
|
+
if (arg === "--test-command" && nextArg) {
|
|
123
|
+
options.testCommand = nextArg;
|
|
124
|
+
i++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (arg === "--test-command") {
|
|
128
|
+
throw new Error("Missing value for --test-command");
|
|
129
|
+
}
|
|
130
|
+
if (arg === "--verification-report-file" && nextArg) {
|
|
131
|
+
options.verificationReportFile = path.resolve(options.cwd, nextArg);
|
|
132
|
+
i++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (arg === "--verification-report-file") {
|
|
136
|
+
throw new Error("Missing value for --verification-report-file");
|
|
137
|
+
}
|
|
138
|
+
if (arg === "--plan-file" && nextArg) {
|
|
139
|
+
options.decisionPlanFile = path.resolve(options.cwd, nextArg);
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (arg === "--plan-file") {
|
|
144
|
+
throw new Error("Missing value for --plan-file");
|
|
145
|
+
}
|
|
47
146
|
// Pass through common workspace / cwd args
|
|
48
147
|
if (arg === "--workspace") {
|
|
49
148
|
options.workspace = true;
|
|
50
149
|
continue;
|
|
51
150
|
}
|
|
52
151
|
if (arg === "--cwd" && nextArg) {
|
|
53
|
-
options.cwd = nextArg;
|
|
152
|
+
options.cwd = path.resolve(nextArg);
|
|
54
153
|
i++;
|
|
55
154
|
continue;
|
|
56
155
|
}
|
|
156
|
+
if (arg.startsWith("-")) {
|
|
157
|
+
throw new Error(`Unknown dashboard option: ${arg}`);
|
|
158
|
+
}
|
|
57
159
|
}
|
|
58
160
|
return options;
|
|
59
161
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type { DashboardOptions, DashboardResult } from "../../types/index.js";
|
|
2
|
-
export declare function runDashboard(options: DashboardOptions): Promise<DashboardResult>;
|
|
1
|
+
import type { DashboardOptions, DashboardResult, ReviewResult } from "../../types/index.js";
|
|
2
|
+
export declare function runDashboard(options: DashboardOptions, prebuiltReview?: ReviewResult): Promise<DashboardResult>;
|