@cellajs/cli 0.4.0 → 0.4.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.
- package/README.md +89 -25
- package/dist/config.js.map +1 -1
- package/dist/index.js +986 -416
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env tsx
|
|
2
2
|
|
|
3
3
|
// src/cella-cli.ts
|
|
4
|
-
import { existsSync as
|
|
5
|
-
import { join as
|
|
6
|
-
import
|
|
4
|
+
import { existsSync as existsSync13, statSync as statSync3 } from "fs";
|
|
5
|
+
import { join as join19, resolve as resolve7 } from "path";
|
|
6
|
+
import process9 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import process5 from "process";
|
|
@@ -219,7 +219,7 @@ import process4 from "process";
|
|
|
219
219
|
// package.json
|
|
220
220
|
var package_default = {
|
|
221
221
|
name: "@cellajs/cli",
|
|
222
|
-
version: "0.4.
|
|
222
|
+
version: "0.4.2",
|
|
223
223
|
private: false,
|
|
224
224
|
license: "MIT",
|
|
225
225
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -300,7 +300,7 @@ var package_default = {
|
|
|
300
300
|
"sync",
|
|
301
301
|
"template"
|
|
302
302
|
],
|
|
303
|
-
packageManager: "pnpm@11.
|
|
303
|
+
packageManager: "pnpm@11.28.7",
|
|
304
304
|
commitlint: {
|
|
305
305
|
extends: [
|
|
306
306
|
"@commitlint/config-conventional"
|
|
@@ -536,16 +536,18 @@ function printFileSection(files, title, linkOptions, options) {
|
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
|
+
function filesWithStatus(files, status) {
|
|
540
|
+
return files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
541
|
+
}
|
|
539
542
|
function printFileGroup(files, status, linkOptions, options) {
|
|
540
|
-
const filtered = files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
541
543
|
const config = statusConfig[status];
|
|
542
544
|
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
543
|
-
printFileSection(
|
|
545
|
+
printFileSection(filesWithStatus(files, status), title, linkOptions, { ...options, icon: config.icon });
|
|
544
546
|
}
|
|
545
547
|
function findProtectedBehind(files) {
|
|
546
548
|
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path));
|
|
547
549
|
}
|
|
548
|
-
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so
|
|
550
|
+
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so review what upstream changed and adopt what you need.";
|
|
549
551
|
function protectionIcon(file) {
|
|
550
552
|
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
551
553
|
}
|
|
@@ -553,22 +555,11 @@ function formatUpstreamChangedLines(file) {
|
|
|
553
555
|
if (file.upstreamChangedLines === void 0) return "";
|
|
554
556
|
return colors_default.yellow(` \xB7 ${plural(file.upstreamChangedLines, "line")} changed upstream`);
|
|
555
557
|
}
|
|
556
|
-
function protectedDiffHints(source, paths) {
|
|
557
|
-
const range = source?.upstreamDiffRange;
|
|
558
|
-
if (!range) return [];
|
|
559
|
-
const listed = new Set(paths);
|
|
560
|
-
const groups = (source?.protectedUpstreamChanges ?? []).filter((group) => group.paths.some((p) => listed.has(p)));
|
|
561
|
-
if (groups.length === 0) return [];
|
|
562
|
-
return [
|
|
563
|
-
"what upstream changed since the last sync, per pinned/ignored entry:",
|
|
564
|
-
...groups.map((group) => ` git diff ${range} -- ${shellPath(group.entry)}`)
|
|
565
|
-
];
|
|
566
|
-
}
|
|
567
558
|
function formatUpstreamLinesAbsent(file) {
|
|
568
559
|
const n = file.upstreamLinesAbsent ?? 0;
|
|
569
560
|
return n > 0 ? colors_default.yellow(` \xB7 ${plural(n, "upstream line")} absent`) : "";
|
|
570
561
|
}
|
|
571
|
-
function printAnalysisFileGroups(files, linkOptions
|
|
562
|
+
function printAnalysisFileGroups(files, linkOptions) {
|
|
572
563
|
const protectedBehind = findProtectedBehind(files);
|
|
573
564
|
printFileGroup(files, "behind", linkOptions, {
|
|
574
565
|
title: colors_default.cyan("\u2193 behind on upstream")
|
|
@@ -585,13 +576,7 @@ function printAnalysisFileGroups(files, linkOptions, diffSource) {
|
|
|
585
576
|
icon: protectionIcon,
|
|
586
577
|
suffix: formatUpstreamChangedLines,
|
|
587
578
|
dateSource: "upstream",
|
|
588
|
-
hint:
|
|
589
|
-
PROTECTED_BEHIND_HINT,
|
|
590
|
-
...protectedDiffHints(
|
|
591
|
-
diffSource,
|
|
592
|
-
protectedBehind.map((file) => file.path)
|
|
593
|
-
)
|
|
594
|
-
]
|
|
579
|
+
hint: PROTECTED_BEHIND_HINT
|
|
595
580
|
});
|
|
596
581
|
printFileGroup(files, "drifted", linkOptions, {
|
|
597
582
|
title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
@@ -636,18 +621,15 @@ function printProtectedConflicts(result) {
|
|
|
636
621
|
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
637
622
|
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
638
623
|
}
|
|
639
|
-
|
|
640
|
-
}
|
|
641
|
-
function plural(count2, singular, pluralForm = `${singular}s`) {
|
|
642
|
-
return `${count2} ${count2 === 1 ? singular : pluralForm}`;
|
|
624
|
+
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
643
625
|
}
|
|
644
|
-
function
|
|
645
|
-
return
|
|
626
|
+
function plural(count3, singular, pluralForm = `${singular}s`) {
|
|
627
|
+
return `${count3} ${count3 === 1 ? singular : pluralForm}`;
|
|
646
628
|
}
|
|
647
629
|
function printIgnoredUpstreamChanges(result) {
|
|
648
630
|
const groups = result.ignoredUpstreamChanges ?? [];
|
|
649
631
|
if (groups.length === 0) return;
|
|
650
|
-
const total = groups.reduce((
|
|
632
|
+
const total = groups.reduce((count3, group) => count3 + group.paths.length, 0);
|
|
651
633
|
console.info();
|
|
652
634
|
console.info(
|
|
653
635
|
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
@@ -657,11 +639,8 @@ function printIgnoredUpstreamChanges(result) {
|
|
|
657
639
|
if (group.added > 0) counts.push(`${group.added} new`);
|
|
658
640
|
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
659
641
|
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
660
|
-
if (result.upstreamDiffRange) {
|
|
661
|
-
console.info(colors_default.dim(` git diff ${result.upstreamDiffRange} -- ${shellPath(group.entry)}`));
|
|
662
|
-
}
|
|
663
642
|
}
|
|
664
|
-
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync:
|
|
643
|
+
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync: review and adopt what you need."));
|
|
665
644
|
}
|
|
666
645
|
function printUpstreamOverrideChanges(result) {
|
|
667
646
|
const report = result.upstreamOverrides;
|
|
@@ -1393,18 +1372,6 @@ function groupIgnoredUpstreamChanges(files, ignored) {
|
|
|
1393
1372
|
for (const group of groups.values()) group.paths.sort();
|
|
1394
1373
|
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1395
1374
|
}
|
|
1396
|
-
function groupProtectedUpstreamChanges(paths, config) {
|
|
1397
|
-
const [, findEntry] = entryMatcher([...config.overrides?.pinned ?? [], ...config.overrides?.ignored ?? []]);
|
|
1398
|
-
const groups = /* @__PURE__ */ new Map();
|
|
1399
|
-
for (const path of paths) {
|
|
1400
|
-
const entry = findEntry(path) ?? path;
|
|
1401
|
-
const group = groups.get(entry) ?? { entry, paths: [] };
|
|
1402
|
-
group.paths.push(path);
|
|
1403
|
-
groups.set(entry, group);
|
|
1404
|
-
}
|
|
1405
|
-
for (const group of groups.values()) group.paths.sort();
|
|
1406
|
-
return [...groups.values()];
|
|
1407
|
-
}
|
|
1408
1375
|
async function validateOverrides(config, forkPath, upstreamRef) {
|
|
1409
1376
|
const warnings = [];
|
|
1410
1377
|
const ignored = config.overrides?.ignored ?? [];
|
|
@@ -1524,7 +1491,7 @@ var serviceDefinitions = [
|
|
|
1524
1491
|
},
|
|
1525
1492
|
{
|
|
1526
1493
|
name: "audit",
|
|
1527
|
-
description: "update outdated packages, then rebuild the security overrides from a fresh audit",
|
|
1494
|
+
description: "update outdated packages and runtime pins, then rebuild the security overrides from a fresh audit",
|
|
1528
1495
|
options: [["--list", "report only: no prompts, and nothing is written"]]
|
|
1529
1496
|
},
|
|
1530
1497
|
{
|
|
@@ -1845,8 +1812,8 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
|
|
|
1845
1812
|
packageJsonSync: forkConfig.settings.packageJsonSync ?? DEFAULT_PACKAGE_JSON_SYNC
|
|
1846
1813
|
};
|
|
1847
1814
|
const changes = diffOverrideLists(base, incoming, fork);
|
|
1848
|
-
const
|
|
1849
|
-
return
|
|
1815
|
+
const count3 = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
|
|
1816
|
+
return count3 > 0 ? { kind: "changes", ...changes } : void 0;
|
|
1850
1817
|
}
|
|
1851
1818
|
|
|
1852
1819
|
// src/services/analyze-core.ts
|
|
@@ -2281,7 +2248,7 @@ function resultMeta(config, ctx) {
|
|
|
2281
2248
|
upstreamCommit: ctx.upstreamCommit
|
|
2282
2249
|
};
|
|
2283
2250
|
}
|
|
2284
|
-
async function upstreamOnlyReports(config, ctx, files
|
|
2251
|
+
async function upstreamOnlyReports(config, ctx, files) {
|
|
2285
2252
|
const { forkPath } = config;
|
|
2286
2253
|
try {
|
|
2287
2254
|
const [baseSha, upstreamSha, upstreamOverrides] = await Promise.all([
|
|
@@ -2293,8 +2260,7 @@ async function upstreamOnlyReports(config, ctx, files, protectedConflicts) {
|
|
|
2293
2260
|
return {
|
|
2294
2261
|
upstreamDiffRange: `${baseSha}..${upstreamSha}`,
|
|
2295
2262
|
upstreamOverrides,
|
|
2296
|
-
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
|
|
2297
|
-
protectedUpstreamChanges: groupProtectedUpstreamChanges(protectedConflicts, config)
|
|
2263
|
+
ignoredUpstreamChanges: groupIgnoredUpstreamChanges(files, config.overrides?.ignored ?? [])
|
|
2298
2264
|
};
|
|
2299
2265
|
} catch {
|
|
2300
2266
|
return {};
|
|
@@ -2379,7 +2345,7 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2379
2345
|
autoMergedFiles,
|
|
2380
2346
|
protectedConflicts,
|
|
2381
2347
|
...resultMeta(config, ctx),
|
|
2382
|
-
...await upstreamOnlyReports(config, ctx, analyzedFiles
|
|
2348
|
+
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2383
2349
|
...notesResult(notes)
|
|
2384
2350
|
};
|
|
2385
2351
|
}
|
|
@@ -2397,7 +2363,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2397
2363
|
);
|
|
2398
2364
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2399
2365
|
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2400
|
-
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles
|
|
2366
|
+
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles);
|
|
2401
2367
|
onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run, no changes applied`);
|
|
2402
2368
|
return {
|
|
2403
2369
|
success: true,
|
|
@@ -2618,6 +2584,234 @@ async function runMigrate(config) {
|
|
|
2618
2584
|
}
|
|
2619
2585
|
}
|
|
2620
2586
|
|
|
2587
|
+
// src/services/review-page.ts
|
|
2588
|
+
import { basename as basename2 } from "path";
|
|
2589
|
+
import process7 from "process";
|
|
2590
|
+
import { pathToFileURL } from "url";
|
|
2591
|
+
|
|
2592
|
+
// src/utils/diff.ts
|
|
2593
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2594
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
2595
|
+
import { join as join9 } from "path";
|
|
2596
|
+
import process6 from "process";
|
|
2597
|
+
function gitDiffFile(cwd, range, filePath) {
|
|
2598
|
+
try {
|
|
2599
|
+
return execFileSync2("git", ["diff", "--no-color", range, "--", filePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
2600
|
+
} catch (error) {
|
|
2601
|
+
const stderr = error.stderr?.toString().trim();
|
|
2602
|
+
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
function escapeHtml(text) {
|
|
2606
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
2607
|
+
}
|
|
2608
|
+
async function renderDiffPage(sections, meta) {
|
|
2609
|
+
const { preloadPatchDiff } = await import("@pierre/diffs/ssr");
|
|
2610
|
+
const renderFile = async (file) => {
|
|
2611
|
+
const { prerenderedHTML } = await preloadPatchDiff({
|
|
2612
|
+
patch: file.patch,
|
|
2613
|
+
options: {
|
|
2614
|
+
diffStyle: "split",
|
|
2615
|
+
themeType: "system",
|
|
2616
|
+
lineDiffType: "word-alt",
|
|
2617
|
+
hunkSeparators: "line-info",
|
|
2618
|
+
overflow: "wrap"
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
return `<div class="diff-shell"><template shadowrootmode="open">
|
|
2622
|
+
${prerenderedHTML}
|
|
2623
|
+
</template></div>`;
|
|
2624
|
+
};
|
|
2625
|
+
const blocks = [];
|
|
2626
|
+
for (const [index2, section] of sections.entries()) {
|
|
2627
|
+
const files = [];
|
|
2628
|
+
for (const file of section.files) files.push(await renderFile(file));
|
|
2629
|
+
const count3 = section.files.length + (section.omitted?.length ?? 0);
|
|
2630
|
+
const title = section.title ? `<h2>${escapeHtml(section.title)} <span class="count">\xB7 ${count3} ${count3 === 1 ? "file" : "files"}</span></h2>` : "";
|
|
2631
|
+
const hint = section.hint ? `<p class="hint">${escapeHtml(section.hint)}</p>` : "";
|
|
2632
|
+
const omitted = section.omitted?.length ? `<p class="hint">Not shown here (binary, too large, or past the page limit):</p><ul class="omitted">${section.omitted.map((path) => `<li><code>${escapeHtml(path)}</code></li>`).join("")}</ul>` : "";
|
|
2633
|
+
blocks.push(`<section id="section-${index2 + 1}">${title}${hint}${files.join("\n")}${omitted}</section>`);
|
|
2634
|
+
}
|
|
2635
|
+
const titled = sections.filter((section) => section.title);
|
|
2636
|
+
const index = titled.length > 1 ? `<nav><ol>${sections.map(
|
|
2637
|
+
(section, i) => section.title ? `<li><a href="#section-${i + 1}">${escapeHtml(section.title)}</a> <span class="count">\xB7 ${section.files.length + (section.omitted?.length ?? 0)}</span></li>` : ""
|
|
2638
|
+
).join("")}</ol></nav>` : "";
|
|
2639
|
+
const note = meta.note ? `<span class="note">${escapeHtml(meta.note)}</span>` : "";
|
|
2640
|
+
return `<!doctype html>
|
|
2641
|
+
<html lang="en">
|
|
2642
|
+
<head>
|
|
2643
|
+
<meta charset="utf-8">
|
|
2644
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2645
|
+
<title>${escapeHtml(meta.title)}</title>
|
|
2646
|
+
<style>
|
|
2647
|
+
:root { color-scheme: light dark; }
|
|
2648
|
+
* { box-sizing: border-box; }
|
|
2649
|
+
body {
|
|
2650
|
+
margin: 0;
|
|
2651
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
2652
|
+
background: light-dark(#fafafa, #111);
|
|
2653
|
+
color: light-dark(#111, #eee);
|
|
2654
|
+
}
|
|
2655
|
+
header {
|
|
2656
|
+
display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap;
|
|
2657
|
+
padding: 0.9rem 1.25rem;
|
|
2658
|
+
border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
2659
|
+
position: sticky; top: 0; z-index: 1;
|
|
2660
|
+
background: inherit;
|
|
2661
|
+
}
|
|
2662
|
+
header .brand { font-weight: 600; color: light-dark(#0891b2, #22d3ee); }
|
|
2663
|
+
header code { font-family: ui-monospace, monospace; font-size: 0.9rem; }
|
|
2664
|
+
header .note { margin-left: auto; font-size: 0.8rem; opacity: 0.7; }
|
|
2665
|
+
main { max-width: 1400px; margin: 1rem auto; padding: 0 1rem 2rem; }
|
|
2666
|
+
nav ol { margin: 0 0 1.5rem; padding-left: 1.25rem; line-height: 1.7; }
|
|
2667
|
+
nav a { color: inherit; }
|
|
2668
|
+
section { scroll-margin-top: 4rem; }
|
|
2669
|
+
h2 { font-size: 1.05rem; margin: 2rem 0 0.25rem; }
|
|
2670
|
+
.count { font-weight: normal; opacity: 0.6; }
|
|
2671
|
+
.hint { margin: 0 0 1rem; font-size: 0.9rem; opacity: 0.75; max-width: 80ch; }
|
|
2672
|
+
.diff-shell {
|
|
2673
|
+
border: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
2674
|
+
border-radius: 8px; overflow: hidden;
|
|
2675
|
+
background: light-dark(#fff, #161616);
|
|
2676
|
+
margin-bottom: 1rem;
|
|
2677
|
+
}
|
|
2678
|
+
.omitted { font-size: 0.85rem; opacity: 0.8; }
|
|
2679
|
+
.omitted code { font-family: ui-monospace, monospace; }
|
|
2680
|
+
</style>
|
|
2681
|
+
</head>
|
|
2682
|
+
<body>
|
|
2683
|
+
<header>
|
|
2684
|
+
<span class="brand">\u29C8 cella diff</span>
|
|
2685
|
+
<code>${escapeHtml(meta.title)}</code>
|
|
2686
|
+
${note}
|
|
2687
|
+
</header>
|
|
2688
|
+
<main>
|
|
2689
|
+
${index}
|
|
2690
|
+
${blocks.join("\n")}
|
|
2691
|
+
</main>
|
|
2692
|
+
</body>
|
|
2693
|
+
</html>`;
|
|
2694
|
+
}
|
|
2695
|
+
async function writeDiffPage(sections, meta, repoPath, name) {
|
|
2696
|
+
const html = await renderDiffPage(sections, meta);
|
|
2697
|
+
const gitDir = execFileSync2("git", ["rev-parse", "--absolute-git-dir"], { cwd: repoPath, encoding: "utf8" }).trim();
|
|
2698
|
+
const dir = join9(gitDir, "cella-diff");
|
|
2699
|
+
mkdirSync(dir, { recursive: true });
|
|
2700
|
+
const outPath = join9(dir, `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
2701
|
+
writeFileSync(outPath, html);
|
|
2702
|
+
return outPath;
|
|
2703
|
+
}
|
|
2704
|
+
function openInBrowser(target) {
|
|
2705
|
+
try {
|
|
2706
|
+
execFileSync2(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
2707
|
+
return true;
|
|
2708
|
+
} catch {
|
|
2709
|
+
return false;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
// src/services/review-page.ts
|
|
2714
|
+
var MAX_PAGE_FILES = 50;
|
|
2715
|
+
var MAX_PATCH_BYTES = 512 * 1024;
|
|
2716
|
+
function reviewSections(result, command) {
|
|
2717
|
+
const [base, upstream] = result.upstreamDiffRange?.split("..") ?? [];
|
|
2718
|
+
if (!base || !upstream) return [];
|
|
2719
|
+
const sinceSync = `${base}..${upstream}`;
|
|
2720
|
+
const dropped = command === "sync" ? {
|
|
2721
|
+
title: "protected files kept the fork version",
|
|
2722
|
+
hint: "What upstream changed in these files since the last sync. The fork kept its own version, so none of it arrived: adopt what you need.",
|
|
2723
|
+
range: sinceSync,
|
|
2724
|
+
paths: result.protectedConflicts ?? []
|
|
2725
|
+
} : {
|
|
2726
|
+
title: "protected but behind upstream",
|
|
2727
|
+
hint: "What upstream changed in these files since the last sync. They are pinned or ignored, so the sync keeps the fork version and none of it arrives: adopt what you need.",
|
|
2728
|
+
range: sinceSync,
|
|
2729
|
+
paths: findProtectedBehind(result.files).map((file) => file.path)
|
|
2730
|
+
};
|
|
2731
|
+
const sections = [
|
|
2732
|
+
dropped,
|
|
2733
|
+
{
|
|
2734
|
+
title: "ignored paths changed upstream",
|
|
2735
|
+
hint: "What upstream changed under ignored paths since the last sync. The fork left these files untouched, and ignored paths never sync.",
|
|
2736
|
+
range: sinceSync,
|
|
2737
|
+
paths: (result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths)
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
title: "pinned files that match the previous upstream",
|
|
2741
|
+
hint: "What upstream changed in these files since the last sync. The fork copy still equals the previous upstream, so the pin only drops these changes: unpin to take them.",
|
|
2742
|
+
range: sinceSync,
|
|
2743
|
+
paths: findMaskingPins(result.files).map((file) => file.path)
|
|
2744
|
+
}
|
|
2745
|
+
];
|
|
2746
|
+
if (command === "analyze") {
|
|
2747
|
+
sections.push(
|
|
2748
|
+
{
|
|
2749
|
+
title: "protected in fork, upstream lines absent",
|
|
2750
|
+
hint: "The fork's version against upstream's, both as they are now: added lines are upstream content the fork does not have, dropped by an earlier sync or removed on purpose.",
|
|
2751
|
+
range: `HEAD..${upstream}`,
|
|
2752
|
+
paths: filesWithStatus(result.files, "ahead").filter((file) => (file.upstreamLinesAbsent ?? 0) > 0).map((file) => file.path)
|
|
2753
|
+
},
|
|
2754
|
+
{
|
|
2755
|
+
title: "drifted from upstream",
|
|
2756
|
+
hint: "What the fork changed in these files since the last sync. They are not pinned or ignored, so upstream changes still merge into them.",
|
|
2757
|
+
range: `${base}..HEAD`,
|
|
2758
|
+
paths: filesWithStatus(result.files, "drifted").map((file) => file.path)
|
|
2759
|
+
}
|
|
2760
|
+
);
|
|
2761
|
+
}
|
|
2762
|
+
return sections.filter((section) => section.paths.length > 0);
|
|
2763
|
+
}
|
|
2764
|
+
async function writeReviewPage(forkPath, sections, meta) {
|
|
2765
|
+
const [base, upstream] = meta.range.split("..");
|
|
2766
|
+
let diffs = 0;
|
|
2767
|
+
const pageSections = [];
|
|
2768
|
+
for (const section of sections) {
|
|
2769
|
+
const pageSection = { title: section.title, hint: section.hint, files: [], omitted: [] };
|
|
2770
|
+
for (const path of section.paths) {
|
|
2771
|
+
if (diffs >= MAX_PAGE_FILES) {
|
|
2772
|
+
pageSection.omitted?.push(path);
|
|
2773
|
+
continue;
|
|
2774
|
+
}
|
|
2775
|
+
const patch = gitDiffFile(forkPath, section.range, path);
|
|
2776
|
+
if (patch.length === 0) continue;
|
|
2777
|
+
if (patch.length > MAX_PATCH_BYTES || !patch.includes("\n@@ ")) {
|
|
2778
|
+
pageSection.omitted?.push(path);
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
pageSection.files.push({ path, patch: patch.toString() });
|
|
2782
|
+
diffs++;
|
|
2783
|
+
}
|
|
2784
|
+
if (pageSection.files.length > 0 || pageSection.omitted?.length) pageSections.push(pageSection);
|
|
2785
|
+
}
|
|
2786
|
+
if (diffs === 0) return void 0;
|
|
2787
|
+
const page = await writeDiffPage(
|
|
2788
|
+
pageSections,
|
|
2789
|
+
{ title: `${meta.command} review`, note: `${basename2(forkPath)} \xB7 last sync ${base} \u2192 upstream ${upstream}` },
|
|
2790
|
+
forkPath,
|
|
2791
|
+
`${meta.command}-review`
|
|
2792
|
+
);
|
|
2793
|
+
return { path: page, diffs };
|
|
2794
|
+
}
|
|
2795
|
+
async function reportReviewPage(forkPath, result, command) {
|
|
2796
|
+
const sections = reviewSections(result, command);
|
|
2797
|
+
if (sections.length === 0 || !result.upstreamDiffRange) return;
|
|
2798
|
+
createSpinner("writing review page...");
|
|
2799
|
+
let page;
|
|
2800
|
+
try {
|
|
2801
|
+
page = await writeReviewPage(forkPath, sections, { command, range: result.upstreamDiffRange });
|
|
2802
|
+
} catch {
|
|
2803
|
+
page = void 0;
|
|
2804
|
+
}
|
|
2805
|
+
spinnerSuccess();
|
|
2806
|
+
if (!page) return;
|
|
2807
|
+
const opened = command === "sync" && process7.stdout.isTTY === true && openInBrowser(page.path);
|
|
2808
|
+
console.info();
|
|
2809
|
+
console.info(
|
|
2810
|
+
`${colors_default.cyan("\u29C8")} review page with ${plural(page.diffs, "diff")}${opened ? ", opened in the browser" : ""}:`
|
|
2811
|
+
);
|
|
2812
|
+
console.info(colors_default.dim(` ${hyperlink(page.path, pathToFileURL(page.path).href)}`));
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2621
2815
|
// src/services/analyze.ts
|
|
2622
2816
|
var scopeStatuses = {
|
|
2623
2817
|
all: /* @__PURE__ */ new Set(["ahead", "drifted", "diverged"]),
|
|
@@ -2668,23 +2862,28 @@ async function runAnalyze(config) {
|
|
|
2668
2862
|
fileLinkMode: config.settings.fileLinkMode,
|
|
2669
2863
|
forkPath: config.forkPath
|
|
2670
2864
|
};
|
|
2671
|
-
printAnalysisFileGroups(result.files, linkOptions
|
|
2865
|
+
printAnalysisFileGroups(result.files, linkOptions);
|
|
2672
2866
|
printEngineReports(result, "analysis summary");
|
|
2867
|
+
await reportReviewPage(config.forkPath, result, "analyze");
|
|
2673
2868
|
await printMigrationNotesLine(config, result);
|
|
2674
2869
|
console.info();
|
|
2675
2870
|
return result;
|
|
2676
2871
|
}
|
|
2677
2872
|
|
|
2678
2873
|
// src/services/audit.ts
|
|
2679
|
-
import {
|
|
2680
|
-
import {
|
|
2681
|
-
import {
|
|
2682
|
-
import { checkbox, confirm as confirm2, Separator } from "@inquirer/prompts";
|
|
2874
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2875
|
+
import { basename as basename4, join as join13, relative as relative3 } from "path";
|
|
2876
|
+
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2683
2877
|
|
|
2684
2878
|
// src/utils/audit-utils.ts
|
|
2685
2879
|
import { execFile as execFile2 } from "child_process";
|
|
2686
2880
|
import { promisify as promisify2 } from "util";
|
|
2687
2881
|
var execFileAsync2 = promisify2(execFile2);
|
|
2882
|
+
function outdatedPackageName(key) {
|
|
2883
|
+
const name = key.split(" ")[0];
|
|
2884
|
+
const at = name.indexOf("@", 1);
|
|
2885
|
+
return at === -1 ? name : name.slice(0, at);
|
|
2886
|
+
}
|
|
2688
2887
|
function getRepoUrl(repositoryUrl) {
|
|
2689
2888
|
if (!repositoryUrl) return null;
|
|
2690
2889
|
let url = repositoryUrl;
|
|
@@ -2712,11 +2911,6 @@ function getReleasesUrl(repoUrl) {
|
|
|
2712
2911
|
if (!isGitHubRepoUrl(repoUrl)) return null;
|
|
2713
2912
|
return `${repoUrl}/releases`;
|
|
2714
2913
|
}
|
|
2715
|
-
function isMajorVersionChange(current, latest) {
|
|
2716
|
-
const from = parseComparableVersion(current);
|
|
2717
|
-
const to = parseComparableVersion(latest);
|
|
2718
|
-
return !!from && !!to && to.major > from.major;
|
|
2719
|
-
}
|
|
2720
2914
|
async function runPnpm(args, cwd) {
|
|
2721
2915
|
try {
|
|
2722
2916
|
const { stdout, stderr } = await execFileAsync2("pnpm", args, {
|
|
@@ -2742,9 +2936,23 @@ async function runPnpmAudit(cwd) {
|
|
|
2742
2936
|
if (!result || typeof result.advisories !== "object" || result.advisories === null) return null;
|
|
2743
2937
|
return result;
|
|
2744
2938
|
}
|
|
2939
|
+
async function readReleaseAgeMinutes(cwd) {
|
|
2940
|
+
const minutes = Number.parseInt((await runPnpm(["config", "get", "minimumReleaseAge"], cwd)).stdout, 10);
|
|
2941
|
+
return Number.isNaN(minutes) ? 0 : minutes;
|
|
2942
|
+
}
|
|
2745
2943
|
async function fetchPublishTimes(name, cwd) {
|
|
2746
2944
|
const times = await runPnpmJson(["view", name, "time", "--json"], cwd);
|
|
2747
|
-
return times && typeof times === "object" ? times : null;
|
|
2945
|
+
return times && typeof times === "object" && !("error" in times) ? times : null;
|
|
2946
|
+
}
|
|
2947
|
+
function editJsonText(content, change) {
|
|
2948
|
+
const json = JSON.parse(content);
|
|
2949
|
+
change(json);
|
|
2950
|
+
const indent = content.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
|
|
2951
|
+
return `${JSON.stringify(json, null, indent)}${content.endsWith("\n") ? "\n" : ""}`;
|
|
2952
|
+
}
|
|
2953
|
+
function rewriteSpecifier(spec, latest) {
|
|
2954
|
+
const match = spec.match(/^([\^~]?)\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?$/);
|
|
2955
|
+
return match ? `${match[1]}${latest}` : null;
|
|
2748
2956
|
}
|
|
2749
2957
|
var vulnIcons = {
|
|
2750
2958
|
critical: colors_default.red("\u25CF"),
|
|
@@ -2770,6 +2978,265 @@ function formatDependents(dependents) {
|
|
|
2770
2978
|
return `${dependents[0]} +${dependents.length - 1}`;
|
|
2771
2979
|
}
|
|
2772
2980
|
|
|
2981
|
+
// src/utils/local-tools.ts
|
|
2982
|
+
import { execFile as execFile3 } from "child_process";
|
|
2983
|
+
import { existsSync as existsSync7, realpathSync as realpathSync2 } from "fs";
|
|
2984
|
+
import { tmpdir } from "os";
|
|
2985
|
+
import { delimiter, dirname as dirname6, join as join11, sep as sep2 } from "path";
|
|
2986
|
+
import { promisify as promisify3 } from "util";
|
|
2987
|
+
|
|
2988
|
+
// src/utils/runtimes.ts
|
|
2989
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
2990
|
+
import { basename as basename3, dirname as dirname5, join as join10 } from "path";
|
|
2991
|
+
var DAY = 24 * 60 * 60 * 1e3;
|
|
2992
|
+
var FROZEN_AFTER = 180 * DAY;
|
|
2993
|
+
var RECENT_PUSH = 30 * DAY;
|
|
2994
|
+
var EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
2995
|
+
var FROM_LINE = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?\s*$/i;
|
|
2996
|
+
var NODE_ARG_TAG = /^(?:docker\.io\/)?(?:library\/)?node:\$\{?\w+\}?([\w.-]*)$/;
|
|
2997
|
+
var MINOR_TAG = /^(\d+(?:\.\d+){1,2})(-[\w.-]+)?$/;
|
|
2998
|
+
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(basename3(path));
|
|
2999
|
+
function parseDockerfile(file, content) {
|
|
3000
|
+
const images = [];
|
|
3001
|
+
const nodeImageSuffixes = [];
|
|
3002
|
+
const stages = /* @__PURE__ */ new Set();
|
|
3003
|
+
content.split("\n").forEach((text, line) => {
|
|
3004
|
+
const match = text.match(FROM_LINE);
|
|
3005
|
+
if (!match) return;
|
|
3006
|
+
const [, ref, stage] = match;
|
|
3007
|
+
const fromStage = stages.has(ref.toLowerCase());
|
|
3008
|
+
if (stage) stages.add(stage.toLowerCase());
|
|
3009
|
+
if (fromStage || ref.includes("@")) return;
|
|
3010
|
+
const nodeArg = ref.match(NODE_ARG_TAG);
|
|
3011
|
+
if (nodeArg) {
|
|
3012
|
+
nodeImageSuffixes.push(nodeArg[1]);
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
if (ref.includes("$")) return;
|
|
3016
|
+
const slash = ref.lastIndexOf("/");
|
|
3017
|
+
const colon = ref.lastIndexOf(":");
|
|
3018
|
+
if (colon <= slash) return;
|
|
3019
|
+
let image = ref.slice(0, colon);
|
|
3020
|
+
const tag = ref.slice(colon + 1);
|
|
3021
|
+
const host = image.includes("/") ? image.split("/")[0] : "";
|
|
3022
|
+
if (host === "docker.io") image = image.slice(host.length + 1).replace(/^library\//, "");
|
|
3023
|
+
else if (host.includes(".") || host.includes(":") || host === "localhost") return;
|
|
3024
|
+
if (image === "scratch") return;
|
|
3025
|
+
images.push({ file, line, image, tag });
|
|
3026
|
+
});
|
|
3027
|
+
return { images, nodeImageSuffixes };
|
|
3028
|
+
}
|
|
3029
|
+
async function readRuntimePins(forkPath) {
|
|
3030
|
+
let manifest = {};
|
|
3031
|
+
try {
|
|
3032
|
+
manifest = JSON.parse(readFileSync2(join10(forkPath, "package.json"), "utf8"));
|
|
3033
|
+
} catch {
|
|
3034
|
+
}
|
|
3035
|
+
const node = manifest.engines?.node?.trim() ?? "";
|
|
3036
|
+
const pnpm = manifest.packageManager?.match(/^pnpm@(\d+\.\d+\.\d+)(?:\+.*)?$/)?.[1] ?? null;
|
|
3037
|
+
const tracked = (await git(["ls-files", "-z"], forkPath, { ignoreErrors: true })).split("\0").filter(isDockerfile);
|
|
3038
|
+
const images = [];
|
|
3039
|
+
const nodeImageSuffixes = /* @__PURE__ */ new Set();
|
|
3040
|
+
for (const file of tracked) {
|
|
3041
|
+
let content;
|
|
3042
|
+
try {
|
|
3043
|
+
content = readFileSync2(join10(forkPath, file), "utf8");
|
|
3044
|
+
} catch {
|
|
3045
|
+
continue;
|
|
3046
|
+
}
|
|
3047
|
+
const parsed = parseDockerfile(file, content);
|
|
3048
|
+
images.push(...parsed.images);
|
|
3049
|
+
for (const suffix of parsed.nodeImageSuffixes) nodeImageSuffixes.add(suffix);
|
|
3050
|
+
}
|
|
3051
|
+
return {
|
|
3052
|
+
node: EXACT_VERSION.test(node) ? node : null,
|
|
3053
|
+
pnpm,
|
|
3054
|
+
images,
|
|
3055
|
+
nodeImageSuffixes: [...nodeImageSuffixes]
|
|
3056
|
+
};
|
|
3057
|
+
}
|
|
3058
|
+
async function fetchJson(url, init) {
|
|
3059
|
+
try {
|
|
3060
|
+
const response = await fetch(url, { ...init, signal: AbortSignal.timeout(1e4) });
|
|
3061
|
+
return response.ok ? await response.json() : null;
|
|
3062
|
+
} catch {
|
|
3063
|
+
return null;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
var hubRepository = (image) => image.includes("/") ? image : `library/${image}`;
|
|
3067
|
+
var hubTag = (entry) => {
|
|
3068
|
+
const { name, last_updated } = entry ?? {};
|
|
3069
|
+
const pushedAt = Date.parse(last_updated ?? "");
|
|
3070
|
+
return name && !Number.isNaN(pushedAt) ? { name, pushedAt } : null;
|
|
3071
|
+
};
|
|
3072
|
+
async function fetchNodeReleases(major, now) {
|
|
3073
|
+
const index = await fetchJson("https://nodejs.org/dist/index.json");
|
|
3074
|
+
if (!Array.isArray(index)) return null;
|
|
3075
|
+
const releases = index.map((entry) => entry).filter((entry) => entry.version && parseComparableVersion(entry.version)?.major === major).map((entry) => ({
|
|
3076
|
+
version: String(entry.version).replace(/^v/, ""),
|
|
3077
|
+
publishedAt: Date.parse(entry.date ?? ""),
|
|
3078
|
+
security: entry.security === true
|
|
3079
|
+
})).filter((release) => !Number.isNaN(release.publishedAt));
|
|
3080
|
+
await Promise.all(
|
|
3081
|
+
releases.filter((release) => now - release.publishedAt < 2 * DAY).map(async (release) => {
|
|
3082
|
+
try {
|
|
3083
|
+
const response = await fetch(`https://nodejs.org/dist/v${release.version}/SHASUMS256.txt`, {
|
|
3084
|
+
method: "HEAD",
|
|
3085
|
+
signal: AbortSignal.timeout(1e4)
|
|
3086
|
+
});
|
|
3087
|
+
const modified = Date.parse(response.headers.get("last-modified") ?? "");
|
|
3088
|
+
release.publishedAt = Number.isNaN(modified) ? release.publishedAt + DAY : modified;
|
|
3089
|
+
} catch {
|
|
3090
|
+
release.publishedAt += DAY;
|
|
3091
|
+
}
|
|
3092
|
+
})
|
|
3093
|
+
);
|
|
3094
|
+
return releases;
|
|
3095
|
+
}
|
|
3096
|
+
async function fetchImageTags(pin) {
|
|
3097
|
+
const shape = pin.tag.match(MINOR_TAG);
|
|
3098
|
+
if (!shape) return null;
|
|
3099
|
+
const base = `https://hub.docker.com/v2/repositories/${hubRepository(pin.image)}/tags`;
|
|
3100
|
+
const filter = shape[2] ?? `${shape[1].split(".")[0]}.`;
|
|
3101
|
+
const [list, pinned] = await Promise.all([
|
|
3102
|
+
fetchJson(`${base}?page_size=100&ordering=last_updated&name=${encodeURIComponent(filter)}`),
|
|
3103
|
+
fetchJson(`${base}/${encodeURIComponent(pin.tag)}`)
|
|
3104
|
+
]);
|
|
3105
|
+
const results = list?.results;
|
|
3106
|
+
if (!Array.isArray(results)) return null;
|
|
3107
|
+
return { pinned: hubTag(pinned), tags: results.flatMap((entry) => hubTag(entry) ?? []) };
|
|
3108
|
+
}
|
|
3109
|
+
async function fetchPnpmAdvisories(version) {
|
|
3110
|
+
const answer = await fetchJson("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {
|
|
3111
|
+
method: "POST",
|
|
3112
|
+
headers: { "content-type": "application/json" },
|
|
3113
|
+
body: JSON.stringify({ pnpm: [version] })
|
|
3114
|
+
});
|
|
3115
|
+
return answer ? answer.pnpm ?? [] : null;
|
|
3116
|
+
}
|
|
3117
|
+
async function fetchRuntimeFeeds(pins, cwd, now) {
|
|
3118
|
+
const nodeMajor = pins.node ? parseComparableVersion(pins.node)?.major : void 0;
|
|
3119
|
+
const [node, published, advisories, images] = await Promise.all([
|
|
3120
|
+
nodeMajor === void 0 ? null : fetchNodeReleases(nodeMajor, now),
|
|
3121
|
+
pins.pnpm ? fetchPublishTimes("pnpm", cwd) : null,
|
|
3122
|
+
pins.pnpm ? fetchPnpmAdvisories(pins.pnpm) : null,
|
|
3123
|
+
Promise.all(pins.images.map(fetchImageTags))
|
|
3124
|
+
]);
|
|
3125
|
+
return { node, pnpm: published && advisories ? { published, advisories } : null, images };
|
|
3126
|
+
}
|
|
3127
|
+
var count = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
3128
|
+
var monthYear = (time) => new Date(time).toLocaleString("en", { month: "short", year: "numeric", timeZone: "UTC" }).toLowerCase();
|
|
3129
|
+
function newestAged(pinned, versions, cutoff) {
|
|
3130
|
+
const major = parseComparableVersion(pinned)?.major;
|
|
3131
|
+
return versions.filter(
|
|
3132
|
+
(entry) => !isPrerelease(entry.version) && parseComparableVersion(entry.version)?.major === major && compareVersionStrings(entry.version, pinned) > 0 && entry.publishedAt <= cutoff
|
|
3133
|
+
).sort((a, b) => compareVersionStrings(b.version, a.version))[0];
|
|
3134
|
+
}
|
|
3135
|
+
function planRuntimeUpdates(pins, feeds, options) {
|
|
3136
|
+
const cutoff = options.now - options.releaseAgeMinutes * 60 * 1e3;
|
|
3137
|
+
const updates = [];
|
|
3138
|
+
const unchecked = [];
|
|
3139
|
+
if (pins.node) {
|
|
3140
|
+
if (!feeds.node) unchecked.push("node");
|
|
3141
|
+
const target = feeds.node && newestAged(pins.node, feeds.node, cutoff);
|
|
3142
|
+
if (target) {
|
|
3143
|
+
const node = pins.node;
|
|
3144
|
+
const security = (feeds.node ?? []).some(
|
|
3145
|
+
(release) => release.security && compareVersionStrings(release.version, node) > 0 && compareVersionStrings(release.version, target.version) <= 0
|
|
3146
|
+
);
|
|
3147
|
+
updates.push({
|
|
3148
|
+
name: "node",
|
|
3149
|
+
current: node,
|
|
3150
|
+
target: target.version,
|
|
3151
|
+
file: "package.json",
|
|
3152
|
+
severity: security ? "high" : null,
|
|
3153
|
+
note: security ? "security release" : "",
|
|
3154
|
+
releasesUrl: `https://nodejs.org/en/blog/release/v${target.version}`,
|
|
3155
|
+
pin: { kind: "node" }
|
|
3156
|
+
});
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
if (pins.pnpm) {
|
|
3160
|
+
if (!feeds.pnpm) unchecked.push("pnpm");
|
|
3161
|
+
const published = Object.entries(feeds.pnpm?.published ?? {}).map(([version, time]) => ({
|
|
3162
|
+
version,
|
|
3163
|
+
publishedAt: Date.parse(time)
|
|
3164
|
+
}));
|
|
3165
|
+
const target = feeds.pnpm && newestAged(pins.pnpm, published, cutoff);
|
|
3166
|
+
if (target) {
|
|
3167
|
+
const pnpm = pins.pnpm;
|
|
3168
|
+
const fixed = (feeds.pnpm?.advisories ?? []).filter(
|
|
3169
|
+
(advisory) => satisfiesComparators(pnpm, advisory.vulnerable_versions) && !satisfiesComparators(target.version, advisory.vulnerable_versions)
|
|
3170
|
+
).length;
|
|
3171
|
+
updates.push({
|
|
3172
|
+
name: "pnpm",
|
|
3173
|
+
current: pnpm,
|
|
3174
|
+
target: target.version,
|
|
3175
|
+
file: "package.json",
|
|
3176
|
+
severity: fixed > 0 ? "high" : null,
|
|
3177
|
+
note: fixed > 0 ? count(fixed, "advisory", "advisories") : "",
|
|
3178
|
+
releasesUrl: `https://github.com/pnpm/pnpm/releases/tag/v${target.version}`,
|
|
3179
|
+
pin: { kind: "pnpm" }
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
pins.images.forEach((pin, i) => {
|
|
3184
|
+
const shape = pin.tag.match(MINOR_TAG);
|
|
3185
|
+
if (!shape) return;
|
|
3186
|
+
const feed = feeds.images[i];
|
|
3187
|
+
if (!feed) {
|
|
3188
|
+
unchecked.push(pin.image);
|
|
3189
|
+
return;
|
|
3190
|
+
}
|
|
3191
|
+
const [, version, suffix = ""] = shape;
|
|
3192
|
+
const parts = version.split(".").length;
|
|
3193
|
+
const major = version.split(".")[0];
|
|
3194
|
+
const target = feed.tags.map((tag) => ({ tag, match: tag.name.match(MINOR_TAG) })).filter(
|
|
3195
|
+
({ match }) => match && (match[2] ?? "") === suffix && match[1].split(".").length === parts && match[1].split(".")[0] === major && compareVersionStrings(match[1], version) > 0
|
|
3196
|
+
).sort((a, b) => compareVersionStrings(b.match?.[1] ?? "", a.match?.[1] ?? ""))[0];
|
|
3197
|
+
if (!target?.match) return;
|
|
3198
|
+
const frozen = !!feed.pinned && options.now - feed.pinned.pushedAt > FROZEN_AFTER && options.now - target.tag.pushedAt < RECENT_PUSH;
|
|
3199
|
+
const repository = hubRepository(pin.image);
|
|
3200
|
+
updates.push({
|
|
3201
|
+
name: pin.image,
|
|
3202
|
+
current: version,
|
|
3203
|
+
target: target.match[1],
|
|
3204
|
+
file: pin.file,
|
|
3205
|
+
severity: frozen ? "moderate" : null,
|
|
3206
|
+
note: frozen && feed.pinned ? `tag frozen since ${monthYear(feed.pinned.pushedAt)}` : "",
|
|
3207
|
+
releasesUrl: repository.startsWith("library/") ? `https://hub.docker.com/_/${pin.image}/tags` : `https://hub.docker.com/r/${repository}/tags`,
|
|
3208
|
+
pin: { kind: "image", line: pin.line, ref: `${pin.image}:${pin.tag}`, to: `${pin.image}:${target.tag.name}` }
|
|
3209
|
+
});
|
|
3210
|
+
});
|
|
3211
|
+
return { updates, unchecked };
|
|
3212
|
+
}
|
|
3213
|
+
function applyRuntimeUpdate(content, update) {
|
|
3214
|
+
const { pin } = update;
|
|
3215
|
+
if (pin.kind === "image") {
|
|
3216
|
+
const lines = content.split("\n");
|
|
3217
|
+
lines[pin.line] = lines[pin.line].replace(pin.ref, pin.to);
|
|
3218
|
+
return lines.join("\n");
|
|
3219
|
+
}
|
|
3220
|
+
return editJsonText(content, (json) => {
|
|
3221
|
+
if (pin.kind === "node") json.engines.node = update.target;
|
|
3222
|
+
else json.packageManager = `pnpm@${update.target}`;
|
|
3223
|
+
});
|
|
3224
|
+
}
|
|
3225
|
+
async function missingNodeImages(version, suffixes) {
|
|
3226
|
+
const missing = await Promise.all(
|
|
3227
|
+
suffixes.map(async (suffix) => {
|
|
3228
|
+
const tag = `${version}${suffix}`;
|
|
3229
|
+
const found = await fetchJson(`https://hub.docker.com/v2/repositories/library/node/tags/${tag}`);
|
|
3230
|
+
return found ? [] : [`node:${tag}`];
|
|
3231
|
+
})
|
|
3232
|
+
);
|
|
3233
|
+
return missing.flat();
|
|
3234
|
+
}
|
|
3235
|
+
function pinLocation(file) {
|
|
3236
|
+
const dir = dirname5(file);
|
|
3237
|
+
return dir === "." ? file : dir;
|
|
3238
|
+
}
|
|
3239
|
+
|
|
2773
3240
|
// src/utils/security-plan.ts
|
|
2774
3241
|
var SEVERITIES = ["critical", "high", "moderate", "low", "info"];
|
|
2775
3242
|
function isAuditOverride(key) {
|
|
@@ -2885,6 +3352,130 @@ function liveExcludes(input) {
|
|
|
2885
3352
|
});
|
|
2886
3353
|
}
|
|
2887
3354
|
|
|
3355
|
+
// src/utils/local-tools.ts
|
|
3356
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3357
|
+
function findOnPath(name, skip = []) {
|
|
3358
|
+
for (const dir of (getEnv("PATH") ?? "").split(delimiter)) {
|
|
3359
|
+
if (!dir || skip.includes(dir) || dir.endsWith(`node_modules${sep2}.bin`)) continue;
|
|
3360
|
+
if (existsSync7(join11(dir, name))) return join11(dir, name);
|
|
3361
|
+
}
|
|
3362
|
+
return null;
|
|
3363
|
+
}
|
|
3364
|
+
function installerOf(file) {
|
|
3365
|
+
let real = file;
|
|
3366
|
+
try {
|
|
3367
|
+
real = realpathSync2(file);
|
|
3368
|
+
} catch {
|
|
3369
|
+
}
|
|
3370
|
+
if (real.includes(`${sep2}.volta${sep2}`)) return "volta";
|
|
3371
|
+
return /[\\/](Cellar|homebrew|linuxbrew)[\\/]/.test(real) ? "brew" : null;
|
|
3372
|
+
}
|
|
3373
|
+
async function versionOf(file, cwd) {
|
|
3374
|
+
try {
|
|
3375
|
+
const { stdout } = await execFileAsync3(file, ["--version"], { encoding: "utf8", timeout: 1e4, cwd });
|
|
3376
|
+
return stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
3377
|
+
} catch {
|
|
3378
|
+
return null;
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
async function readNode(now) {
|
|
3382
|
+
const version = process.version.slice(1);
|
|
3383
|
+
const major = parseComparableVersion(version)?.major ?? 0;
|
|
3384
|
+
return { version, installer: installerOf(process.execPath), releases: await fetchNodeReleases(major, now) };
|
|
3385
|
+
}
|
|
3386
|
+
async function readPnpm(cwd) {
|
|
3387
|
+
const running = getEnv("npm_execpath");
|
|
3388
|
+
const file = findOnPath("pnpm", running ? [dirname6(running)] : []);
|
|
3389
|
+
const version = file && await versionOf(file, tmpdir());
|
|
3390
|
+
if (!file || !version) return null;
|
|
3391
|
+
const [published, advisories] = await Promise.all([fetchPublishTimes("pnpm", cwd), fetchPnpmAdvisories(version)]);
|
|
3392
|
+
return { version, installer: installerOf(file), published, advisories };
|
|
3393
|
+
}
|
|
3394
|
+
async function fetchOsvSeverities(module, version) {
|
|
3395
|
+
const answer = await fetchJson("https://api.osv.dev/v1/query", {
|
|
3396
|
+
method: "POST",
|
|
3397
|
+
body: JSON.stringify({ package: { name: module, ecosystem: "Go" }, version })
|
|
3398
|
+
});
|
|
3399
|
+
if (!answer) return null;
|
|
3400
|
+
const byGhsa = /* @__PURE__ */ new Map();
|
|
3401
|
+
for (const vuln of answer.vulns ?? []) {
|
|
3402
|
+
const id = [vuln.id, ...vuln.aliases ?? []].find((alias) => alias.startsWith("GHSA-")) ?? vuln.id;
|
|
3403
|
+
const severity = SEVERITIES.find((level) => level === vuln.database_specific?.severity?.toLowerCase());
|
|
3404
|
+
if (severity || !byGhsa.has(id)) byGhsa.set(id, severity ?? "moderate");
|
|
3405
|
+
}
|
|
3406
|
+
return [...byGhsa.values()];
|
|
3407
|
+
}
|
|
3408
|
+
async function readGh() {
|
|
3409
|
+
const file = findOnPath("gh");
|
|
3410
|
+
const version = file && await versionOf(file);
|
|
3411
|
+
if (!file || !version) return null;
|
|
3412
|
+
const [list, advisories] = await Promise.all([
|
|
3413
|
+
fetchJson("https://api.github.com/repos/cli/cli/releases?per_page=30"),
|
|
3414
|
+
fetchOsvSeverities("github.com/cli/cli/v2", version)
|
|
3415
|
+
]);
|
|
3416
|
+
const releases = Array.isArray(list) ? list.flatMap((entry) => {
|
|
3417
|
+
const { tag_name, published_at } = entry;
|
|
3418
|
+
const publishedAt = Date.parse(published_at ?? "");
|
|
3419
|
+
return tag_name && !Number.isNaN(publishedAt) ? [{ version: tag_name.replace(/^v/, ""), publishedAt }] : [];
|
|
3420
|
+
}) : null;
|
|
3421
|
+
return { version, installer: installerOf(file), releases, advisories };
|
|
3422
|
+
}
|
|
3423
|
+
async function readLocalTools(cwd, now) {
|
|
3424
|
+
const [node, pnpm, gh] = await Promise.all([readNode(now), readPnpm(cwd), readGh()]);
|
|
3425
|
+
return { node, pnpm, gh };
|
|
3426
|
+
}
|
|
3427
|
+
function upgradeCommand(name, installer, target) {
|
|
3428
|
+
if (installer === "brew") return `brew upgrade ${name}`;
|
|
3429
|
+
if (!target) return "no fixed release older than the release age yet";
|
|
3430
|
+
if (installer === "volta") return `volta install ${name}@${target}`;
|
|
3431
|
+
return name === "pnpm" ? `pnpm self-update ${target}` : `update to ${target}`;
|
|
3432
|
+
}
|
|
3433
|
+
function toCheck(name, tool, missed, target, noun) {
|
|
3434
|
+
if (!missed)
|
|
3435
|
+
return { name, version: tool.version, severity: null, note: "not checked, no answer from the feed", fix: "" };
|
|
3436
|
+
if (missed.length === 0) return { name, version: tool.version, severity: null, note: "", fix: "" };
|
|
3437
|
+
const high = missed.filter((severity) => severity === "critical" || severity === "high").length;
|
|
3438
|
+
return {
|
|
3439
|
+
name,
|
|
3440
|
+
version: tool.version,
|
|
3441
|
+
severity: SEVERITIES.find((severity) => missed.includes(severity)) ?? null,
|
|
3442
|
+
note: `${plural(missed.length, ...noun)}${high > 0 && high < missed.length ? `, ${high} high` : ""}`,
|
|
3443
|
+
fix: upgradeCommand(name, tool.installer, target)
|
|
3444
|
+
};
|
|
3445
|
+
}
|
|
3446
|
+
function planLocalTools(tools, options) {
|
|
3447
|
+
const cutoff = options.now - options.releaseAgeMinutes * 60 * 1e3;
|
|
3448
|
+
const { node, pnpm, gh } = tools;
|
|
3449
|
+
const nodeTarget = node.releases ? newestAged(node.version, node.releases, cutoff) : void 0;
|
|
3450
|
+
const securityReleases = (node.releases ?? []).filter(
|
|
3451
|
+
(release) => release.security && nodeTarget && compareVersionStrings(release.version, node.version) > 0 && compareVersionStrings(release.version, nodeTarget.version) <= 0
|
|
3452
|
+
);
|
|
3453
|
+
const checks = [
|
|
3454
|
+
toCheck(
|
|
3455
|
+
"node",
|
|
3456
|
+
node,
|
|
3457
|
+
node.releases && securityReleases.map(() => "high"),
|
|
3458
|
+
nodeTarget?.version,
|
|
3459
|
+
["security release", "security releases"]
|
|
3460
|
+
)
|
|
3461
|
+
];
|
|
3462
|
+
if (pnpm) {
|
|
3463
|
+
const affecting = (version) => (pnpm.advisories ?? []).filter((advisory) => satisfiesComparators(version, advisory.vulnerable_versions));
|
|
3464
|
+
const fixed = Object.entries(pnpm.published ?? {}).map(([version, time]) => ({ version, publishedAt: Date.parse(time) })).filter(
|
|
3465
|
+
(release) => parseComparableVersion(release.version) && !isPrerelease(release.version) && release.publishedAt <= cutoff && compareVersionStrings(release.version, pnpm.version) > 0 && affecting(release.version).length === 0
|
|
3466
|
+
).sort((a, b) => compareVersionStrings(b.version, a.version));
|
|
3467
|
+
const major = parseComparableVersion(pnpm.version)?.major;
|
|
3468
|
+
const target = fixed.find((release) => parseComparableVersion(release.version)?.major === major) ?? fixed[0];
|
|
3469
|
+
const missed = pnpm.advisories && pnpm.published && affecting(pnpm.version).map((advisory) => advisory.severity);
|
|
3470
|
+
checks.push(toCheck("pnpm", pnpm, missed, target?.version, ["advisory", "advisories"]));
|
|
3471
|
+
}
|
|
3472
|
+
if (gh) {
|
|
3473
|
+
const target = gh.releases ? newestAged(gh.version, gh.releases, cutoff) : void 0;
|
|
3474
|
+
checks.push(toCheck("gh", gh, gh.advisories, target?.version, ["advisory", "advisories"]));
|
|
3475
|
+
}
|
|
3476
|
+
return checks;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
2888
3479
|
// src/utils/workspace-yaml.ts
|
|
2889
3480
|
import { isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
2890
3481
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -2965,37 +3556,37 @@ function writeAuditSettings(content, next) {
|
|
|
2965
3556
|
|
|
2966
3557
|
// src/services/audit-security.ts
|
|
2967
3558
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2968
|
-
import { cpSync, existsSync as
|
|
2969
|
-
import { tmpdir } from "os";
|
|
2970
|
-
import { dirname as
|
|
3559
|
+
import { cpSync, existsSync as existsSync8, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync, writeFileSync as writeFileSync2 } from "fs";
|
|
3560
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3561
|
+
import { dirname as dirname7, join as join12, relative as relative2 } from "path";
|
|
2971
3562
|
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
2972
3563
|
var WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
2973
3564
|
var LOCK_FILE = "pnpm-lock.yaml";
|
|
2974
3565
|
var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
|
|
2975
|
-
var readOr = (path, fallback = "") =>
|
|
2976
|
-
var
|
|
3566
|
+
var readOr = (path, fallback = "") => existsSync8(path) ? readFileSync3(path, "utf8") : fallback;
|
|
3567
|
+
var count2 = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
2977
3568
|
var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
|
|
2978
3569
|
var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
|
|
2979
3570
|
var sameEntries = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([key, value]) => b[key] === value);
|
|
2980
3571
|
async function createStaging(forkPath, workspaceYaml) {
|
|
2981
|
-
const staging = mkdtempSync(
|
|
3572
|
+
const staging = mkdtempSync(join12(tmpdir2(), "cella-audit-"));
|
|
2982
3573
|
const projects = await runPnpmJson(["ls", "-r", "--depth", "-1", "--json"], forkPath) ?? [];
|
|
2983
3574
|
const files = [
|
|
2984
3575
|
...RESOLUTION_FILES,
|
|
2985
3576
|
...readPatchFiles(workspaceYaml),
|
|
2986
|
-
...projects.map((project) =>
|
|
3577
|
+
...projects.map((project) => join12(relative2(forkPath, project.path), "package.json"))
|
|
2987
3578
|
];
|
|
2988
3579
|
for (const file of new Set(files)) {
|
|
2989
|
-
if (file.startsWith("..") || !
|
|
2990
|
-
|
|
2991
|
-
cpSync(
|
|
3580
|
+
if (file.startsWith("..") || !existsSync8(join12(forkPath, file))) continue;
|
|
3581
|
+
mkdirSync2(dirname7(join12(staging, file)), { recursive: true });
|
|
3582
|
+
cpSync(join12(forkPath, file), join12(staging, file));
|
|
2992
3583
|
}
|
|
2993
3584
|
return staging;
|
|
2994
3585
|
}
|
|
2995
3586
|
async function resolveStaging(staging, workspaceYaml, settings) {
|
|
2996
|
-
|
|
3587
|
+
writeFileSync2(join12(staging, WORKSPACE_FILE), writeAuditSettings(workspaceYaml, settings));
|
|
2997
3588
|
const result = await runPnpm(["install", "--lockfile-only", "--no-frozen-lockfile", "--ignore-scripts"], staging);
|
|
2998
|
-
return result.ok ? { lock: readOr(
|
|
3589
|
+
return result.ok ? { lock: readOr(join12(staging, LOCK_FILE)) } : { error: `${result.stdout}${result.stderr}`.trim() };
|
|
2999
3590
|
}
|
|
3000
3591
|
function lockedVersions(lock, name) {
|
|
3001
3592
|
const escaped = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
@@ -3096,9 +3687,11 @@ function changeLine(label, before, after) {
|
|
|
3096
3687
|
const counts = moved ? `${before.length} \u2192 ${after.length}` : String(after.length);
|
|
3097
3688
|
return ` ${label.padEnd(9)} ${counts.padEnd(7)} ${colors_default.dim(moved || "unchanged")}`;
|
|
3098
3689
|
}
|
|
3690
|
+
var NO_EDITS = { manifests: {}, files: {} };
|
|
3099
3691
|
async function runSecurityPass(forkPath, options) {
|
|
3100
|
-
const
|
|
3101
|
-
const
|
|
3692
|
+
const { releaseAgeMinutes, edits = NO_EDITS } = options;
|
|
3693
|
+
const workspaceYaml = readOr(join12(forkPath, WORKSPACE_FILE));
|
|
3694
|
+
const workspaceLock = readOr(join12(forkPath, LOCK_FILE));
|
|
3102
3695
|
let previous;
|
|
3103
3696
|
try {
|
|
3104
3697
|
previous = readAuditSettings(workspaceYaml);
|
|
@@ -3106,12 +3699,13 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3106
3699
|
console.error(
|
|
3107
3700
|
`${colors_default.red("\u2717")} ${WORKSPACE_FILE} could not be read: ${error instanceof Error ? error.message : error}`
|
|
3108
3701
|
);
|
|
3109
|
-
return;
|
|
3702
|
+
return false;
|
|
3110
3703
|
}
|
|
3111
3704
|
const auditKeys = Object.keys(previous.overrides).filter(isAuditOverride);
|
|
3112
3705
|
const lasting = Object.fromEntries(Object.entries(previous.overrides).filter(([key]) => !isAuditOverride(key)));
|
|
3706
|
+
const updatesManifests = Object.keys(edits.manifests).length > 0;
|
|
3113
3707
|
const spinner = createSpinner(
|
|
3114
|
-
auditKeys.length > 0 ? `resolving without the ${
|
|
3708
|
+
auditKeys.length > 0 ? `resolving without the ${count2(auditKeys.length, "audit override")}...` : "resolving the workspace..."
|
|
3115
3709
|
);
|
|
3116
3710
|
const fail = (message, detail = "") => {
|
|
3117
3711
|
spinner.stop();
|
|
@@ -3119,16 +3713,21 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3119
3713
|
if (detail) console.error(colors_default.dim(detail.split("\n").slice(-12).join("\n")));
|
|
3120
3714
|
console.info(colors_default.dim(" the workspace is unchanged"));
|
|
3121
3715
|
console.info();
|
|
3716
|
+
return false;
|
|
3122
3717
|
};
|
|
3123
3718
|
const staging = await createStaging(forkPath, workspaceYaml);
|
|
3124
3719
|
try {
|
|
3720
|
+
for (const [file, content] of Object.entries(edits.manifests)) writeFileSync2(join12(staging, file), content);
|
|
3125
3721
|
const bare = await resolveStaging(staging, workspaceYaml, { ...previous, overrides: lasting, ignoredGhsas: [] });
|
|
3126
|
-
if ("error" in bare)
|
|
3722
|
+
if ("error" in bare) {
|
|
3723
|
+
const without = updatesManifests ? "with the selected updates" : "without its audit overrides";
|
|
3724
|
+
return fail(`could not resolve the workspace ${without}`, bare.error);
|
|
3725
|
+
}
|
|
3127
3726
|
spinnerText("checking for vulnerabilities...");
|
|
3128
3727
|
const audit = await runPnpmAudit(staging);
|
|
3129
3728
|
if (!audit) return fail("pnpm audit gave no answer, so nothing can be decided");
|
|
3130
3729
|
const advisories = advisoriesFromAudit(audit);
|
|
3131
|
-
spinnerText(`checking ${
|
|
3730
|
+
spinnerText(`checking ${count2(advisories.length, "advisory", "advisories")} against the registry...`);
|
|
3132
3731
|
const versionedExcludes = previous.releaseAgeExcludes.flatMap((entry) => parseVersionedExclude(entry)?.name ?? []);
|
|
3133
3732
|
const names = [.../* @__PURE__ */ new Set([...advisories.map((advisory) => advisory.module), ...versionedExcludes])];
|
|
3134
3733
|
const times = await Promise.all(names.map((name) => fetchPublishTimes(name, forkPath)));
|
|
@@ -3137,8 +3736,6 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3137
3736
|
);
|
|
3138
3737
|
const unanswered = advisories.map((advisory) => advisory.module).filter((name) => !published[name]);
|
|
3139
3738
|
if (unanswered.length > 0) return fail(`the registry did not answer for ${[...new Set(unanswered)].join(", ")}`);
|
|
3140
|
-
const releaseAge = Number.parseInt((await runPnpm(["config", "get", "minimumReleaseAge"], forkPath)).stdout, 10);
|
|
3141
|
-
const releaseAgeMinutes = Number.isNaN(releaseAge) ? 0 : releaseAge;
|
|
3142
3739
|
const releaseAgeHours = Math.round(releaseAgeMinutes / 60);
|
|
3143
3740
|
const planInput = { advisories, published, previous, releaseAgeMinutes, now: Date.now() };
|
|
3144
3741
|
const items = planSecurity(planInput);
|
|
@@ -3156,60 +3753,72 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3156
3753
|
if (!options.apply) {
|
|
3157
3754
|
if (changes) console.info(colors_default.dim("run without --list to apply"));
|
|
3158
3755
|
console.info();
|
|
3159
|
-
return;
|
|
3756
|
+
return false;
|
|
3160
3757
|
}
|
|
3161
3758
|
let decisions;
|
|
3162
3759
|
try {
|
|
3163
3760
|
decisions = await decide(items, releaseAgeHours);
|
|
3164
3761
|
} catch {
|
|
3165
3762
|
console.info(colors_default.dim(" cancelled, the workspace is unchanged"));
|
|
3166
|
-
return;
|
|
3763
|
+
return false;
|
|
3167
3764
|
}
|
|
3168
3765
|
const open = decisions.filter((decision) => decision.action === "open").map((decision) => decision.item.module);
|
|
3169
3766
|
const kept = Object.assign({}, lasting, ...open.map((module) => standingOverrides(module, planInput)));
|
|
3170
3767
|
const next = settingsFor(decisions, kept, excludes);
|
|
3171
3768
|
const nextYaml = writeAuditSettings(workspaceYaml, next);
|
|
3172
|
-
|
|
3173
|
-
|
|
3769
|
+
const settingsChanged = nextYaml !== workspaceYaml;
|
|
3770
|
+
const otherFiles = Object.keys(edits.files);
|
|
3771
|
+
const writeFiles = () => {
|
|
3772
|
+
for (const [file, content] of Object.entries(edits.files)) writeFileSync2(join12(forkPath, file), content);
|
|
3773
|
+
if (otherFiles.length > 0) console.info(`${checkMark} ${otherFiles.join(", ")} updated`);
|
|
3774
|
+
};
|
|
3775
|
+
if (!settingsChanged && !updatesManifests) {
|
|
3776
|
+
writeFiles();
|
|
3777
|
+
const state = `${count2(auditKeys.length, "override")} \xB7 ${next.ignoredGhsas.length} ignored`;
|
|
3174
3778
|
console.info(`${checkMark} security settings are current ${colors_default.dim(`\xB7 ${state}`)}`);
|
|
3175
3779
|
printOpen(open);
|
|
3176
|
-
return;
|
|
3780
|
+
return true;
|
|
3177
3781
|
}
|
|
3178
3782
|
const summarize = () => {
|
|
3783
|
+
if (!settingsChanged) return;
|
|
3179
3784
|
console.info(colors_default.bold(WORKSPACE_FILE));
|
|
3180
3785
|
console.info(changeLine("overrides", auditKeys, Object.keys(next.overrides).filter(isAuditOverride)));
|
|
3181
3786
|
console.info(changeLine("excludes", previous.releaseAgeExcludes, next.releaseAgeExcludes));
|
|
3182
3787
|
console.info(changeLine("ignored", previous.ignoredGhsas, next.ignoredGhsas));
|
|
3183
3788
|
console.info();
|
|
3184
3789
|
};
|
|
3185
|
-
if (sameEntries(next.overrides, previous.overrides)) {
|
|
3186
|
-
|
|
3790
|
+
if (!updatesManifests && sameEntries(next.overrides, previous.overrides)) {
|
|
3791
|
+
writeFiles();
|
|
3792
|
+
writeFileSync2(join12(forkPath, WORKSPACE_FILE), nextYaml);
|
|
3187
3793
|
console.info(`${checkMark} ${WORKSPACE_FILE} updated ${colors_default.dim("\xB7 nothing to install")}`);
|
|
3188
3794
|
printOpen(open);
|
|
3189
3795
|
summarize();
|
|
3190
|
-
return;
|
|
3796
|
+
return true;
|
|
3191
3797
|
}
|
|
3192
3798
|
const stagingSpinner = createSpinner("resolving with the new settings...");
|
|
3193
3799
|
const staged = await resolveStaging(staging, workspaceYaml, next);
|
|
3194
3800
|
stagingSpinner.stop();
|
|
3195
3801
|
if ("error" in staged) return fail("could not resolve the workspace with the new settings", staged.error);
|
|
3196
|
-
|
|
3197
|
-
|
|
3802
|
+
const written = { ...edits.files, ...edits.manifests, [WORKSPACE_FILE]: nextYaml, [LOCK_FILE]: staged.lock };
|
|
3803
|
+
const before = Object.fromEntries(Object.keys(written).map((file) => [file, readOr(join12(forkPath, file))]));
|
|
3804
|
+
before[WORKSPACE_FILE] = workspaceYaml;
|
|
3805
|
+
before[LOCK_FILE] = workspaceLock;
|
|
3806
|
+
for (const [file, content] of Object.entries(written)) writeFileSync2(join12(forkPath, file), content);
|
|
3198
3807
|
const install = spawnSync2("pnpm", ["install"], { cwd: forkPath, stdio: "inherit" });
|
|
3199
3808
|
console.info();
|
|
3200
3809
|
if (install.status !== 0) {
|
|
3201
|
-
|
|
3202
|
-
writeFileSync(join9(forkPath, LOCK_FILE), workspaceLock);
|
|
3810
|
+
for (const [file, content] of Object.entries(before)) writeFileSync2(join12(forkPath, file), content);
|
|
3203
3811
|
console.error(`${colors_default.red("\u2717")} pnpm install failed (exit code ${install.status})`);
|
|
3204
|
-
console.info(colors_default.dim(` ${
|
|
3812
|
+
console.info(colors_default.dim(` ${Object.keys(before).join(", ")} are back as they were; run pnpm install to match`));
|
|
3205
3813
|
console.info();
|
|
3206
|
-
return;
|
|
3814
|
+
return false;
|
|
3207
3815
|
}
|
|
3208
3816
|
const verified = await runPnpmAudit(forkPath);
|
|
3209
3817
|
const reported = verified ? [...new Set(advisoriesFromAudit(verified).map((advisory) => advisory.module))] : open;
|
|
3210
3818
|
console.info(`${checkMark} installed${reported.length === 0 ? ` ${colors_default.dim("\xB7")} audit clean` : ""}`);
|
|
3211
3819
|
printOpen(reported);
|
|
3212
3820
|
summarize();
|
|
3821
|
+
return true;
|
|
3213
3822
|
} finally {
|
|
3214
3823
|
spinner.stop();
|
|
3215
3824
|
rmSync(staging, { recursive: true, force: true });
|
|
@@ -3217,7 +3826,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3217
3826
|
}
|
|
3218
3827
|
function printOpen(modules) {
|
|
3219
3828
|
if (modules.length > 0) {
|
|
3220
|
-
console.info(`${warningMark} ${
|
|
3829
|
+
console.info(`${warningMark} ${count2(modules.length, "package")} still reported: ${modules.join(", ")}`);
|
|
3221
3830
|
}
|
|
3222
3831
|
console.info();
|
|
3223
3832
|
}
|
|
@@ -3250,7 +3859,7 @@ function printPlan(plan) {
|
|
|
3250
3859
|
printDropped("ignore dropped", droppedIgnores);
|
|
3251
3860
|
const lastingCount = Object.keys(plan.lasting).length;
|
|
3252
3861
|
if (lastingCount > 0) {
|
|
3253
|
-
console.info(colors_default.dim(`${
|
|
3862
|
+
console.info(colors_default.dim(`${count2(lastingCount, "override")} without a version selector left as written`));
|
|
3254
3863
|
}
|
|
3255
3864
|
if (items.some(isBreaking)) {
|
|
3256
3865
|
console.info(colors_default.dim("\u26A0 a breaking jump: another major, or another minor below 1.0"));
|
|
@@ -3260,13 +3869,14 @@ function printPlan(plan) {
|
|
|
3260
3869
|
}
|
|
3261
3870
|
|
|
3262
3871
|
// src/services/audit.ts
|
|
3872
|
+
var DEPENDENCY_SECTIONS = ["dependencies", "devDependencies", "optionalDependencies"];
|
|
3263
3873
|
function createDependentPkgReader() {
|
|
3264
3874
|
const cache = /* @__PURE__ */ new Map();
|
|
3265
3875
|
return (location) => {
|
|
3266
3876
|
let cached = cache.get(location);
|
|
3267
3877
|
if (cached === void 0) {
|
|
3268
3878
|
try {
|
|
3269
|
-
cached = JSON.parse(
|
|
3879
|
+
cached = JSON.parse(readFileSync4(join13(location, "package.json"), "utf8"));
|
|
3270
3880
|
} catch {
|
|
3271
3881
|
cached = null;
|
|
3272
3882
|
}
|
|
@@ -3277,85 +3887,111 @@ function createDependentPkgReader() {
|
|
|
3277
3887
|
}
|
|
3278
3888
|
async function runAudit(config) {
|
|
3279
3889
|
const { forkPath } = config;
|
|
3280
|
-
const spinner = createSpinner("checking
|
|
3890
|
+
const spinner = createSpinner("checking packages and runtimes...");
|
|
3281
3891
|
try {
|
|
3282
|
-
const
|
|
3892
|
+
const pins = await readRuntimePins(forkPath);
|
|
3893
|
+
const now = Date.now();
|
|
3894
|
+
const [outdated, auditResult, releaseAgeMinutes, feeds, machine] = await Promise.all([
|
|
3283
3895
|
runPnpmJson(["-r", "outdated", "--json", "--long"], forkPath),
|
|
3284
|
-
runPnpmAudit(forkPath)
|
|
3896
|
+
runPnpmAudit(forkPath),
|
|
3897
|
+
readReleaseAgeMinutes(forkPath),
|
|
3898
|
+
fetchRuntimeFeeds(pins, forkPath, now),
|
|
3899
|
+
readLocalTools(forkPath, now)
|
|
3285
3900
|
]);
|
|
3286
3901
|
if (!outdated) {
|
|
3287
3902
|
spinnerFail("pnpm outdated gave no answer: run pnpm -r outdated to see why");
|
|
3288
3903
|
console.info(`${warningMark} security pass skipped: it runs after the update check`);
|
|
3289
3904
|
return;
|
|
3290
3905
|
}
|
|
3291
|
-
const
|
|
3292
|
-
const
|
|
3293
|
-
|
|
3294
|
-
|
|
3906
|
+
const runtimes = planRuntimeUpdates(pins, feeds, { releaseAgeMinutes, now });
|
|
3907
|
+
const localTools = planLocalTools(machine, { releaseAgeMinutes, now });
|
|
3908
|
+
const rows = [
|
|
3909
|
+
...packageRows(outdated, auditResult ? advisoriesFromAudit(auditResult) : []),
|
|
3910
|
+
...runtimes.updates.map(runtimeRow)
|
|
3911
|
+
];
|
|
3912
|
+
if (rows.length === 0) {
|
|
3913
|
+
spinnerSuccess("all packages and runtimes are up to date");
|
|
3914
|
+
printNotes(forkPath, runtimes.unchecked);
|
|
3295
3915
|
console.info();
|
|
3296
|
-
|
|
3916
|
+
printLocalTools(localTools);
|
|
3917
|
+
await runSecurityPass(forkPath, { apply: !config.list, releaseAgeMinutes });
|
|
3297
3918
|
return;
|
|
3298
3919
|
}
|
|
3299
|
-
const advisories = auditResult ? advisoriesFromAudit(auditResult) : [];
|
|
3300
|
-
const severityOf = (name) => SEVERITIES.find((severity) => advisories.some((a) => a.module === name && a.severity === severity)) ?? null;
|
|
3301
|
-
const readDependentPkg = createDependentPkgReader();
|
|
3302
|
-
const enhancedPackages = packageNames.map((name) => {
|
|
3303
|
-
const pkg = outdatedPackages[name];
|
|
3304
|
-
const repoUrl = getRepoUrl(pkg.latestManifest?.repository?.url);
|
|
3305
|
-
const pinnedIn = [];
|
|
3306
|
-
for (const d of pkg.dependentPackages) {
|
|
3307
|
-
const spec = readDependentPkg(d.location)?.[pkg.dependencyType]?.[name] || "";
|
|
3308
|
-
if (spec && !spec.startsWith("^") && !spec.startsWith("~") && !spec.startsWith(">")) {
|
|
3309
|
-
pinnedIn.push(d.name);
|
|
3310
|
-
}
|
|
3311
|
-
}
|
|
3312
|
-
return {
|
|
3313
|
-
name,
|
|
3314
|
-
current: pkg.current,
|
|
3315
|
-
latest: pkg.latest,
|
|
3316
|
-
dependentPackages: pkg.dependentPackages,
|
|
3317
|
-
isDev: pkg.dependencyType === "devDependencies",
|
|
3318
|
-
isMajorUpdate: isMajorVersionChange(pkg.current, pkg.latest),
|
|
3319
|
-
pinnedIn,
|
|
3320
|
-
repoUrl,
|
|
3321
|
-
releasesUrl: getReleasesUrl(repoUrl),
|
|
3322
|
-
severity: severityOf(name)
|
|
3323
|
-
};
|
|
3324
|
-
});
|
|
3325
|
-
enhancedPackages.sort((a, b) => {
|
|
3326
|
-
const depCompare = dependentsText(a).localeCompare(dependentsText(b));
|
|
3327
|
-
if (depCompare !== 0) return depCompare;
|
|
3328
|
-
return a.name.localeCompare(b.name);
|
|
3329
|
-
});
|
|
3330
3920
|
spinner.stop();
|
|
3331
|
-
printOutdatedResults(
|
|
3921
|
+
printOutdatedResults(rows, forkPath);
|
|
3922
|
+
printNotes(forkPath, runtimes.unchecked);
|
|
3332
3923
|
console.info();
|
|
3333
|
-
|
|
3334
|
-
if (
|
|
3335
|
-
|
|
3336
|
-
console.info(`${warningMark} security pass skipped: it needs a workspace that installs`);
|
|
3924
|
+
printLocalTools(localTools);
|
|
3925
|
+
if (config.list) {
|
|
3926
|
+
await runSecurityPass(forkPath, { apply: false, releaseAgeMinutes });
|
|
3337
3927
|
return;
|
|
3338
3928
|
}
|
|
3339
|
-
|
|
3929
|
+
const selected = await promptForUpdates(rows);
|
|
3930
|
+
if (!selected) return;
|
|
3931
|
+
const { edits, bumped } = await planEdits(forkPath, rows, selected, pins.nodeImageSuffixes);
|
|
3932
|
+
if (await runSecurityPass(forkPath, { apply: true, releaseAgeMinutes, edits })) printNext(bumped);
|
|
3340
3933
|
} finally {
|
|
3341
3934
|
spinner.stop();
|
|
3342
3935
|
}
|
|
3343
3936
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3937
|
+
function packageRows(outdated, advisories) {
|
|
3938
|
+
const severityOf = (name) => SEVERITIES.find((severity) => advisories.some((a) => a.module === name && a.severity === severity)) ?? null;
|
|
3939
|
+
const readDependentPkg = createDependentPkgReader();
|
|
3940
|
+
const rows = Object.entries(outdated).map(([key, pkg]) => {
|
|
3941
|
+
const name = outdatedPackageName(key);
|
|
3942
|
+
const repoUrl = getRepoUrl(pkg.latestManifest?.repository?.url);
|
|
3943
|
+
const current = pkg.current ?? "missing";
|
|
3944
|
+
const pinnedIn = [];
|
|
3945
|
+
for (const d of pkg.dependentPackages) {
|
|
3946
|
+
const spec = readDependentPkg(d.location)?.[pkg.dependencyType]?.[name] || "";
|
|
3947
|
+
if (spec && !spec.startsWith("^") && !spec.startsWith("~") && !spec.startsWith(">")) {
|
|
3948
|
+
pinnedIn.push(d.name);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
return {
|
|
3952
|
+
key,
|
|
3953
|
+
name,
|
|
3954
|
+
current,
|
|
3955
|
+
latest: pkg.latest,
|
|
3956
|
+
dependentPackages: pkg.dependentPackages,
|
|
3957
|
+
isDev: pkg.dependencyType === "devDependencies",
|
|
3958
|
+
breaking: isBreakingJump(current, pkg.latest),
|
|
3959
|
+
pinnedIn,
|
|
3960
|
+
repoUrl,
|
|
3961
|
+
releasesUrl: getReleasesUrl(repoUrl),
|
|
3962
|
+
severity: severityOf(name)
|
|
3963
|
+
};
|
|
3964
|
+
});
|
|
3965
|
+
return rows.sort((a, b) => inText(a).localeCompare(inText(b)) || a.name.localeCompare(b.name));
|
|
3966
|
+
}
|
|
3967
|
+
function runtimeRow(update) {
|
|
3968
|
+
return {
|
|
3969
|
+
key: `runtime:${update.file}:${update.name}`,
|
|
3970
|
+
name: update.name,
|
|
3971
|
+
current: update.current,
|
|
3972
|
+
latest: update.target,
|
|
3973
|
+
dependentPackages: [],
|
|
3974
|
+
isDev: false,
|
|
3975
|
+
breaking: isBreakingJump(update.current, update.target),
|
|
3976
|
+
pinnedIn: [],
|
|
3977
|
+
repoUrl: null,
|
|
3978
|
+
releasesUrl: update.releasesUrl,
|
|
3979
|
+
severity: update.severity,
|
|
3980
|
+
runtime: update
|
|
3981
|
+
};
|
|
3982
|
+
}
|
|
3983
|
+
var isRed = (row) => row.severity === "critical" || row.severity === "high";
|
|
3984
|
+
var inText = (row) => row.runtime ? pinLocation(row.runtime.file) : formatDependents(row.dependentPackages.map((d) => d.name));
|
|
3985
|
+
function printOutdatedResults(rows, forkPath) {
|
|
3346
3986
|
const DEV_TAG = " (dev)";
|
|
3987
|
+
const RUNTIME_TAG = " (runtime)";
|
|
3347
3988
|
const MAX_NAME_LEN = 35;
|
|
3348
3989
|
const MAX_DEPENDENTS_LEN = 15;
|
|
3349
|
-
const
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
);
|
|
3353
|
-
const
|
|
3354
|
-
const maxLatestLen = Math.max(6, ...enhancedPackages.map((p) => p.latest.length));
|
|
3355
|
-
const maxDependentsLen = Math.min(
|
|
3356
|
-
MAX_DEPENDENTS_LEN,
|
|
3357
|
-
Math.max(10, ...enhancedPackages.map((p) => dependentsText(p).length))
|
|
3358
|
-
);
|
|
3990
|
+
const tag = (row) => row.runtime ? RUNTIME_TAG : row.isDev ? DEV_TAG : "";
|
|
3991
|
+
const maxNameLen = Math.min(MAX_NAME_LEN, Math.max(7, ...rows.map((p) => p.name.length + tag(p).length)));
|
|
3992
|
+
const maxCurrentLen = Math.max(7, ...rows.map((p) => p.current.length));
|
|
3993
|
+
const maxLatestLen = Math.max(6, ...rows.map((p) => p.latest.length));
|
|
3994
|
+
const maxDependentsLen = Math.min(MAX_DEPENDENTS_LEN, Math.max(10, ...rows.map((p) => inText(p).length)));
|
|
3359
3995
|
const header = [
|
|
3360
3996
|
" ",
|
|
3361
3997
|
colors_default.bold("package".padEnd(maxNameLen)),
|
|
@@ -3365,15 +4001,14 @@ function printOutdatedResults(enhancedPackages, forkPath) {
|
|
|
3365
4001
|
colors_default.bold("links")
|
|
3366
4002
|
].join(" ");
|
|
3367
4003
|
console.info(header);
|
|
3368
|
-
for (const pkg of
|
|
4004
|
+
for (const pkg of rows) {
|
|
3369
4005
|
const vulnIndicator = pkg.severity ? getVulnIcon(pkg.severity) : " ";
|
|
3370
|
-
const
|
|
3371
|
-
const displayName = middleTruncate(fullName, maxNameLen);
|
|
4006
|
+
const displayName = middleTruncate(`${pkg.name}${tag(pkg)}`, maxNameLen);
|
|
3372
4007
|
const name = colors_default.white(displayName.padEnd(maxNameLen));
|
|
3373
4008
|
const current = colors_default.red(pkg.current.padEnd(maxCurrentLen));
|
|
3374
4009
|
const latestText = pkg.latest.padEnd(maxLatestLen);
|
|
3375
|
-
const latest = pkg.
|
|
3376
|
-
const dependents = colors_default.dim(middleTruncate(
|
|
4010
|
+
const latest = pkg.breaking ? colors_default.bold(colors_default.green(latestText)) : colors_default.green(latestText);
|
|
4011
|
+
const dependents = colors_default.dim(middleTruncate(inText(pkg), maxDependentsLen).padEnd(maxDependentsLen));
|
|
3377
4012
|
const links = [];
|
|
3378
4013
|
if (pkg.releasesUrl) {
|
|
3379
4014
|
links.push(hyperlink(colors_default.blue("rel"), pkg.releasesUrl));
|
|
@@ -3386,9 +4021,9 @@ function printOutdatedResults(enhancedPackages, forkPath) {
|
|
|
3386
4021
|
}
|
|
3387
4022
|
console.info();
|
|
3388
4023
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
3389
|
-
for (const pkg of
|
|
4024
|
+
for (const pkg of rows) {
|
|
3390
4025
|
for (const { name, location } of pkg.dependentPackages) {
|
|
3391
|
-
const dependentName = name ||
|
|
4026
|
+
const dependentName = name || basename4(location);
|
|
3392
4027
|
const existing = dependentMap.get(dependentName);
|
|
3393
4028
|
if (existing) {
|
|
3394
4029
|
existing.count++;
|
|
@@ -3399,55 +4034,76 @@ function printOutdatedResults(enhancedPackages, forkPath) {
|
|
|
3399
4034
|
}
|
|
3400
4035
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
3401
4036
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
3402
|
-
for (const [dependentName, { location, count:
|
|
3403
|
-
const packageJsonPath2 =
|
|
4037
|
+
for (const [dependentName, { location, count: count3 }] of sortedDependents) {
|
|
4038
|
+
const packageJsonPath2 = join13(location, "package.json");
|
|
3404
4039
|
const relativePath = relative3(forkPath, packageJsonPath2);
|
|
3405
4040
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
3406
4041
|
console.info(
|
|
3407
|
-
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${
|
|
4042
|
+
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count3}`)}`
|
|
3408
4043
|
);
|
|
3409
4044
|
}
|
|
3410
4045
|
}
|
|
3411
|
-
function
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
const
|
|
3415
|
-
const
|
|
4046
|
+
function printNotes(forkPath, unchecked) {
|
|
4047
|
+
let held = [];
|
|
4048
|
+
try {
|
|
4049
|
+
const updateConfig = readTopLevel(readFileSync4(join13(forkPath, "pnpm-workspace.yaml"), "utf8"), "updateConfig");
|
|
4050
|
+
const ignored = updateConfig?.ignoreDependencies;
|
|
4051
|
+
held = Array.isArray(ignored) ? ignored.map(String) : [];
|
|
4052
|
+
} catch {
|
|
4053
|
+
}
|
|
4054
|
+
if (held.length > 0) console.info(` ${colors_default.dim(`held: ${held.join(", ")} \xB7 pnpm-workspace.yaml updateConfig`)}`);
|
|
4055
|
+
if (unchecked.length > 0)
|
|
4056
|
+
console.info(` ${colors_default.dim(`not checked, no answer from the feed: ${unchecked.join(", ")}`)}`);
|
|
4057
|
+
}
|
|
4058
|
+
function printLocalTools(checks) {
|
|
4059
|
+
const width = Math.max(...checks.map((tool) => `${tool.name} ${tool.version}`.length));
|
|
4060
|
+
console.info(colors_default.bold("this machine"));
|
|
4061
|
+
for (const tool of checks) {
|
|
4062
|
+
const label = `${tool.name} ${tool.version}`.padEnd(width);
|
|
4063
|
+
const detail = tool.severity ? `${tool.note} ${colors_default.dim("\xB7")} ${tool.fix}` : colors_default.dim(tool.note || "no missing security fixes");
|
|
4064
|
+
console.info(`${tool.severity ? getVulnIcon(tool.severity) : " "} ${label} ${detail}`);
|
|
4065
|
+
}
|
|
4066
|
+
console.info();
|
|
4067
|
+
}
|
|
4068
|
+
function buildUpdateChoices(rows) {
|
|
4069
|
+
const makeChoice = (row) => {
|
|
4070
|
+
const devTag = row.isDev ? colors_default.dim(" (dev)") : "";
|
|
4071
|
+
const version = `${colors_default.red(row.current)} \u2192 ${colors_default.green(row.latest)}`;
|
|
4072
|
+
const notes = [
|
|
4073
|
+
row.breaking ? colors_default.bold("breaking") : "",
|
|
4074
|
+
row.runtime?.note ? colors_default.dim(row.runtime.note) : "",
|
|
4075
|
+
row.pinnedIn.length > 0 ? `${warningMark} ${colors_default.dim(`pinned in ${row.pinnedIn.join(", ")}`)}` : ""
|
|
4076
|
+
].filter(Boolean);
|
|
3416
4077
|
return {
|
|
3417
|
-
name: `${
|
|
3418
|
-
value:
|
|
3419
|
-
checked:
|
|
4078
|
+
name: `${row.name}${devTag} ${version}${notes.length > 0 ? ` ${notes.join(" ")}` : ""}`,
|
|
4079
|
+
value: row.key,
|
|
4080
|
+
checked: isRed(row)
|
|
3420
4081
|
};
|
|
3421
4082
|
};
|
|
3422
|
-
const
|
|
3423
|
-
const
|
|
4083
|
+
const runtimes = rows.filter((p) => p.runtime);
|
|
4084
|
+
const pinned = rows.filter((p) => !p.runtime && p.pinnedIn.length > 0);
|
|
4085
|
+
const regular = rows.filter((p) => !p.runtime && p.pinnedIn.length === 0);
|
|
3424
4086
|
const choices = [];
|
|
4087
|
+
if (runtimes.length > 0) {
|
|
4088
|
+
choices.push(new Separator(colors_default.yellow(`\u2500\u2500 runtimes (${runtimes.length}) \u2500\u2500`)));
|
|
4089
|
+
choices.push(...runtimes.map(makeChoice));
|
|
4090
|
+
}
|
|
3425
4091
|
if (pinned.length > 0) {
|
|
3426
4092
|
choices.push(new Separator(colors_default.yellow(`\u2500\u2500 pinned (${pinned.length}) \u2500\u2500`)));
|
|
3427
4093
|
choices.push(...pinned.map(makeChoice));
|
|
3428
4094
|
}
|
|
3429
4095
|
if (regular.length > 0) {
|
|
3430
|
-
if (
|
|
4096
|
+
if (choices.length > 0) choices.push(new Separator(colors_default.dim(`\u2500\u2500 packages (${regular.length}) \u2500\u2500`)));
|
|
3431
4097
|
choices.push(...regular.map(makeChoice));
|
|
3432
4098
|
}
|
|
3433
4099
|
return choices;
|
|
3434
4100
|
}
|
|
3435
|
-
async function promptForUpdates(
|
|
3436
|
-
let wantsUpdate;
|
|
4101
|
+
async function promptForUpdates(rows) {
|
|
3437
4102
|
try {
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
console.info();
|
|
3443
|
-
if (!wantsUpdate) return "skipped";
|
|
3444
|
-
const choices = buildUpdateChoices(packages);
|
|
3445
|
-
let selected;
|
|
3446
|
-
try {
|
|
3447
|
-
selected = await checkbox({
|
|
3448
|
-
message: "toggle packages to update",
|
|
3449
|
-
choices,
|
|
3450
|
-
required: true,
|
|
4103
|
+
const selected = await checkbox({
|
|
4104
|
+
message: `toggle what to update ${colors_default.dim("\xB7 enter with nothing ticked skips")}`,
|
|
4105
|
+
choices: buildUpdateChoices(rows),
|
|
4106
|
+
required: false,
|
|
3451
4107
|
pageSize: 20,
|
|
3452
4108
|
loop: false,
|
|
3453
4109
|
theme: {
|
|
@@ -3460,83 +4116,93 @@ async function promptForUpdates(packages, forkPath) {
|
|
|
3460
4116
|
const tips = keys.map(([key, action]) => `${colors_default.dim(key)} ${action}`);
|
|
3461
4117
|
return tips.join(" ");
|
|
3462
4118
|
},
|
|
3463
|
-
renderSelectedChoices: (selectedChoices) => colors_default.dim(`${selectedChoices.length}
|
|
4119
|
+
renderSelectedChoices: (selectedChoices) => colors_default.dim(`${selectedChoices.length} selected`)
|
|
3464
4120
|
}
|
|
3465
4121
|
}
|
|
3466
4122
|
});
|
|
4123
|
+
console.info();
|
|
4124
|
+
return selected;
|
|
3467
4125
|
} catch {
|
|
3468
|
-
return
|
|
4126
|
+
return null;
|
|
3469
4127
|
}
|
|
3470
|
-
return executeUpdates(packages, forkPath, selected);
|
|
3471
4128
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
4129
|
+
function bumpText(update) {
|
|
4130
|
+
if (update.pin.kind === "node") return `engines.node ${update.target}`;
|
|
4131
|
+
if (update.pin.kind === "pnpm") return `packageManager pnpm@${update.target}`;
|
|
4132
|
+
return update.pin.to;
|
|
4133
|
+
}
|
|
4134
|
+
async function planEdits(forkPath, rows, selected, nodeImageSuffixes) {
|
|
4135
|
+
const contents = /* @__PURE__ */ new Map();
|
|
4136
|
+
const summary = /* @__PURE__ */ new Map();
|
|
4137
|
+
const bumped = [];
|
|
4138
|
+
const edit = (file, label, item, resolves, change) => {
|
|
4139
|
+
const entry = contents.get(file) ?? { content: readFileSync4(join13(forkPath, file), "utf8"), resolves: false };
|
|
4140
|
+
const next = change(entry.content);
|
|
4141
|
+
if (next === entry.content) return;
|
|
4142
|
+
contents.set(file, { content: next, resolves: entry.resolves || resolves });
|
|
4143
|
+
summary.set(label, [...summary.get(label) ?? [], item]);
|
|
4144
|
+
};
|
|
4145
|
+
const chosen = rows.filter((row) => selected.includes(row.key));
|
|
4146
|
+
for (const row of chosen) {
|
|
4147
|
+
if (row.runtime) continue;
|
|
4148
|
+
for (const { name: dependent, location } of row.dependentPackages) {
|
|
4149
|
+
const file = relative3(forkPath, join13(location, "package.json"));
|
|
4150
|
+
edit(
|
|
4151
|
+
file,
|
|
4152
|
+
dependent || file,
|
|
4153
|
+
row.name,
|
|
4154
|
+
true,
|
|
4155
|
+
(content) => editJsonText(content, (json) => {
|
|
4156
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
4157
|
+
const deps = json[section];
|
|
4158
|
+
const spec = deps?.[row.name];
|
|
4159
|
+
if (!deps || spec === void 0) continue;
|
|
4160
|
+
const next = rewriteSpecifier(spec, row.latest);
|
|
4161
|
+
if (next) deps[row.name] = next;
|
|
4162
|
+
else console.info(`${warningMark} ${row.name} in ${dependent || file} left as written: ${spec}`);
|
|
4163
|
+
}
|
|
4164
|
+
})
|
|
4165
|
+
);
|
|
3491
4166
|
}
|
|
3492
4167
|
}
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
4168
|
+
for (const row of chosen) {
|
|
4169
|
+
const update = row.runtime;
|
|
4170
|
+
if (!update) continue;
|
|
4171
|
+
if (update.pin.kind === "node") {
|
|
4172
|
+
const missing = await missingNodeImages(update.target, nodeImageSuffixes);
|
|
4173
|
+
if (missing.length > 0) {
|
|
4174
|
+
console.info(`${warningMark} node ${update.target} skipped: ${missing.join(", ")} is not on docker hub yet`);
|
|
4175
|
+
continue;
|
|
4176
|
+
}
|
|
3502
4177
|
}
|
|
4178
|
+
edit(update.file, update.file, bumpText(update), false, (content) => applyRuntimeUpdate(content, update));
|
|
4179
|
+
bumped.push(update);
|
|
3503
4180
|
}
|
|
3504
|
-
for (const [
|
|
3505
|
-
|
|
3506
|
-
console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(workspace)}: ${colors_default.dim(pkgList)}`);
|
|
4181
|
+
for (const [label, items] of summary) {
|
|
4182
|
+
console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(label)}: ${colors_default.dim(items.join(", "))}`);
|
|
3507
4183
|
}
|
|
3508
|
-
console.info();
|
|
3509
|
-
const
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
const normalizeResult = spawnSync3("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
3513
|
-
cwd: forkPath,
|
|
3514
|
-
stdio: "inherit"
|
|
3515
|
-
});
|
|
3516
|
-
console.info();
|
|
3517
|
-
if (normalizeResult.status !== 0) {
|
|
3518
|
-
console.error(colors_default.red(`\u2717 catalog normalization failed (exit code ${normalizeResult.status})`));
|
|
3519
|
-
return "failed";
|
|
3520
|
-
}
|
|
4184
|
+
if (summary.size > 0) console.info();
|
|
4185
|
+
const edits = { manifests: {}, files: {} };
|
|
4186
|
+
for (const [file, { content, resolves }] of contents) {
|
|
4187
|
+
(resolves ? edits.manifests : edits.files)[file] = content;
|
|
3521
4188
|
}
|
|
3522
|
-
|
|
3523
|
-
console.info();
|
|
3524
|
-
if (result.status !== 0) {
|
|
3525
|
-
console.error(colors_default.red(`\u2717 update failed (exit code ${result.status})`));
|
|
3526
|
-
return "failed";
|
|
3527
|
-
}
|
|
3528
|
-
console.info(colors_default.green(`\u2713 ${selectedPackages.length} package${selectedPackages.length > 1 ? "s" : ""} updated`));
|
|
3529
|
-
console.info();
|
|
3530
|
-
return "updated";
|
|
4189
|
+
return { edits, bumped };
|
|
3531
4190
|
}
|
|
3532
|
-
function
|
|
3533
|
-
const
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
4191
|
+
function printNext(bumped) {
|
|
4192
|
+
const steps = [];
|
|
4193
|
+
const node = bumped.find((update) => update.pin.kind === "node");
|
|
4194
|
+
if (node && process.version !== `v${node.target}`) {
|
|
4195
|
+
const command = getEnv("VOLTA_HOME") ? `volta install node@${node.target}` : `switch to node ${node.target}`;
|
|
4196
|
+
steps.push([command, `this machine runs ${process.version.slice(1)}`]);
|
|
4197
|
+
}
|
|
4198
|
+
for (const update of bumped) {
|
|
4199
|
+
if (update.pin.kind === "image") steps.push([`docker build -f ${update.file} .`, "check that the image builds"]);
|
|
3539
4200
|
}
|
|
4201
|
+
if (steps.length === 0) return;
|
|
4202
|
+
const width = Math.max(...steps.map(([command]) => command.length));
|
|
4203
|
+
console.info(colors_default.bold("next"));
|
|
4204
|
+
for (const [command, why] of steps) console.info(` ${colors_default.dim("\u2022")} ${command.padEnd(width)} ${colors_default.dim(why)}`);
|
|
4205
|
+
console.info();
|
|
3540
4206
|
}
|
|
3541
4207
|
|
|
3542
4208
|
// src/services/contributions.ts
|
|
@@ -3552,111 +4218,9 @@ import {
|
|
|
3552
4218
|
} from "@inquirer/core";
|
|
3553
4219
|
import { select as select3 } from "@inquirer/prompts";
|
|
3554
4220
|
|
|
3555
|
-
// src/utils/diff.ts
|
|
3556
|
-
import { execFileSync as execFileSync2 } from "child_process";
|
|
3557
|
-
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3558
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
3559
|
-
import { basename as basename3, join as join11 } from "path";
|
|
3560
|
-
import process6 from "process";
|
|
3561
|
-
function gitDiffFile(cwd, range, filePath, options = {}) {
|
|
3562
|
-
const args = ["diff", "--no-color"];
|
|
3563
|
-
if (options.dstPrefix) args.push("--src-prefix=cella/", `--dst-prefix=${options.dstPrefix}/`);
|
|
3564
|
-
args.push(range, "--", filePath);
|
|
3565
|
-
try {
|
|
3566
|
-
return execFileSync2("git", args, { cwd, maxBuffer: MAX_BUFFER });
|
|
3567
|
-
} catch (error) {
|
|
3568
|
-
const stderr = error.stderr?.toString().trim();
|
|
3569
|
-
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
3570
|
-
}
|
|
3571
|
-
}
|
|
3572
|
-
function escapeHtml(text) {
|
|
3573
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3574
|
-
}
|
|
3575
|
-
async function renderDiffPage(patch, meta) {
|
|
3576
|
-
const { preloadPatchDiff } = await import("@pierre/diffs/ssr");
|
|
3577
|
-
const { prerenderedHTML } = await preloadPatchDiff({
|
|
3578
|
-
patch,
|
|
3579
|
-
options: {
|
|
3580
|
-
diffStyle: "split",
|
|
3581
|
-
themeType: "system",
|
|
3582
|
-
lineDiffType: "word-alt",
|
|
3583
|
-
hunkSeparators: "line-info",
|
|
3584
|
-
overflow: "wrap",
|
|
3585
|
-
disableFileHeader: true
|
|
3586
|
-
}
|
|
3587
|
-
});
|
|
3588
|
-
const title = `${escapeHtml(meta.filePath)} \xB7 ${escapeHtml(meta.srcLabel)} vs ${escapeHtml(meta.dstLabel)}`;
|
|
3589
|
-
const note = meta.note ? `<span class="note">\xB7 ${escapeHtml(meta.note)}</span>` : "";
|
|
3590
|
-
return `<!doctype html>
|
|
3591
|
-
<html lang="en">
|
|
3592
|
-
<head>
|
|
3593
|
-
<meta charset="utf-8">
|
|
3594
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3595
|
-
<title>${title}</title>
|
|
3596
|
-
<style>
|
|
3597
|
-
:root { color-scheme: light dark; }
|
|
3598
|
-
* { box-sizing: border-box; }
|
|
3599
|
-
body {
|
|
3600
|
-
margin: 0;
|
|
3601
|
-
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
3602
|
-
background: light-dark(#fafafa, #111);
|
|
3603
|
-
color: light-dark(#111, #eee);
|
|
3604
|
-
}
|
|
3605
|
-
header {
|
|
3606
|
-
display: flex; align-items: baseline; gap: 0.75rem; flex-wrap: wrap;
|
|
3607
|
-
padding: 0.9rem 1.25rem;
|
|
3608
|
-
border-bottom: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
3609
|
-
position: sticky; top: 0;
|
|
3610
|
-
background: inherit;
|
|
3611
|
-
}
|
|
3612
|
-
header .brand { font-weight: 600; color: light-dark(#0891b2, #22d3ee); }
|
|
3613
|
-
header code { font-family: ui-monospace, monospace; font-size: 0.9rem; }
|
|
3614
|
-
header .sides { margin-left: auto; font-size: 0.8rem; opacity: 0.7; }
|
|
3615
|
-
main { max-width: 1400px; margin: 1rem auto; padding: 0 1rem 2rem; }
|
|
3616
|
-
.diff-shell {
|
|
3617
|
-
border: 1px solid light-dark(#e2e2e2, #2a2a2a);
|
|
3618
|
-
border-radius: 8px; overflow: hidden;
|
|
3619
|
-
background: light-dark(#fff, #161616);
|
|
3620
|
-
}
|
|
3621
|
-
</style>
|
|
3622
|
-
</head>
|
|
3623
|
-
<body>
|
|
3624
|
-
<header>
|
|
3625
|
-
<span class="brand">\u29C8 cella diff</span>
|
|
3626
|
-
<code>${escapeHtml(meta.filePath)}</code>
|
|
3627
|
-
<span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
|
|
3628
|
-
</header>
|
|
3629
|
-
<main>
|
|
3630
|
-
<!-- Declarative shadow DOM: the library's CSS targets :host, which only
|
|
3631
|
-
matches inside a shadow root (its browser component renders into one). -->
|
|
3632
|
-
<div class="diff-shell">
|
|
3633
|
-
<template shadowrootmode="open">
|
|
3634
|
-
${prerenderedHTML}
|
|
3635
|
-
</template>
|
|
3636
|
-
</div>
|
|
3637
|
-
</main>
|
|
3638
|
-
</body>
|
|
3639
|
-
</html>`;
|
|
3640
|
-
}
|
|
3641
|
-
function openWithDefaultApp(target) {
|
|
3642
|
-
try {
|
|
3643
|
-
execFileSync2(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
3644
|
-
} catch {
|
|
3645
|
-
}
|
|
3646
|
-
}
|
|
3647
|
-
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
3648
|
-
const html = await renderDiffPage(patch, meta);
|
|
3649
|
-
const dir = join11(tmpdir2(), `cella-diff-${basename3(repoPath)}`);
|
|
3650
|
-
mkdirSync2(dir, { recursive: true });
|
|
3651
|
-
const outPath = join11(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3652
|
-
writeFileSync2(outPath, html);
|
|
3653
|
-
openWithDefaultApp(outPath);
|
|
3654
|
-
return outPath;
|
|
3655
|
-
}
|
|
3656
|
-
|
|
3657
4221
|
// src/services/contrib-core.ts
|
|
3658
4222
|
import { rmSync as rmSync2 } from "fs";
|
|
3659
|
-
import { dirname as
|
|
4223
|
+
import { dirname as dirname8, join as join14, resolve as resolve4 } from "path";
|
|
3660
4224
|
async function listFilesAtRef(repoPath, ref) {
|
|
3661
4225
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
3662
4226
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -3672,10 +4236,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3672
4236
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
3673
4237
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
3674
4238
|
for (const f of baseFiles) {
|
|
3675
|
-
let dir =
|
|
4239
|
+
let dir = dirname8(f);
|
|
3676
4240
|
while (dir !== ".") {
|
|
3677
4241
|
baseDirs.add(dir);
|
|
3678
|
-
dir =
|
|
4242
|
+
dir = dirname8(dir);
|
|
3679
4243
|
}
|
|
3680
4244
|
}
|
|
3681
4245
|
const files = [];
|
|
@@ -3688,7 +4252,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
3688
4252
|
if (inCella && !inFork) {
|
|
3689
4253
|
kind = "deleted";
|
|
3690
4254
|
} else if (!inCella && inFork) {
|
|
3691
|
-
if (!baseDirs.has(
|
|
4255
|
+
if (!baseDirs.has(dirname8(file.path))) continue;
|
|
3692
4256
|
kind = "created";
|
|
3693
4257
|
} else {
|
|
3694
4258
|
kind = "modified";
|
|
@@ -3712,7 +4276,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
3712
4276
|
async function buildContribBranch(repoPath, baseRef, forkRef, files, forkName) {
|
|
3713
4277
|
const branch = `contrib/${forkName}`;
|
|
3714
4278
|
const gitDir = resolve4(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
3715
|
-
const tmpIndex =
|
|
4279
|
+
const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
3716
4280
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
3717
4281
|
try {
|
|
3718
4282
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -3747,20 +4311,20 @@ ${commitBody}`;
|
|
|
3747
4311
|
}
|
|
3748
4312
|
|
|
3749
4313
|
// src/services/fork-utils.ts
|
|
3750
|
-
import { existsSync as
|
|
3751
|
-
import { dirname as
|
|
4314
|
+
import { existsSync as existsSync9 } from "fs";
|
|
4315
|
+
import { dirname as dirname9, resolve as resolve5 } from "path";
|
|
3752
4316
|
async function resolveForkBasePath(repoPath) {
|
|
3753
4317
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
3754
|
-
return commonDir ?
|
|
4318
|
+
return commonDir ? dirname9(resolve5(repoPath, commonDir)) : repoPath;
|
|
3755
4319
|
}
|
|
3756
4320
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
3757
4321
|
const resolvedPath = resolve5(basePath, fork.localPath);
|
|
3758
4322
|
if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
|
|
3759
4323
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
3760
4324
|
}
|
|
3761
|
-
if (!
|
|
3762
|
-
if (!
|
|
3763
|
-
if (requireConfig && !
|
|
4325
|
+
if (!existsSync9(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
4326
|
+
if (!existsSync9(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
4327
|
+
if (requireConfig && !existsSync9(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
3764
4328
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
3765
4329
|
}
|
|
3766
4330
|
return { fork, resolvedPath, valid: true };
|
|
@@ -3831,8 +4395,13 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
3831
4395
|
try {
|
|
3832
4396
|
const patch = gitDiffFile(cwd, `${baseRef}..${branch}`, path);
|
|
3833
4397
|
if (patch.length === 0) return setStatusMsg(`no changes for ${path}`);
|
|
3834
|
-
|
|
3835
|
-
|
|
4398
|
+
const page2 = await writeDiffPage(
|
|
4399
|
+
[{ files: [{ path, patch: patch.toString() }] }],
|
|
4400
|
+
{ title: path, note: `cella \u2190 \u2192 ${forkName}` },
|
|
4401
|
+
cwd,
|
|
4402
|
+
path
|
|
4403
|
+
);
|
|
4404
|
+
setStatusMsg(openInBrowser(page2) ? `opened ${path} in browser` : `diff page: ${page2}`);
|
|
3836
4405
|
} catch (error) {
|
|
3837
4406
|
setStatusMsg(`diff failed: ${errorMessage(error)}`);
|
|
3838
4407
|
}
|
|
@@ -4061,20 +4630,20 @@ async function runContributions(config) {
|
|
|
4061
4630
|
|
|
4062
4631
|
// src/services/forks.ts
|
|
4063
4632
|
import { resolve as resolve6 } from "path";
|
|
4064
|
-
import
|
|
4633
|
+
import process8 from "process";
|
|
4065
4634
|
import { Separator as Separator2, select as select5 } from "@inquirer/prompts";
|
|
4066
4635
|
|
|
4067
4636
|
// src/services/sync.ts
|
|
4068
|
-
import { spawnSync as
|
|
4637
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4069
4638
|
import { select as select4 } from "@inquirer/prompts";
|
|
4070
4639
|
|
|
4071
4640
|
// src/utils/gh.ts
|
|
4072
|
-
import { spawnSync as
|
|
4641
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4073
4642
|
function ghAvailable() {
|
|
4074
|
-
return
|
|
4643
|
+
return spawnSync3("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
4075
4644
|
}
|
|
4076
4645
|
function runGh(cwd, args) {
|
|
4077
|
-
const result =
|
|
4646
|
+
const result = spawnSync3("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4078
4647
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
4079
4648
|
return { ok: result.status === 0, output };
|
|
4080
4649
|
}
|
|
@@ -4106,12 +4675,12 @@ function closePr(cwd, ref) {
|
|
|
4106
4675
|
}
|
|
4107
4676
|
|
|
4108
4677
|
// src/services/packages.ts
|
|
4109
|
-
import { existsSync as
|
|
4110
|
-
import { join as
|
|
4678
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
4679
|
+
import { join as join16 } from "path";
|
|
4111
4680
|
|
|
4112
4681
|
// src/services/package-usage.ts
|
|
4113
|
-
import { existsSync as
|
|
4114
|
-
import { join as
|
|
4682
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, statSync } from "fs";
|
|
4683
|
+
import { join as join15 } from "path";
|
|
4115
4684
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4116
4685
|
function escapeRegExp(value) {
|
|
4117
4686
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4119,7 +4688,7 @@ function escapeRegExp(value) {
|
|
|
4119
4688
|
function readSearchable(filePath) {
|
|
4120
4689
|
try {
|
|
4121
4690
|
if (statSync(filePath).size > MAX_FILE_BYTES) return "";
|
|
4122
|
-
const text =
|
|
4691
|
+
const text = readFileSync5(filePath, "utf8");
|
|
4123
4692
|
return text.includes("\0") ? "" : text;
|
|
4124
4693
|
} catch {
|
|
4125
4694
|
return "";
|
|
@@ -4127,7 +4696,7 @@ function readSearchable(filePath) {
|
|
|
4127
4696
|
}
|
|
4128
4697
|
function fileSource(forkPath, path) {
|
|
4129
4698
|
let text;
|
|
4130
|
-
return { path, label: path, text: () => text ??= readSearchable(
|
|
4699
|
+
return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
|
|
4131
4700
|
}
|
|
4132
4701
|
async function readScriptsAtRef(forkPath, ref, path) {
|
|
4133
4702
|
try {
|
|
@@ -4139,7 +4708,7 @@ async function readScriptsAtRef(forkPath, ref, path) {
|
|
|
4139
4708
|
async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
4140
4709
|
let forkScripts;
|
|
4141
4710
|
try {
|
|
4142
|
-
forkScripts = JSON.parse(
|
|
4711
|
+
forkScripts = JSON.parse(readFileSync5(join15(forkPath, path), "utf8")).scripts ?? {};
|
|
4143
4712
|
} catch {
|
|
4144
4713
|
return [];
|
|
4145
4714
|
}
|
|
@@ -4149,11 +4718,11 @@ async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
|
4149
4718
|
}
|
|
4150
4719
|
function binNames(forkPath, location, name) {
|
|
4151
4720
|
const bareName = name.split("/").pop();
|
|
4152
|
-
for (const dir of [
|
|
4153
|
-
const manifest =
|
|
4154
|
-
if (!
|
|
4721
|
+
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
4722
|
+
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
4723
|
+
if (!existsSync10(manifest)) continue;
|
|
4155
4724
|
try {
|
|
4156
|
-
const { bin } = JSON.parse(
|
|
4725
|
+
const { bin } = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
4157
4726
|
if (typeof bin === "string") return [bareName];
|
|
4158
4727
|
return bin && typeof bin === "object" ? Object.keys(bin) : [];
|
|
4159
4728
|
} catch {
|
|
@@ -4332,9 +4901,9 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4332
4901
|
Object.assign(pkg, Object.fromEntries(entries));
|
|
4333
4902
|
}
|
|
4334
4903
|
function readPackageJson(filePath) {
|
|
4335
|
-
if (!
|
|
4904
|
+
if (!existsSync11(filePath)) return null;
|
|
4336
4905
|
try {
|
|
4337
|
-
return JSON.parse(
|
|
4906
|
+
return JSON.parse(readFileSync6(filePath, "utf8"));
|
|
4338
4907
|
} catch (error) {
|
|
4339
4908
|
throw new Error(`cannot parse ${filePath}: ${errorMessage(error)}`);
|
|
4340
4909
|
}
|
|
@@ -4366,13 +4935,13 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4366
4935
|
const changes = [];
|
|
4367
4936
|
const kept = [];
|
|
4368
4937
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4369
|
-
const pkgPath =
|
|
4938
|
+
const pkgPath = join16(forkPath, pkgRelPath);
|
|
4370
4939
|
const forkPkg = readPackageJson(pkgPath);
|
|
4371
4940
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4372
4941
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4373
4942
|
if (!upstream) return { updated: false, changes, kept };
|
|
4374
4943
|
if (!forkPkg) {
|
|
4375
|
-
const workspaceArrived = relativePath !== "" &&
|
|
4944
|
+
const workspaceArrived = relativePath !== "" && existsSync11(join16(forkPath, relativePath));
|
|
4376
4945
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4377
4946
|
writeFileSync3(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4378
4947
|
`, "utf8");
|
|
@@ -4578,6 +5147,7 @@ async function runSync(config, options) {
|
|
|
4578
5147
|
const staged = result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
4579
5148
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && staged ? options.stagedBranch : void 0;
|
|
4580
5149
|
printSyncComplete(result, { stagedBranch });
|
|
5150
|
+
await reportReviewPage(config.forkPath, result, "sync");
|
|
4581
5151
|
await printMigrationNotesLine(config, result);
|
|
4582
5152
|
return result;
|
|
4583
5153
|
}
|
|
@@ -4686,7 +5256,7 @@ async function shipSyncBranch(config, branch) {
|
|
|
4686
5256
|
console.info(colors_default.dim("opening a pull request..."));
|
|
4687
5257
|
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
4688
5258
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
4689
|
-
const pr =
|
|
5259
|
+
const pr = spawnSync4("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
4690
5260
|
cwd: forkPath,
|
|
4691
5261
|
encoding: "utf8",
|
|
4692
5262
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -4717,7 +5287,7 @@ ${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
|
4717
5287
|
function finalizeWorkspace(forkPath) {
|
|
4718
5288
|
for (const args of [["install"], ["check"]]) {
|
|
4719
5289
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
4720
|
-
const result =
|
|
5290
|
+
const result = spawnSync4("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
4721
5291
|
if (result.status !== 0) return false;
|
|
4722
5292
|
}
|
|
4723
5293
|
return true;
|
|
@@ -4983,7 +5553,7 @@ async function runForks(config) {
|
|
|
4983
5553
|
loop: false
|
|
4984
5554
|
});
|
|
4985
5555
|
if (selectedPath === "_exit") {
|
|
4986
|
-
|
|
5556
|
+
process8.exit(0);
|
|
4987
5557
|
}
|
|
4988
5558
|
const resolvedForkPath = resolve6(forkBasePath, selectedPath);
|
|
4989
5559
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
@@ -4998,11 +5568,11 @@ async function runForks(config) {
|
|
|
4998
5568
|
|
|
4999
5569
|
// src/services/stats.ts
|
|
5000
5570
|
import { readFile as readFile3 } from "fs/promises";
|
|
5001
|
-
import { join as
|
|
5571
|
+
import { join as join18 } from "path";
|
|
5002
5572
|
|
|
5003
5573
|
// src/utils/coverage-utils.ts
|
|
5004
|
-
import { existsSync as
|
|
5005
|
-
import { join as
|
|
5574
|
+
import { existsSync as existsSync12, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
5575
|
+
import { join as join17, relative as relative4, sep as sep3 } from "path";
|
|
5006
5576
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5007
5577
|
function colorPct(pct) {
|
|
5008
5578
|
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
@@ -5020,14 +5590,14 @@ function formatAge(ms) {
|
|
|
5020
5590
|
return `${mins} minute${mins === 1 ? "" : "s"} ago`;
|
|
5021
5591
|
}
|
|
5022
5592
|
function printCoverageSummary(forkPath) {
|
|
5023
|
-
const file =
|
|
5593
|
+
const file = join17(forkPath, ".coverage", "coverage-summary.json");
|
|
5024
5594
|
let summary;
|
|
5025
5595
|
let mtimeMs = 0;
|
|
5026
5596
|
let problem = "no coverage data found";
|
|
5027
|
-
if (
|
|
5597
|
+
if (existsSync12(file)) {
|
|
5028
5598
|
try {
|
|
5029
5599
|
mtimeMs = statSync2(file).mtimeMs;
|
|
5030
|
-
summary = JSON.parse(
|
|
5600
|
+
summary = JSON.parse(readFileSync7(file, "utf8"));
|
|
5031
5601
|
} catch {
|
|
5032
5602
|
problem = "could not read coverage-summary.json";
|
|
5033
5603
|
}
|
|
@@ -5045,7 +5615,7 @@ function printCoverageSummary(forkPath) {
|
|
|
5045
5615
|
if (key === "total") continue;
|
|
5046
5616
|
const rel = relative4(forkPath, key);
|
|
5047
5617
|
if (rel.startsWith("..")) continue;
|
|
5048
|
-
const pkg = rel.split(
|
|
5618
|
+
const pkg = rel.split(sep3)[0];
|
|
5049
5619
|
const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
|
|
5050
5620
|
acc.covered += entry.lines.covered;
|
|
5051
5621
|
acc.total += entry.lines.total;
|
|
@@ -5071,7 +5641,7 @@ function printCoverageSummary(forkPath) {
|
|
|
5071
5641
|
// src/services/stats.ts
|
|
5072
5642
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
5073
5643
|
async function getWorkspacePackages(forkPath) {
|
|
5074
|
-
const content = await readFile3(
|
|
5644
|
+
const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
|
|
5075
5645
|
const patterns = readTopLevel(content, "packages");
|
|
5076
5646
|
return (Array.isArray(patterns) ? patterns.map(String) : []).map((pattern) => {
|
|
5077
5647
|
if (pattern.endsWith("/*")) {
|
|
@@ -5119,9 +5689,9 @@ async function collectStats(forkPath) {
|
|
|
5119
5689
|
const entries = [];
|
|
5120
5690
|
for (const line of output.split("\n")) {
|
|
5121
5691
|
if (!line) continue;
|
|
5122
|
-
const
|
|
5123
|
-
if (
|
|
5124
|
-
entries.push({ path: line.slice(0,
|
|
5692
|
+
const sep4 = line.lastIndexOf(":");
|
|
5693
|
+
if (sep4 === -1) continue;
|
|
5694
|
+
entries.push({ path: line.slice(0, sep4), loc: Number.parseInt(line.slice(sep4 + 1), 10) });
|
|
5125
5695
|
}
|
|
5126
5696
|
const emptyCat = () => ({
|
|
5127
5697
|
test: { files: 0, loc: 0 },
|
|
@@ -5424,19 +5994,19 @@ function getForkPath() {
|
|
|
5424
5994
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5425
5995
|
if (envPath) {
|
|
5426
5996
|
const resolved = resolve7(envPath);
|
|
5427
|
-
if (!
|
|
5997
|
+
if (!existsSync13(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5428
5998
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5429
5999
|
}
|
|
5430
6000
|
return resolved;
|
|
5431
6001
|
}
|
|
5432
|
-
let cwd =
|
|
6002
|
+
let cwd = process9.cwd();
|
|
5433
6003
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
5434
6004
|
cwd = resolve7(cwd, "../..");
|
|
5435
6005
|
}
|
|
5436
6006
|
return cwd;
|
|
5437
6007
|
}
|
|
5438
6008
|
function preflight(forkPath) {
|
|
5439
|
-
if (!
|
|
6009
|
+
if (!existsSync13(join19(forkPath, ".git"))) {
|
|
5440
6010
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5441
6011
|
}
|
|
5442
6012
|
}
|
|
@@ -5454,7 +6024,7 @@ var services = {
|
|
|
5454
6024
|
}
|
|
5455
6025
|
};
|
|
5456
6026
|
function isHelpOrVersionRequest() {
|
|
5457
|
-
return
|
|
6027
|
+
return process9.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process9.argv[2] === "help";
|
|
5458
6028
|
}
|
|
5459
6029
|
async function main() {
|
|
5460
6030
|
try {
|
|
@@ -5473,7 +6043,7 @@ async function main() {
|
|
|
5473
6043
|
} catch (error) {
|
|
5474
6044
|
console.error();
|
|
5475
6045
|
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
5476
|
-
|
|
6046
|
+
process9.exit(1);
|
|
5477
6047
|
}
|
|
5478
6048
|
}
|
|
5479
6049
|
main();
|