@cellajs/cli 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -29
- package/dist/config.d.ts +4 -4
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.js +1336 -1875
- package/dist/index.js.map +1 -1
- package/package.json +2 -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 existsSync12, statSync as statSync3 } from "fs";
|
|
5
|
+
import { join as join17, resolve as resolve7 } from "path";
|
|
6
|
+
import process8 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
9
|
import process5 from "process";
|
|
@@ -77,7 +77,7 @@ var cellaConfigSchema = z.object({
|
|
|
77
77
|
releaseBase: z.string().min(1).optional(),
|
|
78
78
|
packageJsonSync: z.array(z.enum(PACKAGE_JSON_SYNC_KEYS)).optional(),
|
|
79
79
|
syncWithPackages: z.boolean().optional(),
|
|
80
|
-
fileLinkMode: z.enum(["commit", "file"
|
|
80
|
+
fileLinkMode: z.enum(["commit", "file"]).optional()
|
|
81
81
|
}).strict(),
|
|
82
82
|
overrides: z.object({
|
|
83
83
|
ignored: z.array(z.string()).optional(),
|
|
@@ -101,11 +101,6 @@ function errorMessage(error) {
|
|
|
101
101
|
|
|
102
102
|
// src/utils/managed-files.ts
|
|
103
103
|
var CONFIG_FILE = "cella/cella.config.ts";
|
|
104
|
-
var LEGACY_CONFIG_FILE = "cella.config.ts";
|
|
105
|
-
var CONFIG_FILE_PATHS = [CONFIG_FILE, LEGACY_CONFIG_FILE];
|
|
106
|
-
function isConfigFile(filePath) {
|
|
107
|
-
return CONFIG_FILE_PATHS.includes(filePath);
|
|
108
|
-
}
|
|
109
104
|
function isPackageJson(filePath) {
|
|
110
105
|
return filePath === "package.json" || filePath.endsWith("/package.json");
|
|
111
106
|
}
|
|
@@ -114,7 +109,7 @@ function isGeneratedFile(filePath) {
|
|
|
114
109
|
return segments.includes("gen") || (segments.at(-1) ?? "").includes(".gen.");
|
|
115
110
|
}
|
|
116
111
|
function isManagedFile(filePath) {
|
|
117
|
-
return isPackageJson(filePath) || filePath === "pnpm-lock.yaml" ||
|
|
112
|
+
return isPackageJson(filePath) || filePath === "pnpm-lock.yaml" || filePath === CONFIG_FILE;
|
|
118
113
|
}
|
|
119
114
|
|
|
120
115
|
// src/utils/module-territory.ts
|
|
@@ -162,7 +157,6 @@ function resolveAppModuleFolders(repoPath) {
|
|
|
162
157
|
var DEFAULT_UPSTREAM_REMOTE = "cella-upstream";
|
|
163
158
|
var DEFAULT_BRANCH = "main";
|
|
164
159
|
var DEFAULT_SYNC_PREFIX = "cella/sync";
|
|
165
|
-
var DEFAULT_RELEASE_BASE = "main";
|
|
166
160
|
var DEFAULT_PACKAGE_JSON_SYNC = ["dependencies", "devDependencies"];
|
|
167
161
|
function isUpstreamRepo(repoPath) {
|
|
168
162
|
return JSON.parse(readFileSync(join2(repoPath, "package.json"), "utf8")).name === "cella";
|
|
@@ -171,7 +165,7 @@ function isTemporarySyncBranch(branch) {
|
|
|
171
165
|
return branch.startsWith(`${DEFAULT_SYNC_PREFIX}/`);
|
|
172
166
|
}
|
|
173
167
|
function resolveReleaseBase(settings) {
|
|
174
|
-
return settings.releaseBase ??
|
|
168
|
+
return settings.releaseBase ?? DEFAULT_BRANCH;
|
|
175
169
|
}
|
|
176
170
|
function buildTemporarySyncBranch() {
|
|
177
171
|
const now = /* @__PURE__ */ new Date();
|
|
@@ -219,14 +213,13 @@ ${z2.prettifyError(result.error)}`);
|
|
|
219
213
|
}
|
|
220
214
|
|
|
221
215
|
// src/utils/display.ts
|
|
222
|
-
import {
|
|
223
|
-
import { join as join3, resolve as resolve2 } from "path";
|
|
216
|
+
import { resolve as resolve2 } from "path";
|
|
224
217
|
import process4 from "process";
|
|
225
218
|
|
|
226
219
|
// package.json
|
|
227
220
|
var package_default = {
|
|
228
221
|
name: "@cellajs/cli",
|
|
229
|
-
version: "0.
|
|
222
|
+
version: "0.4.0",
|
|
230
223
|
private: false,
|
|
231
224
|
license: "MIT",
|
|
232
225
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -281,6 +274,7 @@ var package_default = {
|
|
|
281
274
|
"@pierre/diffs": "^1.5.1",
|
|
282
275
|
commander: "^15.0.0",
|
|
283
276
|
"ts-morph": "^28.0.0",
|
|
277
|
+
yaml: "^2.9.1",
|
|
284
278
|
zod: "^4.6.5"
|
|
285
279
|
},
|
|
286
280
|
devDependencies: {
|
|
@@ -349,44 +343,12 @@ var package_default = {
|
|
|
349
343
|
};
|
|
350
344
|
|
|
351
345
|
// src/utils/display.ts
|
|
352
|
-
var NAME = "cella cli";
|
|
353
346
|
var VERSION = package_default.version;
|
|
354
347
|
var DIVIDER = "\u2500".repeat(60);
|
|
355
348
|
var MENU_DIVIDER = "\u2500".repeat(40);
|
|
356
349
|
var COMMIT_LIST_MAX = 50;
|
|
357
350
|
var warningMark = colors_default.yellow("\u26A0");
|
|
358
351
|
var checkMark = colors_default.green("\u2713");
|
|
359
|
-
var TerminalSpinner = class {
|
|
360
|
-
constructor(text, isSilent) {
|
|
361
|
-
this.text = text;
|
|
362
|
-
this.enabled = !isSilent && !!process4.stdout.isTTY;
|
|
363
|
-
}
|
|
364
|
-
text;
|
|
365
|
-
frames = ["-", "\\", "|", "/"];
|
|
366
|
-
frameIndex = 0;
|
|
367
|
-
timer = null;
|
|
368
|
-
enabled;
|
|
369
|
-
start() {
|
|
370
|
-
if (!this.enabled || this.timer) return this;
|
|
371
|
-
this.render();
|
|
372
|
-
this.timer = setInterval(() => {
|
|
373
|
-
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
374
|
-
this.render();
|
|
375
|
-
}, 80);
|
|
376
|
-
return this;
|
|
377
|
-
}
|
|
378
|
-
stop() {
|
|
379
|
-
if (this.timer) {
|
|
380
|
-
clearInterval(this.timer);
|
|
381
|
-
this.timer = null;
|
|
382
|
-
}
|
|
383
|
-
if (!this.enabled) return;
|
|
384
|
-
process4.stdout.write("\r\x1B[2K");
|
|
385
|
-
}
|
|
386
|
-
render() {
|
|
387
|
-
process4.stdout.write(`\r${colors_default.cyan(this.frames[this.frameIndex])} ${this.text}`);
|
|
388
|
-
}
|
|
389
|
-
};
|
|
390
352
|
var activeSpinner = null;
|
|
391
353
|
var jsonMode = false;
|
|
392
354
|
function setJsonMode(enabled) {
|
|
@@ -397,42 +359,48 @@ function setJsonMode(enabled) {
|
|
|
397
359
|
`);
|
|
398
360
|
};
|
|
399
361
|
console.info = toStderr;
|
|
400
|
-
console.log = toStderr;
|
|
401
362
|
console.warn = toStderr;
|
|
402
363
|
}
|
|
403
364
|
function writeStdout(text) {
|
|
404
365
|
process4.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
405
366
|
`);
|
|
406
367
|
}
|
|
407
|
-
function getHeader() {
|
|
408
|
-
const right = "cellajs.com";
|
|
409
|
-
const visibleLeft = `\u29C8 ${NAME} v${VERSION}`;
|
|
410
|
-
const padding = Math.max(1, 60 - visibleLeft.length - right.length);
|
|
411
|
-
return `${colors_default.cyan(`\u29C8 ${NAME}`)}${colors_default.dim(` \xB7 v${VERSION}`)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`;
|
|
412
|
-
}
|
|
413
368
|
function printHeader() {
|
|
369
|
+
const left = "\u29C8 cella cli";
|
|
370
|
+
const right = "cellajs.com";
|
|
371
|
+
const padding = Math.max(1, 60 - `${left} v${VERSION}`.length - right.length);
|
|
414
372
|
console.info();
|
|
415
|
-
console.info(
|
|
373
|
+
console.info(`${colors_default.cyan(left)}${colors_default.dim(` \xB7 v${VERSION}`)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`);
|
|
416
374
|
console.info(DIVIDER);
|
|
417
375
|
console.info();
|
|
418
376
|
}
|
|
419
|
-
function printStep(label, detail) {
|
|
420
|
-
console.info(`${checkMark} ${label}`);
|
|
421
|
-
if (detail) {
|
|
422
|
-
console.info(` ${colors_default.dim(detail)}`);
|
|
423
|
-
console.info();
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
377
|
function createSpinner(text) {
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
378
|
+
const enabled = !jsonMode && !getEnv("VITEST") && getEnv("NODE_ENV") !== "test" && !!process4.stdout.isTTY;
|
|
379
|
+
const frames = ["-", "\\", "|", "/"];
|
|
380
|
+
let frame = 0;
|
|
381
|
+
let timer;
|
|
382
|
+
const spinner = {
|
|
383
|
+
text,
|
|
384
|
+
stop() {
|
|
385
|
+
clearInterval(timer);
|
|
386
|
+
if (enabled) process4.stdout.write("\r\x1B[2K");
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
const render = () => process4.stdout.write(`\r${colors_default.cyan(frames[frame++ % frames.length])} ${spinner.text}`);
|
|
390
|
+
if (enabled) {
|
|
391
|
+
render();
|
|
392
|
+
timer = setInterval(render, 80);
|
|
393
|
+
}
|
|
394
|
+
activeSpinner = spinner;
|
|
395
|
+
return spinner;
|
|
431
396
|
}
|
|
432
397
|
function spinnerSuccess(message, detail) {
|
|
433
398
|
stopSpinner();
|
|
434
|
-
if (message)
|
|
435
|
-
|
|
399
|
+
if (!message) return;
|
|
400
|
+
console.info(`${checkMark} ${message}`);
|
|
401
|
+
if (detail) {
|
|
402
|
+
console.info(` ${colors_default.dim(detail)}`);
|
|
403
|
+
console.info();
|
|
436
404
|
}
|
|
437
405
|
}
|
|
438
406
|
function spinnerFail(message) {
|
|
@@ -455,16 +423,8 @@ function hyperlink(label, url) {
|
|
|
455
423
|
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
456
424
|
return `\x1B]8;;${safeUrl}\x07${label}\x1B]8;;\x07`;
|
|
457
425
|
}
|
|
458
|
-
function getVsCodeOpenFileLink(filePath, options) {
|
|
459
|
-
const { forkPath } = options;
|
|
460
|
-
if (!forkPath) return filePath;
|
|
461
|
-
const absolutePath = resolve2(forkPath, filePath);
|
|
462
|
-
const url = `vscode://file${absolutePath}`;
|
|
463
|
-
return hyperlink(filePath, url);
|
|
464
|
-
}
|
|
465
426
|
function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, maxShownCommits = COMMIT_LIST_MAX) {
|
|
466
|
-
const
|
|
467
|
-
const lines = [`${commitLabel} since last merge`];
|
|
427
|
+
const lines = [`${plural(commitCount, "new commit")} since last merge`];
|
|
468
428
|
const shown = commits.slice(0, maxShownCommits);
|
|
469
429
|
for (const commit of shown) {
|
|
470
430
|
const shortHash = commit.hash.slice(0, 7);
|
|
@@ -477,32 +437,22 @@ function formatFetchedUpstreamDetail(commitCount, commits, upstreamGitHubUrl, ma
|
|
|
477
437
|
}
|
|
478
438
|
return lines.join("\n");
|
|
479
439
|
}
|
|
480
|
-
function getFileLink(filePath, commitHash, options) {
|
|
481
|
-
const { upstreamGitHubUrl, upstreamBranch, fileLinkMode = "commit" } = options;
|
|
482
|
-
const mode = fileLinkMode === "local" ? "file" : fileLinkMode;
|
|
483
|
-
if (!upstreamGitHubUrl) {
|
|
484
|
-
return { label: commitHash?.slice(0, 9) || "" };
|
|
485
|
-
}
|
|
486
|
-
if (mode === "file" && upstreamBranch) {
|
|
487
|
-
const url = `${upstreamGitHubUrl}/blob/${upstreamBranch}/${filePath}`;
|
|
488
|
-
return { label: filePath.split("/").pop() || filePath, url };
|
|
489
|
-
}
|
|
490
|
-
if (commitHash) {
|
|
491
|
-
const url = `${upstreamGitHubUrl}/commit/${commitHash}`;
|
|
492
|
-
return { label: commitHash.slice(0, 9), url };
|
|
493
|
-
}
|
|
494
|
-
return { label: "" };
|
|
495
|
-
}
|
|
496
440
|
function printSectionHeader(title) {
|
|
497
441
|
console.info();
|
|
498
442
|
console.info(title);
|
|
499
443
|
console.info(DIVIDER);
|
|
500
444
|
console.info();
|
|
501
445
|
}
|
|
502
|
-
function formatFileDateInfo(filePath, commit, date,
|
|
503
|
-
|
|
504
|
-
const
|
|
505
|
-
|
|
446
|
+
function formatFileDateInfo(filePath, commit, date, options) {
|
|
447
|
+
if (!date) return "";
|
|
448
|
+
const { upstreamGitHubUrl: url, upstreamBranch, fileLinkMode = "commit" } = options;
|
|
449
|
+
let link = commit?.slice(0, 9) ?? "";
|
|
450
|
+
if (url && fileLinkMode === "file" && upstreamBranch) {
|
|
451
|
+
link = hyperlink(filePath.split("/").pop() || filePath, `${url}/blob/${upstreamBranch}/${filePath}`);
|
|
452
|
+
} else if (url) {
|
|
453
|
+
link = commit ? hyperlink(link, `${url}/commit/${commit}`) : "";
|
|
454
|
+
}
|
|
455
|
+
return colors_default.dim(` \u2260 ${date} ${link}`);
|
|
506
456
|
}
|
|
507
457
|
function formatMergeInProgressDetail(conflictCount, autoMergedFiles, linkOptions, maxFiles = 100) {
|
|
508
458
|
const lines = [`${conflictCount} conflicts to resolve in IDE`];
|
|
@@ -511,10 +461,10 @@ function formatMergeInProgressDetail(conflictCount, autoMergedFiles, linkOptions
|
|
|
511
461
|
}
|
|
512
462
|
lines.push("");
|
|
513
463
|
lines.push(` auto-merged files (${autoMergedFiles.length}):`);
|
|
464
|
+
const { forkPath } = linkOptions;
|
|
514
465
|
const shown = autoMergedFiles.slice(0, maxFiles);
|
|
515
466
|
for (const filePath of shown) {
|
|
516
|
-
|
|
517
|
-
lines.push(` - ${fileLink}`);
|
|
467
|
+
lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve2(forkPath, filePath)}`) : filePath}`);
|
|
518
468
|
}
|
|
519
469
|
if (autoMergedFiles.length > shown.length) {
|
|
520
470
|
lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);
|
|
@@ -543,30 +493,25 @@ var summaryStatusConfig = {
|
|
|
543
493
|
},
|
|
544
494
|
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
545
495
|
};
|
|
546
|
-
var statusOrder = Object.keys(statusConfig);
|
|
547
496
|
function printSummary(summary, title = "summary") {
|
|
548
497
|
printSectionHeader(colors_default.cyan(title));
|
|
549
|
-
const maxCount = Math.max(...
|
|
498
|
+
const maxCount = Math.max(...Object.values(summary));
|
|
550
499
|
const countWidth = String(maxCount).length + 1;
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
printLine("diverged", summary.diverged);
|
|
567
|
-
printLine("pinned", summary.pinned);
|
|
568
|
-
console.info();
|
|
569
|
-
printLine("behind", summary.behind);
|
|
500
|
+
const groups = [
|
|
501
|
+
["managed", "ignored"],
|
|
502
|
+
["identical"],
|
|
503
|
+
["ahead", "local", "drifted"],
|
|
504
|
+
["diverged", "pinned"],
|
|
505
|
+
["behind"]
|
|
506
|
+
];
|
|
507
|
+
for (const group of groups) {
|
|
508
|
+
if (group !== groups[0]) console.info();
|
|
509
|
+
for (const status of group) {
|
|
510
|
+
const { icon, label, color, description } = summaryStatusConfig[status];
|
|
511
|
+
const countStr = color(String(summary[status]).padStart(countWidth));
|
|
512
|
+
console.info(` ${icon} ${countStr} ${label.padEnd(15)}${description ? colors_default.dim(description) : ""}`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
570
515
|
}
|
|
571
516
|
function printFileSection(files, title, linkOptions, options) {
|
|
572
517
|
if (files.length === 0) return;
|
|
@@ -600,14 +545,13 @@ function printFileGroup(files, status, linkOptions, options) {
|
|
|
600
545
|
function findProtectedBehind(files) {
|
|
601
546
|
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path));
|
|
602
547
|
}
|
|
603
|
-
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so diff each against upstream
|
|
548
|
+
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so diff each against upstream and adopt what you need.";
|
|
604
549
|
function protectionIcon(file) {
|
|
605
550
|
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
606
551
|
}
|
|
607
552
|
function formatUpstreamChangedLines(file) {
|
|
608
553
|
if (file.upstreamChangedLines === void 0) return "";
|
|
609
|
-
|
|
610
|
-
return colors_default.yellow(` \xB7 ${n} ${n === 1 ? "line" : "lines"} changed upstream`);
|
|
554
|
+
return colors_default.yellow(` \xB7 ${plural(file.upstreamChangedLines, "line")} changed upstream`);
|
|
611
555
|
}
|
|
612
556
|
function protectedDiffHints(source, paths) {
|
|
613
557
|
const range = source?.upstreamDiffRange;
|
|
@@ -622,7 +566,7 @@ function protectedDiffHints(source, paths) {
|
|
|
622
566
|
}
|
|
623
567
|
function formatUpstreamLinesAbsent(file) {
|
|
624
568
|
const n = file.upstreamLinesAbsent ?? 0;
|
|
625
|
-
return n > 0 ? colors_default.yellow(` \xB7 ${n
|
|
569
|
+
return n > 0 ? colors_default.yellow(` \xB7 ${plural(n, "upstream line")} absent`) : "";
|
|
626
570
|
}
|
|
627
571
|
function printAnalysisFileGroups(files, linkOptions, diffSource) {
|
|
628
572
|
const protectedBehind = findProtectedBehind(files);
|
|
@@ -634,7 +578,7 @@ function printAnalysisFileGroups(files, linkOptions, diffSource) {
|
|
|
634
578
|
suffix: formatUpstreamLinesAbsent,
|
|
635
579
|
hint: [
|
|
636
580
|
"these files have fork changes but are protected (pinned); upstream did not change them since the last sync.",
|
|
637
|
-
"pinned files keep the fork
|
|
581
|
+
"pinned files keep the fork copy whole; a count here is upstream content the fork never received, deliberately or not: `analyze --unpinned` shows what a merge would bring in."
|
|
638
582
|
]
|
|
639
583
|
});
|
|
640
584
|
printFileSection(protectedBehind, `${warningMark} ${colors_default.yellow("protected but behind upstream")}`, linkOptions, {
|
|
@@ -659,30 +603,6 @@ function printAnalysisFileGroups(files, linkOptions, diffSource) {
|
|
|
659
603
|
dateSource: "upstream"
|
|
660
604
|
});
|
|
661
605
|
}
|
|
662
|
-
function writeLogFile(forkPath, files) {
|
|
663
|
-
const logPath = join3(forkPath, "cella-sync.log");
|
|
664
|
-
const lines = [
|
|
665
|
-
`cella sync analysis: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
666
|
-
DIVIDER,
|
|
667
|
-
"",
|
|
668
|
-
`complete file list (${files.length} files)`,
|
|
669
|
-
DIVIDER,
|
|
670
|
-
""
|
|
671
|
-
];
|
|
672
|
-
const sortedFiles = [...files].sort((a, b) => {
|
|
673
|
-
const aOrder = statusOrder.indexOf(a.status);
|
|
674
|
-
const bOrder = statusOrder.indexOf(b.status);
|
|
675
|
-
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
676
|
-
return a.path.localeCompare(b.path);
|
|
677
|
-
});
|
|
678
|
-
for (const file of sortedFiles) {
|
|
679
|
-
const config = statusConfig[file.status];
|
|
680
|
-
const icon = config.icon.replace(/\x1b\[[0-9;]*m/g, "");
|
|
681
|
-
lines.push(` ${icon} ${config.label.padEnd(12)} ${file.path}`);
|
|
682
|
-
}
|
|
683
|
-
writeFileSync(logPath, lines.join("\n"), "utf8");
|
|
684
|
-
return logPath;
|
|
685
|
-
}
|
|
686
606
|
function printSyncComplete(result, options = {}) {
|
|
687
607
|
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
688
608
|
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
@@ -708,7 +628,7 @@ function printProtectedConflicts(result) {
|
|
|
708
628
|
console.info();
|
|
709
629
|
console.info(
|
|
710
630
|
`${warningMark} ${colors_default.yellow(
|
|
711
|
-
`${paths.length
|
|
631
|
+
`${plural(paths.length, "protected file kept", "protected files kept")} the fork version, dropping upstream changes:`
|
|
712
632
|
)}`
|
|
713
633
|
);
|
|
714
634
|
for (const path of paths) {
|
|
@@ -718,8 +638,8 @@ function printProtectedConflicts(result) {
|
|
|
718
638
|
}
|
|
719
639
|
for (const line of [PROTECTED_BEHIND_HINT, ...protectedDiffHints(result, paths)]) console.info(colors_default.dim(` ${line}`));
|
|
720
640
|
}
|
|
721
|
-
function plural(
|
|
722
|
-
return `${
|
|
641
|
+
function plural(count2, singular, pluralForm = `${singular}s`) {
|
|
642
|
+
return `${count2} ${count2 === 1 ? singular : pluralForm}`;
|
|
723
643
|
}
|
|
724
644
|
function shellPath(path) {
|
|
725
645
|
return /^[\w./@+-]+$/.test(path) ? path : `'${path.replace(/'/g, `'\\''`)}'`;
|
|
@@ -727,7 +647,7 @@ function shellPath(path) {
|
|
|
727
647
|
function printIgnoredUpstreamChanges(result) {
|
|
728
648
|
const groups = result.ignoredUpstreamChanges ?? [];
|
|
729
649
|
if (groups.length === 0) return;
|
|
730
|
-
const total = groups.reduce((
|
|
650
|
+
const total = groups.reduce((count2, group) => count2 + group.paths.length, 0);
|
|
731
651
|
console.info();
|
|
732
652
|
console.info(
|
|
733
653
|
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
@@ -775,11 +695,10 @@ function findMaskingPins(files) {
|
|
|
775
695
|
function printMaskingPinWarning(files) {
|
|
776
696
|
const masking = findMaskingPins(files);
|
|
777
697
|
if (masking.length === 0) return;
|
|
778
|
-
const many = masking.length > 1;
|
|
779
698
|
console.info();
|
|
780
699
|
console.info(
|
|
781
700
|
`${warningMark} ${colors_default.yellow(
|
|
782
|
-
`${masking.length
|
|
701
|
+
`${plural(masking.length, "pinned file matches", "pinned files match")} the previous upstream but changed upstream:`
|
|
783
702
|
)}`
|
|
784
703
|
);
|
|
785
704
|
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
@@ -791,49 +710,27 @@ function printMaskingPinWarning(files) {
|
|
|
791
710
|
);
|
|
792
711
|
console.info();
|
|
793
712
|
}
|
|
794
|
-
function printFlagWarnings(options) {
|
|
795
|
-
const { hard, unpinned } = options;
|
|
796
|
-
if (!hard && !unpinned) return;
|
|
797
|
-
if (hard) {
|
|
798
|
-
console.info(colors_default.yellow("\u26A0 --hard used: drifted files were treated as behind, overwriting with incoming."));
|
|
799
|
-
}
|
|
800
|
-
if (unpinned) {
|
|
801
|
-
console.info(
|
|
802
|
-
colors_default.yellow(
|
|
803
|
-
"\u26A0 --unpinned used: pinned files in cella/cella.config.ts were ignored, this can result in more drifts."
|
|
804
|
-
)
|
|
805
|
-
);
|
|
806
|
-
}
|
|
807
|
-
console.info(colors_default.yellow(" be extra careful and cherry-pick what you want only."));
|
|
808
|
-
console.info();
|
|
809
|
-
}
|
|
810
713
|
function printEngineReports(result, title) {
|
|
811
714
|
printSummary(result.summary, title);
|
|
812
715
|
printIgnoredUpstreamChanges(result);
|
|
813
716
|
printUpstreamOverrideChanges(result);
|
|
814
717
|
printMaskingPinWarning(result.files);
|
|
815
718
|
}
|
|
816
|
-
function printLogFileReport(config, files) {
|
|
817
|
-
if (!config.logFile) return;
|
|
818
|
-
const logPath = writeLogFile(config.forkPath, files);
|
|
819
|
-
console.info();
|
|
820
|
-
console.info(colors_default.dim(`full file list written to: ${logPath}`));
|
|
821
|
-
}
|
|
822
719
|
|
|
823
720
|
// src/utils/overrides.ts
|
|
824
721
|
import { existsSync as existsSync5 } from "fs";
|
|
825
|
-
import { join as
|
|
722
|
+
import { join as join6 } from "path";
|
|
826
723
|
|
|
827
724
|
// src/utils/git.ts
|
|
828
725
|
import { execFile } from "child_process";
|
|
829
726
|
import { existsSync as existsSync3 } from "fs";
|
|
830
727
|
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
831
|
-
import { dirname as dirname2, join as
|
|
728
|
+
import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
|
|
832
729
|
import { promisify } from "util";
|
|
833
730
|
|
|
834
731
|
// src/utils/manifest.ts
|
|
835
732
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
836
|
-
import { dirname, join as
|
|
733
|
+
import { dirname, join as join3 } from "path";
|
|
837
734
|
var MANIFEST_FILE = "cella/cella.manifest.json";
|
|
838
735
|
function parseSyncManifest(raw) {
|
|
839
736
|
try {
|
|
@@ -847,7 +744,7 @@ function parseSyncManifest(raw) {
|
|
|
847
744
|
}
|
|
848
745
|
async function readSyncManifest(cwd) {
|
|
849
746
|
try {
|
|
850
|
-
return parseSyncManifest(await readFile(
|
|
747
|
+
return parseSyncManifest(await readFile(join3(cwd, MANIFEST_FILE), "utf8"));
|
|
851
748
|
} catch {
|
|
852
749
|
return null;
|
|
853
750
|
}
|
|
@@ -857,7 +754,7 @@ async function readManifestBase(cwd) {
|
|
|
857
754
|
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
858
755
|
}
|
|
859
756
|
async function writeSyncManifest(cwd, manifest) {
|
|
860
|
-
const manifestPath =
|
|
757
|
+
const manifestPath = join3(cwd, MANIFEST_FILE);
|
|
861
758
|
await mkdir(dirname(manifestPath), { recursive: true });
|
|
862
759
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
863
760
|
`, "utf8");
|
|
@@ -878,9 +775,8 @@ async function git(args, cwd, options = {}) {
|
|
|
878
775
|
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
879
776
|
...options.env
|
|
880
777
|
};
|
|
881
|
-
const maxBuffer = options.maxBuffer ?? MAX_BUFFER;
|
|
882
778
|
try {
|
|
883
|
-
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer });
|
|
779
|
+
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer: MAX_BUFFER });
|
|
884
780
|
return stdout.trim();
|
|
885
781
|
} catch (error) {
|
|
886
782
|
if (options.ignoreErrors) return "";
|
|
@@ -890,6 +786,9 @@ async function git(args, cwd, options = {}) {
|
|
|
890
786
|
throw error;
|
|
891
787
|
}
|
|
892
788
|
}
|
|
789
|
+
async function revParse(cwd, ref) {
|
|
790
|
+
return await git(["rev-parse", "-q", "--verify", ref], cwd, { ignoreErrors: true }) || null;
|
|
791
|
+
}
|
|
893
792
|
async function getCurrentBranch(cwd) {
|
|
894
793
|
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
895
794
|
}
|
|
@@ -903,7 +802,7 @@ async function createBranchFrom(cwd, branch, startPoint) {
|
|
|
903
802
|
await git(["switch", "-c", branch, "--no-track", startPoint], cwd);
|
|
904
803
|
}
|
|
905
804
|
async function branchExists(cwd, branch) {
|
|
906
|
-
return await
|
|
805
|
+
return await revParse(cwd, `refs/heads/${branch}`) !== null;
|
|
907
806
|
}
|
|
908
807
|
async function getBranchWorktree(cwd, branch) {
|
|
909
808
|
const output = await git(["worktree", "list", "--porcelain"], cwd, { ignoreErrors: true });
|
|
@@ -968,7 +867,7 @@ async function stageAll(cwd) {
|
|
|
968
867
|
await git(["add", "-A"], cwd);
|
|
969
868
|
}
|
|
970
869
|
async function mergeInProgress(cwd) {
|
|
971
|
-
return await
|
|
870
|
+
return await revParse(cwd, "MERGE_HEAD") !== null;
|
|
972
871
|
}
|
|
973
872
|
async function getShortSha(cwd, ref) {
|
|
974
873
|
return git(["rev-parse", "--short", ref], cwd);
|
|
@@ -978,16 +877,9 @@ async function getWorkingTreeChangeCount(cwd) {
|
|
|
978
877
|
if (!status) return 0;
|
|
979
878
|
return status.split("\n").filter(Boolean).length;
|
|
980
879
|
}
|
|
981
|
-
async function remoteExists(cwd, remoteName) {
|
|
982
|
-
const remotes = await git(["remote"], cwd);
|
|
983
|
-
return remotes.split("\n").includes(remoteName);
|
|
984
|
-
}
|
|
985
880
|
async function ensureRemote(cwd, remoteName, url) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
} else {
|
|
989
|
-
await git(["remote", "add", remoteName, url], cwd);
|
|
990
|
-
}
|
|
881
|
+
await git(["remote", "add", remoteName, url], cwd, { ignoreErrors: true });
|
|
882
|
+
await git(["remote", "set-url", remoteName, url], cwd);
|
|
991
883
|
}
|
|
992
884
|
async function fetchRemote(cwd, remoteName) {
|
|
993
885
|
await git(["fetch", remoteName], cwd);
|
|
@@ -1014,7 +906,7 @@ async function resolveUpstreamCommit(cwd, remoteName, ref) {
|
|
|
1014
906
|
if (ref.startsWith("-")) return null;
|
|
1015
907
|
const tagRef = `refs/${remoteName}/tags/${ref}`;
|
|
1016
908
|
for (const candidate of [`refs/remotes/${remoteName}/${ref}`, tagRef, ref]) {
|
|
1017
|
-
const sha = await
|
|
909
|
+
const sha = await revParse(cwd, `${candidate}^{commit}`);
|
|
1018
910
|
if (!sha) continue;
|
|
1019
911
|
const release = candidate === tagRef && releaseTagPattern.test(ref) ? { tag: ref, ref: tagRef } : void 0;
|
|
1020
912
|
return { sha, release };
|
|
@@ -1048,9 +940,9 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
1048
940
|
const output = await git(["rev-list", "--count", `${fromRef}..${toRef}`], cwd);
|
|
1049
941
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
1050
942
|
}
|
|
1051
|
-
async function listCommitsBetween(cwd, fromRef, toRef,
|
|
1052
|
-
const { skip, limit } = options;
|
|
943
|
+
async function listCommitsBetween(cwd, fromRef, toRef, limit) {
|
|
1053
944
|
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
945
|
+
if (limit) args.push(`-n${limit}`);
|
|
1054
946
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
1055
947
|
if (!output) return [];
|
|
1056
948
|
const commits = [];
|
|
@@ -1060,61 +952,32 @@ async function listCommitsBetween(cwd, fromRef, toRef, options = {}) {
|
|
|
1060
952
|
if (!hash || !message || !date) continue;
|
|
1061
953
|
commits.push({ hash, message, date });
|
|
1062
954
|
}
|
|
1063
|
-
|
|
1064
|
-
return limit && limit > 0 ? commits.slice(start, start + limit) : commits.slice(start);
|
|
955
|
+
return commits;
|
|
1065
956
|
}
|
|
1066
957
|
async function getFileChangeInfo(cwd, fromRef, toRef) {
|
|
1067
958
|
const info = /* @__PURE__ */ new Map();
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
} else if (currentHash && currentDate && !info.has(line)) {
|
|
1085
|
-
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
1086
|
-
}
|
|
959
|
+
const output = await git(["log", "--name-only", "--format=%h %ct %ar", `${fromRef}..${toRef}`], cwd, {
|
|
960
|
+
ignoreErrors: true
|
|
961
|
+
});
|
|
962
|
+
if (!output) return info;
|
|
963
|
+
let currentHash = "";
|
|
964
|
+
let currentDate = "";
|
|
965
|
+
let currentTimestamp = 0;
|
|
966
|
+
for (const line of output.split("\n")) {
|
|
967
|
+
if (!line) continue;
|
|
968
|
+
const match = line.match(/^([a-f0-9]{7,})\s+(\d+)\s+(.+)$/);
|
|
969
|
+
if (match) {
|
|
970
|
+
currentHash = match[1];
|
|
971
|
+
currentTimestamp = Number(match[2]);
|
|
972
|
+
currentDate = match[3];
|
|
973
|
+
} else if (currentHash && currentDate && !info.has(line)) {
|
|
974
|
+
info.set(line, { date: currentDate, hash: currentHash, timestamp: currentTimestamp });
|
|
1087
975
|
}
|
|
1088
|
-
} catch {
|
|
1089
976
|
}
|
|
1090
977
|
return info;
|
|
1091
978
|
}
|
|
1092
979
|
async function isClean(cwd) {
|
|
1093
|
-
|
|
1094
|
-
return !status;
|
|
1095
|
-
}
|
|
1096
|
-
async function assertClean(cwd, subject = "working directory") {
|
|
1097
|
-
if (!await isClean(cwd)) {
|
|
1098
|
-
throw new Error(`${subject} has uncommitted changes. please commit or stash before syncing.`);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
async function createWorktree(cwd, worktreePath, commitRef) {
|
|
1102
|
-
await git(["worktree", "add", "--detach", worktreePath, commitRef], cwd);
|
|
1103
|
-
}
|
|
1104
|
-
async function removeWorktree(cwd, worktreePath) {
|
|
1105
|
-
await git(["worktree", "remove", worktreePath, "--force"], cwd, { ignoreErrors: true });
|
|
1106
|
-
await git(["worktree", "prune"], cwd, { ignoreErrors: true });
|
|
1107
|
-
}
|
|
1108
|
-
async function listWorktrees(cwd) {
|
|
1109
|
-
const output = await git(["worktree", "list", "--porcelain"], cwd);
|
|
1110
|
-
const lines = output.split("\n");
|
|
1111
|
-
const paths = [];
|
|
1112
|
-
for (const line of lines) {
|
|
1113
|
-
if (line.startsWith("worktree ")) {
|
|
1114
|
-
paths.push(line.slice(9));
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
return paths;
|
|
980
|
+
return await getWorkingTreeChangeCount(cwd) === 0;
|
|
1118
981
|
}
|
|
1119
982
|
async function merge(cwd, ref) {
|
|
1120
983
|
try {
|
|
@@ -1140,8 +1003,8 @@ async function fileExistsAtRef(cwd, ref, filePath) {
|
|
|
1140
1003
|
const result = await git(["ls-tree", "--name-only", ref, "--", filePath], cwd, { ignoreErrors: true });
|
|
1141
1004
|
return result !== "";
|
|
1142
1005
|
}
|
|
1143
|
-
async function checkoutFromRef(cwd, ref,
|
|
1144
|
-
await git(["checkout", ref, "--",
|
|
1006
|
+
async function checkoutFromRef(cwd, ref, paths) {
|
|
1007
|
+
await git(["checkout", ref, "--", ...paths], cwd);
|
|
1145
1008
|
}
|
|
1146
1009
|
async function restoreWorktreeFromRef(cwd, ref, filePath) {
|
|
1147
1010
|
await git(["restore", `--source=${ref}`, "--worktree", "--", filePath], cwd);
|
|
@@ -1162,6 +1025,10 @@ async function listIdenticalToRef(cwd, ref) {
|
|
|
1162
1025
|
const differs = new Set(nulSeparated(changed));
|
|
1163
1026
|
return new Set(nulSeparated(tracked).filter((path) => !differs.has(path)));
|
|
1164
1027
|
}
|
|
1028
|
+
async function listChangedSinceRef(cwd, ref, paths) {
|
|
1029
|
+
const out = await git(["--literal-pathspecs", "diff", "--name-only", "--no-renames", "-z", ref, "--", ...paths], cwd);
|
|
1030
|
+
return out.split("\0").filter(Boolean);
|
|
1031
|
+
}
|
|
1165
1032
|
async function getMergeBase(cwd, ref1, ref2) {
|
|
1166
1033
|
return git(["merge-base", ref1, ref2], cwd);
|
|
1167
1034
|
}
|
|
@@ -1170,30 +1037,17 @@ async function getFileChanges(cwd, baseRef, targetRef) {
|
|
|
1170
1037
|
ignoreErrors: true
|
|
1171
1038
|
});
|
|
1172
1039
|
const changes = /* @__PURE__ */ new Map();
|
|
1173
|
-
if (!output) return changes;
|
|
1174
1040
|
const hashOrNull = (hash) => hash === "0".repeat(40) ? null : hash;
|
|
1175
1041
|
for (const line of output.split("\n")) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
});
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
const match = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) ([ADMT])\t(.+)$/);
|
|
1189
|
-
if (match) {
|
|
1190
|
-
const [, baseHash, targetHash, status, filePath] = match;
|
|
1191
|
-
changes.set(filePath, {
|
|
1192
|
-
status,
|
|
1193
|
-
baseHash: hashOrNull(baseHash),
|
|
1194
|
-
targetHash: hashOrNull(targetHash)
|
|
1195
|
-
});
|
|
1196
|
-
}
|
|
1042
|
+
const m = line.match(/^:\d+ \d+ ([a-f0-9]+) ([a-f0-9]+) ([ADMRT])\d*\t([^\t]+)(?:\t(.+))?$/);
|
|
1043
|
+
if (!m) continue;
|
|
1044
|
+
const status = m[3];
|
|
1045
|
+
changes.set(m[5] ?? m[4], {
|
|
1046
|
+
status,
|
|
1047
|
+
baseHash: hashOrNull(m[1]),
|
|
1048
|
+
targetHash: hashOrNull(m[2]),
|
|
1049
|
+
...status === "R" ? { oldPath: m[4] } : {}
|
|
1050
|
+
});
|
|
1197
1051
|
}
|
|
1198
1052
|
return changes;
|
|
1199
1053
|
}
|
|
@@ -1228,38 +1082,23 @@ async function getFileHashesAtRef(cwd, ref) {
|
|
|
1228
1082
|
}
|
|
1229
1083
|
return hashes;
|
|
1230
1084
|
}
|
|
1231
|
-
async function
|
|
1232
|
-
await git(["
|
|
1233
|
-
}
|
|
1234
|
-
async function batchRestoreToHead(cwd, filePaths) {
|
|
1235
|
-
if (filePaths.length === 0) return;
|
|
1236
|
-
await git(["checkout", "HEAD", "--", ...filePaths], cwd);
|
|
1237
|
-
}
|
|
1238
|
-
async function gitRm(cwd, filePath) {
|
|
1239
|
-
await git(["rm", "-f", "--", filePath], cwd, { ignoreErrors: true });
|
|
1240
|
-
}
|
|
1241
|
-
async function batchGitRm(cwd, filePaths) {
|
|
1242
|
-
if (filePaths.length === 0) return;
|
|
1243
|
-
await git(["rm", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1085
|
+
async function gitRm(cwd, paths, options = {}) {
|
|
1086
|
+
await git(["rm", ...options.cached ? ["--cached"] : [], "-f", "--", ...paths], cwd, { ignoreErrors: true });
|
|
1244
1087
|
}
|
|
1245
1088
|
async function getStagedNewFiles(cwd) {
|
|
1246
1089
|
const output = await git(["diff", "--cached", "--name-only", "--diff-filter=A"], cwd, { ignoreErrors: true });
|
|
1247
1090
|
return output ? output.split("\n").filter(Boolean) : [];
|
|
1248
1091
|
}
|
|
1249
|
-
async function batchUnstageFiles(cwd, filePaths) {
|
|
1250
|
-
if (filePaths.length === 0) return;
|
|
1251
|
-
await git(["rm", "--cached", "-f", "--", ...filePaths], cwd, { ignoreErrors: true });
|
|
1252
|
-
}
|
|
1253
1092
|
async function gitMv(cwd, oldPath, newPath) {
|
|
1254
|
-
const newDir = dirname2(
|
|
1093
|
+
const newDir = dirname2(join4(cwd, newPath));
|
|
1255
1094
|
await mkdir2(newDir, { recursive: true });
|
|
1256
1095
|
await git(["mv", "-f", oldPath, newPath], cwd);
|
|
1257
1096
|
}
|
|
1258
1097
|
async function fileExistsInWorktree(cwd, filePath) {
|
|
1259
|
-
return existsSync3(
|
|
1098
|
+
return existsSync3(join4(cwd, filePath));
|
|
1260
1099
|
}
|
|
1261
1100
|
async function removeFileFromWorktree(cwd, filePath) {
|
|
1262
|
-
const fullPath =
|
|
1101
|
+
const fullPath = join4(cwd, filePath);
|
|
1263
1102
|
try {
|
|
1264
1103
|
await unlink(fullPath);
|
|
1265
1104
|
await cleanupEmptyParentDirs(cwd, dirname2(filePath));
|
|
@@ -1267,12 +1106,12 @@ async function removeFileFromWorktree(cwd, filePath) {
|
|
|
1267
1106
|
}
|
|
1268
1107
|
}
|
|
1269
1108
|
async function removeFileFully(cwd, filePath) {
|
|
1270
|
-
await gitRm(cwd, filePath);
|
|
1109
|
+
await gitRm(cwd, [filePath]);
|
|
1271
1110
|
await removeFileFromWorktree(cwd, filePath);
|
|
1272
1111
|
}
|
|
1273
1112
|
async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
1274
1113
|
if (!relativePath || relativePath === ".") return;
|
|
1275
|
-
const fullPath =
|
|
1114
|
+
const fullPath = join4(cwd, relativePath);
|
|
1276
1115
|
try {
|
|
1277
1116
|
const entries = await readdir(fullPath);
|
|
1278
1117
|
if (entries.length === 0) {
|
|
@@ -1284,21 +1123,14 @@ async function cleanupEmptyParentDirs(cwd, relativePath) {
|
|
|
1284
1123
|
}
|
|
1285
1124
|
async function storeLastSyncRef(cwd, upstreamHash) {
|
|
1286
1125
|
await git(["update-ref", "refs/cella/last-sync", upstreamHash], cwd);
|
|
1287
|
-
const head = await
|
|
1288
|
-
if (head)
|
|
1289
|
-
await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1290
|
-
}
|
|
1126
|
+
const head = await revParse(cwd, "HEAD");
|
|
1127
|
+
if (head) await git(["update-ref", "refs/cella/last-sync-head", head], cwd);
|
|
1291
1128
|
}
|
|
1292
1129
|
async function stagePath(cwd, path) {
|
|
1293
1130
|
await git(["add", "--", path], cwd, { ignoreErrors: true });
|
|
1294
1131
|
}
|
|
1295
1132
|
async function getStoredSyncRef(cwd) {
|
|
1296
|
-
|
|
1297
|
-
return ref || null;
|
|
1298
|
-
}
|
|
1299
|
-
async function getStoredSyncHead(cwd) {
|
|
1300
|
-
const ref = await git(["rev-parse", "refs/cella/last-sync-head"], cwd, { ignoreErrors: true });
|
|
1301
|
-
return ref || null;
|
|
1133
|
+
return revParse(cwd, "refs/cella/last-sync");
|
|
1302
1134
|
}
|
|
1303
1135
|
async function readManifestAtRef(cwd, ref) {
|
|
1304
1136
|
const raw = await git(["show", `${ref}:${MANIFEST_FILE}`], cwd, { ignoreErrors: true });
|
|
@@ -1320,57 +1152,13 @@ async function readPackageVersionAtRef(cwd, ref) {
|
|
|
1320
1152
|
}
|
|
1321
1153
|
}
|
|
1322
1154
|
async function commitObjectExists(cwd, sha) {
|
|
1323
|
-
|
|
1324
|
-
await git(["cat-file", "-e", `${sha}^{commit}`], cwd);
|
|
1325
|
-
return true;
|
|
1326
|
-
} catch {
|
|
1327
|
-
return false;
|
|
1328
|
-
}
|
|
1155
|
+
return await revParse(cwd, `${sha}^{commit}`) !== null;
|
|
1329
1156
|
}
|
|
1330
1157
|
async function getRootCommit(cwd, ref) {
|
|
1331
1158
|
const out = await git(["rev-list", "--max-parents=0", ref], cwd, { ignoreErrors: true });
|
|
1332
1159
|
const roots = out.split("\n").filter(Boolean);
|
|
1333
1160
|
return roots.length > 0 ? roots[roots.length - 1] : null;
|
|
1334
1161
|
}
|
|
1335
|
-
var SCAFFOLD_CANDIDATE_LIMIT = 400;
|
|
1336
|
-
var SCAFFOLD_SCORE_CONCURRENCY = 10;
|
|
1337
|
-
var SCAFFOLD_MATCH_MAX_DIFF_RATIO = 0.3;
|
|
1338
|
-
async function inferScaffoldBase(cwd, headRef, upstreamRef) {
|
|
1339
|
-
const root = await getRootCommit(cwd, headRef);
|
|
1340
|
-
if (!root) return null;
|
|
1341
|
-
const rootDate = await git(["show", "-s", "--format=%cI", root], cwd, { ignoreErrors: true });
|
|
1342
|
-
if (!rootDate) return null;
|
|
1343
|
-
const before = new Date(new Date(rootDate).getTime() + 24 * 60 * 60 * 1e3).toISOString();
|
|
1344
|
-
const revList = await git(
|
|
1345
|
-
["rev-list", `--max-count=${SCAFFOLD_CANDIDATE_LIMIT}`, `--before=${before}`, upstreamRef],
|
|
1346
|
-
cwd,
|
|
1347
|
-
{ ignoreErrors: true }
|
|
1348
|
-
);
|
|
1349
|
-
const candidates = revList.split("\n").filter(Boolean);
|
|
1350
|
-
if (candidates.length === 0) return null;
|
|
1351
|
-
const totalPaths = (await git(["ls-tree", "-r", "--name-only", root], cwd)).split("\n").filter(Boolean).length;
|
|
1352
|
-
if (totalPaths === 0) return null;
|
|
1353
|
-
let best = null;
|
|
1354
|
-
outer: for (let i = 0; i < candidates.length; i += SCAFFOLD_SCORE_CONCURRENCY) {
|
|
1355
|
-
const chunk = candidates.slice(i, i + SCAFFOLD_SCORE_CONCURRENCY);
|
|
1356
|
-
const scores = await Promise.all(
|
|
1357
|
-
chunk.map(async (sha) => {
|
|
1358
|
-
try {
|
|
1359
|
-
const out = await git(["diff-tree", "-r", "--name-only", sha, root], cwd);
|
|
1360
|
-
return { sha, diff: out ? out.split("\n").length : 0 };
|
|
1361
|
-
} catch {
|
|
1362
|
-
return { sha, diff: Number.POSITIVE_INFINITY };
|
|
1363
|
-
}
|
|
1364
|
-
})
|
|
1365
|
-
);
|
|
1366
|
-
for (const score of scores) {
|
|
1367
|
-
if (!best || score.diff < best.diff) best = score;
|
|
1368
|
-
if (best.diff === 0) break outer;
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
if (!best || best.diff / totalPaths > SCAFFOLD_MATCH_MAX_DIFF_RATIO) return null;
|
|
1372
|
-
return best.sha;
|
|
1373
|
-
}
|
|
1374
1162
|
async function readRootTrailerBase(cwd, headRef) {
|
|
1375
1163
|
const root = await getRootCommit(cwd, headRef);
|
|
1376
1164
|
if (!root) return null;
|
|
@@ -1409,8 +1197,7 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
|
1409
1197
|
() => getStoredSyncRef(cwd),
|
|
1410
1198
|
() => readManifestBaseAtRef(cwd, headRef),
|
|
1411
1199
|
() => readManifestBase(cwd),
|
|
1412
|
-
() => readRootTrailerBase(cwd, headRef)
|
|
1413
|
-
() => inferScaffoldBase(cwd, headRef, upstreamRef)
|
|
1200
|
+
() => readRootTrailerBase(cwd, headRef)
|
|
1414
1201
|
];
|
|
1415
1202
|
let baseSha = null;
|
|
1416
1203
|
for (const source of sources) {
|
|
@@ -1425,7 +1212,7 @@ async function ensureSyncBase(cwd, headRef, upstreamRef) {
|
|
|
1425
1212
|
`no common ancestor between the fork and '${upstreamRef}', and no sync base could be determined.
|
|
1426
1213
|
|
|
1427
1214
|
This fork has unrelated history with upstream (typical for a create-cella scaffold, or after
|
|
1428
|
-
upstream squashed its history) and no recorded
|
|
1215
|
+
upstream squashed its history) and no recorded base commit exists after fetching
|
|
1429
1216
|
upstream. Seed it manually with the upstream commit the fork was created from, then re-run sync:
|
|
1430
1217
|
|
|
1431
1218
|
git update-ref refs/cella/last-sync <upstream-sha>
|
|
@@ -1448,9 +1235,9 @@ async function isAncestor(cwd, ancestor, descendant) {
|
|
|
1448
1235
|
async function getLegacySyncRef(cwd, headRef) {
|
|
1449
1236
|
const storedRef = await getStoredSyncRef(cwd);
|
|
1450
1237
|
if (!storedRef) return null;
|
|
1451
|
-
const storedHead = await
|
|
1238
|
+
const storedHead = await revParse(cwd, "refs/cella/last-sync-head");
|
|
1452
1239
|
if (storedHead) {
|
|
1453
|
-
const currentHead = await
|
|
1240
|
+
const currentHead = await revParse(cwd, headRef);
|
|
1454
1241
|
if (currentHead && currentHead === storedHead) return null;
|
|
1455
1242
|
}
|
|
1456
1243
|
return storedRef;
|
|
@@ -1468,7 +1255,7 @@ async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
|
1468
1255
|
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
1469
1256
|
}
|
|
1470
1257
|
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
1471
|
-
const headSha = await
|
|
1258
|
+
const headSha = await revParse(cwd, headRef);
|
|
1472
1259
|
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
1473
1260
|
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
1474
1261
|
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
@@ -1486,7 +1273,7 @@ async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
|
1486
1273
|
// src/utils/migration-notes.ts
|
|
1487
1274
|
import { existsSync as existsSync4 } from "fs";
|
|
1488
1275
|
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1489
|
-
import { dirname as dirname3, join as
|
|
1276
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1490
1277
|
var NOTES_DIR = "cella/migrations";
|
|
1491
1278
|
var PENDING_FILE = "cella/cella.migrations.json";
|
|
1492
1279
|
var NOTE_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
@@ -1541,17 +1328,14 @@ async function arrivedNoteIds(cwd, from, to) {
|
|
|
1541
1328
|
const known = new Set(before);
|
|
1542
1329
|
return after.filter((id) => !known.has(id));
|
|
1543
1330
|
}
|
|
1544
|
-
async function readPending(cwd
|
|
1545
|
-
const path =
|
|
1331
|
+
async function readPending(cwd) {
|
|
1332
|
+
const path = join5(cwd, PENDING_FILE);
|
|
1546
1333
|
if (!existsSync4(path)) return [];
|
|
1547
1334
|
const parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
1548
|
-
|
|
1549
|
-
if (!Array.isArray(parsed.applied) || !syncRef) return [];
|
|
1550
|
-
const applied = new Set(parsed.applied);
|
|
1551
|
-
return (await listNoteIds(cwd, syncRef)).filter((id) => !applied.has(id));
|
|
1335
|
+
return Array.isArray(parsed.pending) ? parsed.pending.filter((id) => typeof id === "string") : [];
|
|
1552
1336
|
}
|
|
1553
1337
|
async function writePending(cwd, ids) {
|
|
1554
|
-
const path =
|
|
1338
|
+
const path = join5(cwd, PENDING_FILE);
|
|
1555
1339
|
const pending = [...new Set(ids)].sort();
|
|
1556
1340
|
if (pending.length === 0) {
|
|
1557
1341
|
await rm(path, { force: true });
|
|
@@ -1572,9 +1356,12 @@ function formatNotesLine(total, open, arrived, options = {}) {
|
|
|
1572
1356
|
}
|
|
1573
1357
|
|
|
1574
1358
|
// src/utils/overrides.ts
|
|
1359
|
+
function normalizeEntry(entry) {
|
|
1360
|
+
return entry.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1361
|
+
}
|
|
1575
1362
|
function isUnderAnyFolder(filePath, folders) {
|
|
1576
1363
|
return folders.some((folder) => {
|
|
1577
|
-
const entry = folder
|
|
1364
|
+
const entry = normalizeEntry(folder);
|
|
1578
1365
|
return filePath === entry || filePath.startsWith(`${entry}/`);
|
|
1579
1366
|
});
|
|
1580
1367
|
}
|
|
@@ -1585,13 +1372,17 @@ function isPinned(filePath, config) {
|
|
|
1585
1372
|
if (isManagedFile(filePath)) return true;
|
|
1586
1373
|
return isUnderAnyFolder(filePath, config.overrides?.pinned || []);
|
|
1587
1374
|
}
|
|
1588
|
-
function
|
|
1589
|
-
const entries = [...new Set(
|
|
1375
|
+
function entryMatcher(list) {
|
|
1376
|
+
const entries = [...new Set(list.map(normalizeEntry).filter(Boolean))];
|
|
1590
1377
|
const mostSpecificFirst = [...entries].sort((a, b) => b.length - a.length);
|
|
1378
|
+
return [entries, (path) => mostSpecificFirst.find((entry) => isUnderAnyFolder(path, [entry]))];
|
|
1379
|
+
}
|
|
1380
|
+
function groupIgnoredUpstreamChanges(files, ignored) {
|
|
1381
|
+
const [entries, findEntry] = entryMatcher(ignored);
|
|
1591
1382
|
const groups = /* @__PURE__ */ new Map();
|
|
1592
1383
|
for (const file of files) {
|
|
1593
1384
|
if (!file.upstreamOnly || isManagedFile(file.path) || isGeneratedFile(file.path)) continue;
|
|
1594
|
-
const entry =
|
|
1385
|
+
const entry = findEntry(file.path);
|
|
1595
1386
|
if (!entry) continue;
|
|
1596
1387
|
const group = groups.get(entry) ?? { entry, paths: [], added: 0, deleted: 0 };
|
|
1597
1388
|
group.paths.push(file.path);
|
|
@@ -1603,11 +1394,10 @@ function groupIgnoredUpstreamChanges(files, ignored) {
|
|
|
1603
1394
|
return entries.flatMap((entry) => groups.get(entry) ?? []);
|
|
1604
1395
|
}
|
|
1605
1396
|
function groupProtectedUpstreamChanges(paths, config) {
|
|
1606
|
-
const
|
|
1607
|
-
const mostSpecificFirst = [...new Set(entries)].sort((a, b) => b.length - a.length);
|
|
1397
|
+
const [, findEntry] = entryMatcher([...config.overrides?.pinned ?? [], ...config.overrides?.ignored ?? []]);
|
|
1608
1398
|
const groups = /* @__PURE__ */ new Map();
|
|
1609
1399
|
for (const path of paths) {
|
|
1610
|
-
const entry =
|
|
1400
|
+
const entry = findEntry(path) ?? path;
|
|
1611
1401
|
const group = groups.get(entry) ?? { entry, paths: [] };
|
|
1612
1402
|
group.paths.push(path);
|
|
1613
1403
|
groups.set(entry, group);
|
|
@@ -1615,56 +1405,39 @@ function groupProtectedUpstreamChanges(paths, config) {
|
|
|
1615
1405
|
for (const group of groups.values()) group.paths.sort();
|
|
1616
1406
|
return [...groups.values()];
|
|
1617
1407
|
}
|
|
1618
|
-
function isPinnedForSync(filePath, config, unpinned) {
|
|
1619
|
-
if (isManagedFile(filePath)) return true;
|
|
1620
|
-
if (unpinned) return false;
|
|
1621
|
-
return isPinned(filePath, config);
|
|
1622
|
-
}
|
|
1623
|
-
function hasGlobChars(entry) {
|
|
1624
|
-
return entry.includes("*") || entry.includes("?");
|
|
1625
|
-
}
|
|
1626
1408
|
async function validateOverrides(config, forkPath, upstreamRef) {
|
|
1627
1409
|
const warnings = [];
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
});
|
|
1652
|
-
} else if (!existsSync5(
|
|
1653
|
-
if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef,
|
|
1654
|
-
warnings.push({
|
|
1655
|
-
type: "ignored-not-found",
|
|
1656
|
-
pattern: entry,
|
|
1657
|
-
message: `ignored entry not found: ${entry}`
|
|
1658
|
-
});
|
|
1410
|
+
const ignored = config.overrides?.ignored ?? [];
|
|
1411
|
+
const upstream = upstreamRef && await revParse(forkPath, upstreamRef) ? upstreamRef : void 0;
|
|
1412
|
+
const pinsUpstreamHas = [];
|
|
1413
|
+
for (const entry of config.overrides?.pinned ?? []) {
|
|
1414
|
+
const path = normalizeEntry(entry);
|
|
1415
|
+
const ignoredEntry = ignored.find((folder) => isUnderAnyFolder(path, [folder]));
|
|
1416
|
+
if (/[*?]/.test(entry)) warnings.push(`pin uses glob (not supported, use a path or folder): ${entry}`);
|
|
1417
|
+
else if (!existsSync5(join6(forkPath, path))) warnings.push(`pin not found: ${entry}`);
|
|
1418
|
+
else if (ignoredEntry) warnings.push(`pin does nothing, '${normalizeEntry(ignoredEntry)}' is ignored: ${entry}`);
|
|
1419
|
+
else if (!upstream) continue;
|
|
1420
|
+
else if (await fileExistsAtRef(forkPath, upstream, path)) pinsUpstreamHas.push(entry);
|
|
1421
|
+
else warnings.push(`pin does nothing, upstream no longer has it: ${entry}`);
|
|
1422
|
+
}
|
|
1423
|
+
if (upstream && pinsUpstreamHas.length > 0) {
|
|
1424
|
+
const differs = await listChangedSinceRef(forkPath, upstream, pinsUpstreamHas.map(normalizeEntry));
|
|
1425
|
+
for (const entry of pinsUpstreamHas) {
|
|
1426
|
+
if (differs.some((changed) => isUnderAnyFolder(changed, [entry]))) continue;
|
|
1427
|
+
warnings.push(`pin is identical to upstream, it only drops upstream's next change: ${entry}`);
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
for (const entry of config.overrides?.ignored ?? []) {
|
|
1431
|
+
const path = normalizeEntry(entry);
|
|
1432
|
+
if (/[*?]/.test(entry)) {
|
|
1433
|
+
warnings.push(`ignored entry uses glob (not supported, use a path or folder): ${entry}`);
|
|
1434
|
+
} else if (!existsSync5(join6(forkPath, path))) {
|
|
1435
|
+
if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef, path)) continue;
|
|
1436
|
+
warnings.push(`ignored entry not found: ${entry}`);
|
|
1659
1437
|
}
|
|
1660
1438
|
}
|
|
1661
1439
|
return warnings;
|
|
1662
1440
|
}
|
|
1663
|
-
function printWarnings(warnings) {
|
|
1664
|
-
for (const warning of warnings) {
|
|
1665
|
-
console.info(`${warningMark} ${warning.message}`);
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
1441
|
|
|
1669
1442
|
// src/cli.ts
|
|
1670
1443
|
var str = (value) => typeof value === "string" ? value : void 0;
|
|
@@ -1680,74 +1453,44 @@ function readOptions(opts, operands = []) {
|
|
|
1680
1453
|
throw new Error(`unexpected argument '${operands[0]}'. arguments after the options only go with --run <id>`);
|
|
1681
1454
|
}
|
|
1682
1455
|
return {
|
|
1683
|
-
logFile: flag(opts.log),
|
|
1684
1456
|
verbose: flag(opts.verbose),
|
|
1685
1457
|
list: flag(opts.list),
|
|
1686
1458
|
json: flag(opts.json),
|
|
1687
|
-
diff: str(opts.diff),
|
|
1688
|
-
openDiff: str(opts.openDiff),
|
|
1689
1459
|
scope: normalizedScope,
|
|
1690
1460
|
fork: str(opts.fork),
|
|
1691
|
-
hard: flag(opts.hard),
|
|
1692
|
-
unpinned: flag(opts.unpinned),
|
|
1693
1461
|
track: opts.track === "release" || opts.track === "branch" ? opts.track : void 0,
|
|
1694
1462
|
ref: str(opts.ref) || void 0,
|
|
1695
1463
|
keepConfig: flag(opts.keepConfig),
|
|
1696
|
-
|
|
1697
|
-
checkOverrides: flag(opts.checkOverrides),
|
|
1698
|
-
coverage: flag(opts.coverage),
|
|
1464
|
+
unpinned: flag(opts.unpinned),
|
|
1699
1465
|
since: str(opts.since) || void 0,
|
|
1700
1466
|
md: flag(opts.md),
|
|
1701
1467
|
all: flag(opts.all),
|
|
1702
1468
|
show: str(opts.show),
|
|
1703
1469
|
extract: str(opts.extract),
|
|
1704
1470
|
run,
|
|
1705
|
-
script: str(opts.script),
|
|
1706
1471
|
runArgs: run ? operands : void 0,
|
|
1707
1472
|
mark: Array.isArray(opts.mark) ? opts.mark.filter((id) => typeof id === "string") : void 0
|
|
1708
1473
|
};
|
|
1709
1474
|
}
|
|
1710
|
-
var
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
var trackOption = {
|
|
1719
|
-
flags: "--track <mode>",
|
|
1720
|
-
description: "override upstream tracking for this run: release|branch"
|
|
1721
|
-
};
|
|
1722
|
-
var refOption = {
|
|
1723
|
-
flags: "--ref <ref>",
|
|
1724
|
-
description: "pin the upstream commit for this run: sha, release tag or branch"
|
|
1725
|
-
};
|
|
1726
|
-
var hardOption = {
|
|
1727
|
-
flags: "--hard",
|
|
1728
|
-
description: "overwrite drifted files with upstream version (aggressive realignment)"
|
|
1729
|
-
};
|
|
1730
|
-
var verboseOption = {
|
|
1731
|
-
flags: "-V, --verbose",
|
|
1732
|
-
description: "show detailed output during operations"
|
|
1733
|
-
};
|
|
1734
|
-
var forkOption = {
|
|
1735
|
-
flags: "--fork <name>",
|
|
1736
|
-
description: "pre-select a fork by name (skips the fork selection prompt)"
|
|
1737
|
-
};
|
|
1475
|
+
var jsonOption = ["--json", "machine-readable output for tooling/agents"];
|
|
1476
|
+
var trackOption = ["--track <mode>", "override upstream tracking for this run: release|branch"];
|
|
1477
|
+
var refOption = ["--ref <ref>", "pin the upstream commit for this run: sha, release tag or branch"];
|
|
1478
|
+
var unpinnedOption = [
|
|
1479
|
+
"--unpinned",
|
|
1480
|
+
"lift every pinned entry for this run (managed files stay pinned) and merge from the full upstream history"
|
|
1481
|
+
];
|
|
1482
|
+
var forkOption = ["--fork <name>", "pre-select a fork by name (skips the fork selection prompt)"];
|
|
1738
1483
|
var serviceDefinitions = [
|
|
1739
1484
|
{
|
|
1740
1485
|
name: "analyze",
|
|
1741
1486
|
description: "dry run to see what would change on sync",
|
|
1742
1487
|
options: [
|
|
1743
|
-
|
|
1744
|
-
{ flags: "--list", description: "non-interactive output for tooling (one file per line)" },
|
|
1488
|
+
["--list", "non-interactive output for tooling (one file per line)"],
|
|
1745
1489
|
jsonOption,
|
|
1746
|
-
|
|
1490
|
+
["--scope <scope>", "analyze scope for --list/--json: all|risk|protected"],
|
|
1747
1491
|
trackOption,
|
|
1748
1492
|
refOption,
|
|
1749
|
-
|
|
1750
|
-
{ flags: "--open-diff <path>", description: "open a browser diff for one file, then exit" }
|
|
1493
|
+
unpinnedOption
|
|
1751
1494
|
],
|
|
1752
1495
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1753
1496
|
},
|
|
@@ -1755,12 +1498,10 @@ var serviceDefinitions = [
|
|
|
1755
1498
|
name: "sync",
|
|
1756
1499
|
description: "merge upstream changes onto a fresh branch, sync package.json and open a squash-merge PR",
|
|
1757
1500
|
options: [
|
|
1758
|
-
logOption,
|
|
1759
|
-
hardOption,
|
|
1760
|
-
{ flags: "--unpinned", description: "ignore pinned files (except package.json) to resurface upstream changes" },
|
|
1761
1501
|
trackOption,
|
|
1762
1502
|
refOption,
|
|
1763
|
-
|
|
1503
|
+
["--keep-config", "merge with your sync config as it stands when upstream changed its own"],
|
|
1504
|
+
unpinnedOption
|
|
1764
1505
|
],
|
|
1765
1506
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1766
1507
|
},
|
|
@@ -1768,66 +1509,46 @@ var serviceDefinitions = [
|
|
|
1768
1509
|
name: "migrate",
|
|
1769
1510
|
description: "list the upstream migration notes this app has not handled yet",
|
|
1770
1511
|
options: [
|
|
1771
|
-
|
|
1512
|
+
["--all", "list every upstream note, handled or not"],
|
|
1772
1513
|
jsonOption,
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1514
|
+
["--show <id>", "print one note's README"],
|
|
1515
|
+
["--extract <id>", "write one note's folder under node_modules/.cache to run its codemod"],
|
|
1516
|
+
["--run <id>", "run one note's codemod; files identical to upstream stay as they are"],
|
|
1517
|
+
["--mark <ids...>", "record notes as handled"]
|
|
1518
|
+
],
|
|
1519
|
+
operands: [
|
|
1520
|
+
"[codemodArgs...]",
|
|
1521
|
+
"with --run, after `--`: arguments for the codemod (default: inventory and the note's roots)"
|
|
1778
1522
|
],
|
|
1779
|
-
operands: {
|
|
1780
|
-
flags: "[codemodArgs...]",
|
|
1781
|
-
description: "with --run, after `--`: arguments for the codemod (default: inventory and the note's roots)"
|
|
1782
|
-
},
|
|
1783
1523
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1784
1524
|
},
|
|
1785
1525
|
{
|
|
1786
1526
|
name: "audit",
|
|
1787
|
-
description: "
|
|
1788
|
-
options: [
|
|
1789
|
-
{ flags: "--list", description: "skip interactive update prompts after printing audit results" },
|
|
1790
|
-
{ flags: "--force", description: "bypass pnpm metadata cache for fresh registry data" },
|
|
1791
|
-
{ flags: "--check-overrides", description: "check which pnpm.overrides are still needed" }
|
|
1792
|
-
]
|
|
1527
|
+
description: "update outdated packages, then rebuild the security overrides from a fresh audit",
|
|
1528
|
+
options: [["--list", "report only: no prompts, and nothing is written"]]
|
|
1793
1529
|
},
|
|
1794
1530
|
{
|
|
1795
1531
|
name: "forks",
|
|
1796
1532
|
description: "run normal sync inside local fork repositories",
|
|
1797
1533
|
options: [
|
|
1798
1534
|
forkOption,
|
|
1799
|
-
|
|
1800
|
-
verboseOption,
|
|
1801
|
-
hardOption,
|
|
1802
|
-
{
|
|
1803
|
-
flags: "--keep-config",
|
|
1804
|
-
description: "merge with the fork's sync config as it stands when upstream changed its own"
|
|
1805
|
-
}
|
|
1535
|
+
["--keep-config", "merge with the fork's sync config as it stands when upstream changed its own"]
|
|
1806
1536
|
],
|
|
1807
1537
|
includeInMenu: (context) => context.hasForks
|
|
1808
1538
|
},
|
|
1809
1539
|
{
|
|
1810
1540
|
name: "contributions",
|
|
1811
1541
|
description: "pull and adopt changes from local forks",
|
|
1812
|
-
options: [
|
|
1813
|
-
forkOption,
|
|
1814
|
-
{ flags: "--list", description: "non-interactive output for tooling (tab-separated rows)" },
|
|
1815
|
-
jsonOption,
|
|
1816
|
-
{ flags: "--diff <path>", description: "print the unified diff for one contributed file, then exit" }
|
|
1817
|
-
],
|
|
1542
|
+
options: [forkOption, jsonOption],
|
|
1818
1543
|
includeInMenu: (context) => context.hasForks
|
|
1819
1544
|
},
|
|
1820
1545
|
{
|
|
1821
1546
|
name: "stats",
|
|
1822
1547
|
description: "count files by category and workspace package",
|
|
1823
1548
|
options: [
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
flags: "--since <ref>",
|
|
1828
|
-
description: "count what the current branch changed since it left <ref>, by kind of file"
|
|
1829
|
-
},
|
|
1830
|
-
{ flags: "--md", description: "with --since: print the table as markdown for a pull request description" }
|
|
1549
|
+
["-V, --verbose", "show detailed output during operations"],
|
|
1550
|
+
["--since <ref>", "count what the current branch changed since it left <ref>, by kind of file"],
|
|
1551
|
+
["--md", "with --since: print the table as markdown for a pull request description"]
|
|
1831
1552
|
]
|
|
1832
1553
|
}
|
|
1833
1554
|
];
|
|
@@ -1851,10 +1572,8 @@ function buildServiceChoices(context) {
|
|
|
1851
1572
|
}
|
|
1852
1573
|
function addServiceCommand(program, definition, setSelection) {
|
|
1853
1574
|
const command = program.command(definition.name).description(definition.description);
|
|
1854
|
-
for (const option of definition.options ?? [])
|
|
1855
|
-
|
|
1856
|
-
}
|
|
1857
|
-
if (definition.operands) command.argument(definition.operands.flags, definition.operands.description);
|
|
1575
|
+
for (const option of definition.options ?? []) command.option(...option);
|
|
1576
|
+
if (definition.operands) command.argument(...definition.operands);
|
|
1858
1577
|
command.action((...args) => {
|
|
1859
1578
|
const operands = definition.operands ? args[0] : [];
|
|
1860
1579
|
const opts = definition.operands ? args[1] : args[0];
|
|
@@ -1865,21 +1584,18 @@ function addServiceCommand(program, definition, setSelection) {
|
|
|
1865
1584
|
});
|
|
1866
1585
|
}
|
|
1867
1586
|
function buildProgram(setSelection) {
|
|
1868
|
-
const program = new Command(
|
|
1587
|
+
const program = new Command("cella").version(VERSION, "-v, --version", "output the current version").usage("[service] [options]").helpOption("-h, --help", "display help for a command").showHelpAfterError().addHelpText(
|
|
1869
1588
|
"after",
|
|
1870
1589
|
[
|
|
1871
1590
|
"",
|
|
1872
1591
|
"Examples:",
|
|
1873
1592
|
" $ cella analyze",
|
|
1874
1593
|
" $ cella analyze --json --scope risk",
|
|
1875
|
-
" $ cella analyze --open-diff frontend/src/routes/index.tsx",
|
|
1876
|
-
" $ cella sync --hard",
|
|
1877
|
-
" $ cella sync --unpinned",
|
|
1878
1594
|
" $ cella sync --track branch",
|
|
1879
1595
|
" $ cella sync --ref 4f7d87c",
|
|
1880
1596
|
" $ cella migrate --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1881
1597
|
" $ cella migrate --mark 20261002T0614-config-switch",
|
|
1882
|
-
" $ cella audit --
|
|
1598
|
+
" $ cella audit --list",
|
|
1883
1599
|
" $ cella stats --since origin/main --md",
|
|
1884
1600
|
" $ cella contributions --fork raak --json"
|
|
1885
1601
|
].join("\n")
|
|
@@ -1914,274 +1630,110 @@ async function promptForService(userConfig, forkPath) {
|
|
|
1914
1630
|
console.info();
|
|
1915
1631
|
return selected;
|
|
1916
1632
|
}
|
|
1917
|
-
function buildRuntimeConfig(userConfig, forkPath, selection) {
|
|
1918
|
-
|
|
1919
|
-
return {
|
|
1920
|
-
...userConfig,
|
|
1921
|
-
forkPath,
|
|
1922
|
-
upstreamRef: branchRef,
|
|
1923
|
-
service: selection.service ?? "analyze",
|
|
1924
|
-
...selection.options
|
|
1925
|
-
};
|
|
1633
|
+
function buildRuntimeConfig(userConfig, forkPath, selection, upstreamRef) {
|
|
1634
|
+
return { ...userConfig, forkPath, upstreamRef, service: selection.service ?? "analyze", ...selection.options };
|
|
1926
1635
|
}
|
|
1927
1636
|
async function parseCli(userConfig, forkPath) {
|
|
1928
1637
|
const selection = parseCommandLine(process5.argv);
|
|
1929
1638
|
const machineList = selection.options.list && selection.service !== "audit";
|
|
1930
|
-
if (selection.options.json || selection.options.
|
|
1639
|
+
if (selection.options.json || selection.options.md || machineList) setJsonMode(true);
|
|
1931
1640
|
printHeader();
|
|
1932
|
-
const
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
}
|
|
1641
|
+
const { branchRef } = resolveUpstream(userConfig.settings);
|
|
1642
|
+
const warnings = await validateOverrides(userConfig, forkPath, branchRef);
|
|
1643
|
+
for (const warning of warnings) console.info(`${warningMark} ${warning}`);
|
|
1644
|
+
if (warnings.length > 0) console.info();
|
|
1937
1645
|
if (!selection.service) {
|
|
1938
1646
|
selection.service = await promptForService(userConfig, forkPath);
|
|
1939
1647
|
}
|
|
1940
|
-
return buildRuntimeConfig(userConfig, forkPath, selection);
|
|
1648
|
+
return buildRuntimeConfig(userConfig, forkPath, selection, branchRef);
|
|
1941
1649
|
}
|
|
1942
1650
|
|
|
1943
|
-
// src/services/
|
|
1944
|
-
import {
|
|
1651
|
+
// src/services/merge-engine.ts
|
|
1652
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1653
|
+
import { rm as rm2 } from "fs/promises";
|
|
1654
|
+
import { join as join7 } from "path";
|
|
1945
1655
|
|
|
1946
|
-
// src/utils/
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
} catch (error) {
|
|
1959
|
-
const stderr = error.stderr?.toString().trim();
|
|
1960
|
-
throw new Error(stderr || `failed to diff ${filePath}`);
|
|
1961
|
-
}
|
|
1656
|
+
// src/utils/versions.ts
|
|
1657
|
+
function parseComparableVersion(version) {
|
|
1658
|
+
const trimmed = version.trim();
|
|
1659
|
+
const match = trimmed.match(
|
|
1660
|
+
/^(?:\^|~|>=|<=|>|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
1661
|
+
);
|
|
1662
|
+
if (!match) return null;
|
|
1663
|
+
return {
|
|
1664
|
+
major: Number(match[1]),
|
|
1665
|
+
minor: Number(match[2] ?? 0),
|
|
1666
|
+
patch: Number(match[3] ?? 0)
|
|
1667
|
+
};
|
|
1962
1668
|
}
|
|
1963
|
-
function
|
|
1964
|
-
|
|
1669
|
+
function compareVersions(left, right) {
|
|
1670
|
+
if (left.major !== right.major) return left.major - right.major;
|
|
1671
|
+
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
1672
|
+
return left.patch - right.patch;
|
|
1965
1673
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
}
|
|
1674
|
+
function isPrerelease(version) {
|
|
1675
|
+
return /^v?\d+(?:\.\d+){0,2}-/.test(version.trim());
|
|
1676
|
+
}
|
|
1677
|
+
function compareVersionStrings(left, right) {
|
|
1678
|
+
const a = parseComparableVersion(left);
|
|
1679
|
+
const b = parseComparableVersion(right);
|
|
1680
|
+
if (!a || !b) return Number(!!a) - Number(!!b);
|
|
1681
|
+
return compareVersions(a, b) || Number(isPrerelease(right)) - Number(isPrerelease(left));
|
|
1682
|
+
}
|
|
1683
|
+
function satisfiesComparators(version, range) {
|
|
1684
|
+
return range.split("||").some((alternative) => {
|
|
1685
|
+
const comparators = [...alternative.matchAll(/(>=|<=|>|<|=)?\s*v?(\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?)/g)];
|
|
1686
|
+
if (comparators.length === 0) return false;
|
|
1687
|
+
return comparators.every(([, operator = "=", bound]) => {
|
|
1688
|
+
const order = compareVersionStrings(version, bound);
|
|
1689
|
+
if (operator === ">=") return order >= 0;
|
|
1690
|
+
if (operator === "<=") return order <= 0;
|
|
1691
|
+
if (operator === ">") return order > 0;
|
|
1692
|
+
if (operator === "<") return order < 0;
|
|
1693
|
+
return order === 0;
|
|
1694
|
+
});
|
|
1978
1695
|
});
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1696
|
+
}
|
|
1697
|
+
function isBreakingJump(from, to) {
|
|
1698
|
+
const a = parseComparableVersion(from);
|
|
1699
|
+
const b = parseComparableVersion(to);
|
|
1700
|
+
if (!a || !b) return true;
|
|
1701
|
+
if (a.major !== b.major) return true;
|
|
1702
|
+
if (a.major > 0) return false;
|
|
1703
|
+
if (a.minor !== b.minor) return true;
|
|
1704
|
+
return a.minor === 0 && a.patch !== b.patch;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/utils/cli-version.ts
|
|
1708
|
+
var CLI_PACKAGE = "@cellajs/cli";
|
|
1709
|
+
async function readUpstreamCliRange(repoPath, ref) {
|
|
1710
|
+
const source = await git(["show", `${ref}:package.json`], repoPath, { ignoreErrors: true });
|
|
1711
|
+
if (!source) return null;
|
|
1712
|
+
try {
|
|
1713
|
+
const pkg = JSON.parse(source);
|
|
1714
|
+
return pkg.devDependencies?.[CLI_PACKAGE] ?? pkg.dependencies?.[CLI_PACKAGE] ?? null;
|
|
1715
|
+
} catch {
|
|
1716
|
+
return null;
|
|
1995
1717
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
<header>
|
|
2016
|
-
<span class="brand">\u29C8 cella diff</span>
|
|
2017
|
-
<code>${escapeHtml(meta.filePath)}</code>
|
|
2018
|
-
<span class="sides">${escapeHtml(meta.srcLabel)} \u2190 \u2192 ${escapeHtml(meta.dstLabel)} ${note}</span>
|
|
2019
|
-
</header>
|
|
2020
|
-
<main>
|
|
2021
|
-
<!-- Declarative shadow DOM: the library's CSS targets :host, which only
|
|
2022
|
-
matches inside a shadow root (its browser component renders into one). -->
|
|
2023
|
-
<div class="diff-shell">
|
|
2024
|
-
<template shadowrootmode="open">
|
|
2025
|
-
${prerenderedHTML}
|
|
2026
|
-
</template>
|
|
2027
|
-
</div>
|
|
2028
|
-
</main>
|
|
2029
|
-
</body>
|
|
2030
|
-
</html>`;
|
|
2031
|
-
}
|
|
2032
|
-
function openWithDefaultApp(target) {
|
|
2033
|
-
try {
|
|
2034
|
-
if (process6.platform === "win32") {
|
|
2035
|
-
execFileSync("rundll32", ["url.dll,FileProtocolHandler", target], { stdio: "ignore" });
|
|
2036
|
-
} else {
|
|
2037
|
-
execFileSync(process6.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
2038
|
-
}
|
|
2039
|
-
} catch {
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
async function openDiffInBrowser(patch, meta, repoPath) {
|
|
2043
|
-
const html = await renderDiffPage(patch, meta);
|
|
2044
|
-
const dir = join8(tmpdir(), `cella-diff-${basename2(repoPath)}`);
|
|
2045
|
-
mkdirSync(dir, { recursive: true });
|
|
2046
|
-
const outPath = join8(dir, `${meta.filePath.replace(/[^\w.-]+/g, "-")}.html`);
|
|
2047
|
-
writeFileSync2(outPath, html);
|
|
2048
|
-
openWithDefaultApp(outPath);
|
|
2049
|
-
return outPath;
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
// src/services/merge-engine.ts
|
|
2053
|
-
import { existsSync as existsSync7 } from "fs";
|
|
2054
|
-
import { rm as rm2 } from "fs/promises";
|
|
2055
|
-
import { join as join10 } from "path";
|
|
2056
|
-
|
|
2057
|
-
// src/utils/cleanup.ts
|
|
2058
|
-
import { createHash } from "crypto";
|
|
2059
|
-
import { existsSync as existsSync6, rmSync } from "fs";
|
|
2060
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
2061
|
-
import { basename as basename3, join as join9, resolve as resolve4 } from "path";
|
|
2062
|
-
import process7 from "process";
|
|
2063
|
-
var WORKTREE_PREFIXES = {
|
|
2064
|
-
sync: "cella-sync-",
|
|
2065
|
-
view: "cella-view-"
|
|
2066
|
-
};
|
|
2067
|
-
function buildWorktreePath(kind, repoPath) {
|
|
2068
|
-
const key = createHash("sha256").update(resolve4(repoPath)).digest("hex").slice(0, 8);
|
|
2069
|
-
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}-${key}`);
|
|
2070
|
-
}
|
|
2071
|
-
function legacyWorktreePath(kind, repoPath) {
|
|
2072
|
-
return join9(tmpdir2(), `${WORKTREE_PREFIXES[kind]}${basename3(repoPath)}`);
|
|
2073
|
-
}
|
|
2074
|
-
function getWorktreePath(repoPath) {
|
|
2075
|
-
return buildWorktreePath("sync", repoPath);
|
|
2076
|
-
}
|
|
2077
|
-
var cleanupRegistered = false;
|
|
2078
|
-
var currentWorktreePath = null;
|
|
2079
|
-
var currentRepoPath = null;
|
|
2080
|
-
function registerWorktree(repoPath, worktreePath) {
|
|
2081
|
-
currentRepoPath = repoPath;
|
|
2082
|
-
currentWorktreePath = worktreePath;
|
|
2083
|
-
}
|
|
2084
|
-
function unregisterWorktree() {
|
|
2085
|
-
currentRepoPath = null;
|
|
2086
|
-
currentWorktreePath = null;
|
|
2087
|
-
}
|
|
2088
|
-
async function cleanupWorktree(repoPath, worktreePath) {
|
|
2089
|
-
await removeWorktree(repoPath, worktreePath);
|
|
2090
|
-
if (existsSync6(worktreePath)) {
|
|
2091
|
-
rmSync(worktreePath, { recursive: true, force: true });
|
|
2092
|
-
}
|
|
2093
|
-
unregisterWorktree();
|
|
2094
|
-
}
|
|
2095
|
-
async function cleanupLeftoverWorktrees(repoPath) {
|
|
2096
|
-
for (const kind of Object.keys(WORKTREE_PREFIXES)) {
|
|
2097
|
-
for (const worktreePath of [buildWorktreePath(kind, repoPath), legacyWorktreePath(kind, repoPath)]) {
|
|
2098
|
-
if (existsSync6(worktreePath)) await cleanupWorktree(repoPath, worktreePath);
|
|
2099
|
-
}
|
|
2100
|
-
}
|
|
2101
|
-
const prefixes = Object.values(WORKTREE_PREFIXES);
|
|
2102
|
-
const worktrees = await listWorktrees(repoPath);
|
|
2103
|
-
for (const wt of worktrees) {
|
|
2104
|
-
if (prefixes.some((prefix) => wt.includes(prefix)) && !existsSync6(wt)) {
|
|
2105
|
-
await removeWorktree(repoPath, wt);
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
}
|
|
2109
|
-
async function handleAbort(signal) {
|
|
2110
|
-
console.info();
|
|
2111
|
-
console.info(`${warningMark} interrupted (${signal}): cleaning up...`);
|
|
2112
|
-
if (currentRepoPath && currentWorktreePath) {
|
|
2113
|
-
try {
|
|
2114
|
-
await mergeAbort(currentWorktreePath);
|
|
2115
|
-
} catch {
|
|
2116
|
-
}
|
|
2117
|
-
try {
|
|
2118
|
-
await cleanupWorktree(currentRepoPath, currentWorktreePath);
|
|
2119
|
-
console.info(`${checkMark} no changes were made to your repository.`);
|
|
2120
|
-
} catch (error) {
|
|
2121
|
-
console.error(`${colors_default.red("\u2717")} failed to clean up worktree: ${errorMessage(error)}`);
|
|
2122
|
-
}
|
|
2123
|
-
}
|
|
2124
|
-
process7.exit(1);
|
|
2125
|
-
}
|
|
2126
|
-
function registerSignalHandlers() {
|
|
2127
|
-
if (cleanupRegistered) return;
|
|
2128
|
-
process7.on("SIGINT", () => handleAbort("SIGINT"));
|
|
2129
|
-
process7.on("SIGTERM", () => handleAbort("SIGTERM"));
|
|
2130
|
-
cleanupRegistered = true;
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
// src/utils/versions.ts
|
|
2134
|
-
function parseComparableVersion(version) {
|
|
2135
|
-
const trimmed = version.trim();
|
|
2136
|
-
if (trimmed === "" || trimmed === "*" || trimmed.includes("workspace:") || trimmed.includes("catalog:") || trimmed.includes("file:") || trimmed.includes("link:") || trimmed.includes("git+") || trimmed.includes("github:") || trimmed.includes("http://") || trimmed.includes("https://") || trimmed.includes("||")) {
|
|
2137
|
-
return null;
|
|
2138
|
-
}
|
|
2139
|
-
const match = trimmed.match(
|
|
2140
|
-
/^(?:\^|~|>=|<=|>|<|=)?\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
2141
|
-
);
|
|
2142
|
-
if (!match) return null;
|
|
2143
|
-
return {
|
|
2144
|
-
major: Number(match[1]),
|
|
2145
|
-
minor: Number(match[2] ?? 0),
|
|
2146
|
-
patch: Number(match[3] ?? 0)
|
|
2147
|
-
};
|
|
2148
|
-
}
|
|
2149
|
-
function compareVersions(left, right) {
|
|
2150
|
-
if (left.major !== right.major) return left.major - right.major;
|
|
2151
|
-
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
2152
|
-
return left.patch - right.patch;
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// src/utils/cli-version.ts
|
|
2156
|
-
var CLI_PACKAGE = "@cellajs/cli";
|
|
2157
|
-
async function readUpstreamCliRange(repoPath, ref) {
|
|
2158
|
-
const source = await git(["show", `${ref}:package.json`], repoPath, { ignoreErrors: true });
|
|
2159
|
-
if (!source) return null;
|
|
2160
|
-
try {
|
|
2161
|
-
const pkg = JSON.parse(source);
|
|
2162
|
-
return pkg.devDependencies?.[CLI_PACKAGE] ?? pkg.dependencies?.[CLI_PACKAGE] ?? null;
|
|
2163
|
-
} catch {
|
|
2164
|
-
return null;
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
function cliVersionMismatch(running, range) {
|
|
2168
|
-
if (/^\s*(<|>(?!=))/.test(range)) return null;
|
|
2169
|
-
const required = parseComparableVersion(range);
|
|
2170
|
-
const current = parseComparableVersion(running);
|
|
2171
|
-
if (!required || !current || compareVersions(current, required) >= 0) return null;
|
|
2172
|
-
return [
|
|
2173
|
-
`upstream needs ${CLI_PACKAGE} ${range}, this is ${running}: an older CLI misses fixes to the sync itself.`,
|
|
2174
|
-
` update it, commit, then rerun: pnpm add -D -w '${CLI_PACKAGE}@${range}'`
|
|
2175
|
-
].join("\n");
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
// src/utils/upstream-overrides.ts
|
|
2179
|
-
import { Node, Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
2180
|
-
var LIST_KEYS = ["pinned", "ignored"];
|
|
2181
|
-
function resolveExpression(node, sourceFile, depth = 0) {
|
|
2182
|
-
if (!node || depth > 8) return node;
|
|
2183
|
-
if (Node.isParenthesizedExpression(node) || Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) {
|
|
2184
|
-
return resolveExpression(node.getExpression(), sourceFile, depth + 1);
|
|
1718
|
+
}
|
|
1719
|
+
function cliVersionMismatch(running, range) {
|
|
1720
|
+
if (/^\s*(<|>(?!=))/.test(range)) return null;
|
|
1721
|
+
const required = parseComparableVersion(range);
|
|
1722
|
+
const current = parseComparableVersion(running);
|
|
1723
|
+
if (!required || !current || compareVersions(current, required) >= 0) return null;
|
|
1724
|
+
return [
|
|
1725
|
+
`upstream needs ${CLI_PACKAGE} ${range}, this is ${running}: an older CLI misses fixes to the sync itself.`,
|
|
1726
|
+
` update it, commit, then rerun: pnpm add -D -w '${CLI_PACKAGE}@${range}'`
|
|
1727
|
+
].join("\n");
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/utils/upstream-overrides.ts
|
|
1731
|
+
import { Node, Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1732
|
+
var LIST_KEYS = ["pinned", "ignored"];
|
|
1733
|
+
function resolveExpression(node, sourceFile, depth = 0) {
|
|
1734
|
+
if (!node || depth > 8) return node;
|
|
1735
|
+
if (Node.isParenthesizedExpression(node) || Node.isAsExpression(node) || Node.isSatisfiesExpression(node)) {
|
|
1736
|
+
return resolveExpression(node.getExpression(), sourceFile, depth + 1);
|
|
2185
1737
|
}
|
|
2186
1738
|
if (Node.isIdentifier(node)) {
|
|
2187
1739
|
return resolveExpression(
|
|
@@ -2258,9 +1810,6 @@ function parseOverrideLists(source) {
|
|
|
2258
1810
|
return null;
|
|
2259
1811
|
}
|
|
2260
1812
|
}
|
|
2261
|
-
function normalizeEntry(entry) {
|
|
2262
|
-
return entry.trim().replace(/^\.\//, "").replace(/\/+$/, "");
|
|
2263
|
-
}
|
|
2264
1813
|
function diffOverrideLists(base, incoming, fork) {
|
|
2265
1814
|
const diff = (key) => {
|
|
2266
1815
|
const baseSet = new Set(base[key].map(normalizeEntry).filter(Boolean));
|
|
@@ -2281,22 +1830,14 @@ function diffOverrideLists(base, incoming, fork) {
|
|
|
2281
1830
|
};
|
|
2282
1831
|
return { pinned: diff("pinned"), ignored: diff("ignored"), packageJsonSync: diffKeys() };
|
|
2283
1832
|
}
|
|
2284
|
-
async function readConfigSourceAtRef(repoPath, ref) {
|
|
2285
|
-
for (const path of CONFIG_FILE_PATHS) {
|
|
2286
|
-
const source = await git(["show", `${ref}:${path}`], repoPath, { ignoreErrors: true });
|
|
2287
|
-
if (source) return source;
|
|
2288
|
-
}
|
|
2289
|
-
return null;
|
|
2290
|
-
}
|
|
2291
1833
|
async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, forkConfig) {
|
|
2292
|
-
const [baseSource, incomingSource] = await Promise.all(
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
]);
|
|
1834
|
+
const [baseSource, incomingSource] = await Promise.all(
|
|
1835
|
+
[mergeBaseRef, incomingRef].map((ref) => git(["show", `${ref}:${CONFIG_FILE}`], repoPath, { ignoreErrors: true }))
|
|
1836
|
+
);
|
|
2296
1837
|
if (baseSource === incomingSource) return void 0;
|
|
2297
|
-
const base = baseSource
|
|
1838
|
+
const base = baseSource ? parseOverrideLists(baseSource) : null;
|
|
2298
1839
|
if (!base) return { kind: "unreadable", side: "base" };
|
|
2299
|
-
const incoming = incomingSource
|
|
1840
|
+
const incoming = incomingSource ? parseOverrideLists(incomingSource) : null;
|
|
2300
1841
|
if (!incoming) return { kind: "unreadable", side: "incoming" };
|
|
2301
1842
|
const fork = {
|
|
2302
1843
|
pinned: forkConfig.overrides?.pinned ?? [],
|
|
@@ -2304,8 +1845,8 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
|
|
|
2304
1845
|
packageJsonSync: forkConfig.settings.packageJsonSync ?? DEFAULT_PACKAGE_JSON_SYNC
|
|
2305
1846
|
};
|
|
2306
1847
|
const changes = diffOverrideLists(base, incoming, fork);
|
|
2307
|
-
const
|
|
2308
|
-
return
|
|
1848
|
+
const count2 = Object.values(changes).reduce((n, list) => n + list.added.length + list.removed.length, 0);
|
|
1849
|
+
return count2 > 0 ? { kind: "changes", ...changes } : void 0;
|
|
2309
1850
|
}
|
|
2310
1851
|
|
|
2311
1852
|
// src/services/analyze-core.ts
|
|
@@ -2346,15 +1887,17 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2346
1887
|
const upstreamChange = upstreamChanges.get(filePath);
|
|
2347
1888
|
const isUpstreamRename = upstreamChange?.status === "R" && upstreamChange.oldPath;
|
|
2348
1889
|
const renamedToPath = upstreamRenames.get(filePath);
|
|
1890
|
+
const push = (status2, extra = {}) => analyzedFiles.push({
|
|
1891
|
+
path: filePath,
|
|
1892
|
+
status: status2,
|
|
1893
|
+
isIgnored: fileIsIgnored,
|
|
1894
|
+
isPinned: fileIsPinned,
|
|
1895
|
+
existsInFork: inFork,
|
|
1896
|
+
existsInUpstream: inUpstream,
|
|
1897
|
+
...extra
|
|
1898
|
+
});
|
|
2349
1899
|
if (!changedFiles.has(filePath) && inFork && inUpstream) {
|
|
2350
|
-
|
|
2351
|
-
path: filePath,
|
|
2352
|
-
status: "identical",
|
|
2353
|
-
isIgnored: fileIsIgnored,
|
|
2354
|
-
isPinned: fileIsPinned,
|
|
2355
|
-
existsInFork: true,
|
|
2356
|
-
existsInUpstream: true
|
|
2357
|
-
});
|
|
1900
|
+
push("identical");
|
|
2358
1901
|
continue;
|
|
2359
1902
|
}
|
|
2360
1903
|
processed++;
|
|
@@ -2391,17 +1934,7 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2391
1934
|
renamedFrom = oldPath;
|
|
2392
1935
|
}
|
|
2393
1936
|
const upstreamOnly2 = fileIsIgnored && !forkModifiedOld && isReportableIgnored(filePath);
|
|
2394
|
-
|
|
2395
|
-
path: filePath,
|
|
2396
|
-
status,
|
|
2397
|
-
isIgnored: fileIsIgnored,
|
|
2398
|
-
isPinned: fileIsPinned,
|
|
2399
|
-
existsInFork: inFork,
|
|
2400
|
-
existsInUpstream: true,
|
|
2401
|
-
renamedFrom,
|
|
2402
|
-
upstreamChanged: upstreamChanged2,
|
|
2403
|
-
upstreamOnly: upstreamOnly2 || void 0
|
|
2404
|
-
});
|
|
1937
|
+
push(status, { existsInUpstream: true, renamedFrom, upstreamChanged: upstreamChanged2, upstreamOnly: upstreamOnly2 || void 0 });
|
|
2405
1938
|
continue;
|
|
2406
1939
|
}
|
|
2407
1940
|
if (renamedToPath) {
|
|
@@ -2427,23 +1960,14 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2427
1960
|
if (forkChanged && upstreamChanged2) {
|
|
2428
1961
|
status = fileIsPinned ? "pinned" : "diverged";
|
|
2429
1962
|
} else if (forkChanged && !upstreamChanged2) {
|
|
2430
|
-
status = fileIsPinned ? "ahead" :
|
|
1963
|
+
status = fileIsPinned ? "ahead" : "drifted";
|
|
2431
1964
|
} else {
|
|
2432
1965
|
status = "behind";
|
|
2433
1966
|
}
|
|
2434
1967
|
}
|
|
2435
1968
|
const upstreamChanged = (fileIsPinned || fileIsIgnored) && inUpstream && upstreamHash !== baseHash && forkHash !== baseHash;
|
|
2436
1969
|
const upstreamOnly = fileIsIgnored && isReportableIgnored(filePath) && upstreamHash !== baseHash && forkHash === baseHash;
|
|
2437
|
-
|
|
2438
|
-
path: filePath,
|
|
2439
|
-
status,
|
|
2440
|
-
isIgnored: fileIsIgnored,
|
|
2441
|
-
isPinned: fileIsPinned,
|
|
2442
|
-
existsInFork: inFork,
|
|
2443
|
-
existsInUpstream: inUpstream,
|
|
2444
|
-
upstreamChanged: upstreamChanged || void 0,
|
|
2445
|
-
upstreamOnly: upstreamOnly || void 0
|
|
2446
|
-
});
|
|
1970
|
+
push(status, { upstreamChanged: upstreamChanged || void 0, upstreamOnly: upstreamOnly || void 0 });
|
|
2447
1971
|
}
|
|
2448
1972
|
const flagged = analyzedFiles.filter((file) => file.upstreamChanged && !renamedProtected.has(file.path));
|
|
2449
1973
|
if (flagged.length > 0) {
|
|
@@ -2480,34 +2004,25 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
2480
2004
|
async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomingRef) {
|
|
2481
2005
|
const forkInfo = await getFileChangeInfo(repoPath, mergeBaseRef, localRef);
|
|
2482
2006
|
const upstreamInfo = await getFileChangeInfo(repoPath, mergeBaseRef, incomingRef);
|
|
2007
|
+
const setChanged = (file, info) => {
|
|
2008
|
+
if (!info) return;
|
|
2009
|
+
file.changedAt = info.date;
|
|
2010
|
+
file.changedTs = info.timestamp;
|
|
2011
|
+
file.changedCommit = info.hash;
|
|
2012
|
+
};
|
|
2483
2013
|
for (const file of files) {
|
|
2484
2014
|
if (file.status === "identical") continue;
|
|
2485
2015
|
if (file.status === "ahead" || file.status === "drifted") {
|
|
2486
|
-
|
|
2487
|
-
if (info) {
|
|
2488
|
-
file.changedAt = info.date;
|
|
2489
|
-
file.changedTs = info.timestamp;
|
|
2490
|
-
file.changedCommit = info.hash;
|
|
2491
|
-
}
|
|
2016
|
+
setChanged(file, forkInfo.get(file.path));
|
|
2492
2017
|
} else if (file.status === "behind") {
|
|
2018
|
+
setChanged(file, upstreamInfo.get(file.path));
|
|
2019
|
+
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
2020
|
+
setChanged(file, forkInfo.get(file.path));
|
|
2493
2021
|
const info = upstreamInfo.get(file.path);
|
|
2494
2022
|
if (info) {
|
|
2495
|
-
file.
|
|
2496
|
-
file.
|
|
2497
|
-
file.
|
|
2498
|
-
}
|
|
2499
|
-
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
2500
|
-
const forkFileInfo = forkInfo.get(file.path);
|
|
2501
|
-
const upstreamFileInfo = upstreamInfo.get(file.path);
|
|
2502
|
-
if (forkFileInfo) {
|
|
2503
|
-
file.changedAt = forkFileInfo.date;
|
|
2504
|
-
file.changedTs = forkFileInfo.timestamp;
|
|
2505
|
-
file.changedCommit = forkFileInfo.hash;
|
|
2506
|
-
}
|
|
2507
|
-
if (upstreamFileInfo) {
|
|
2508
|
-
file.upstreamChangedAt = upstreamFileInfo.date;
|
|
2509
|
-
file.upstreamChangedTs = upstreamFileInfo.timestamp;
|
|
2510
|
-
file.upstreamCommit = upstreamFileInfo.hash;
|
|
2023
|
+
file.upstreamChangedAt = info.date;
|
|
2024
|
+
file.upstreamChangedTs = info.timestamp;
|
|
2025
|
+
file.upstreamCommit = info.hash;
|
|
2511
2026
|
}
|
|
2512
2027
|
}
|
|
2513
2028
|
}
|
|
@@ -2530,15 +2045,8 @@ var UpstreamConfigChangedError = class extends Error {
|
|
|
2530
2045
|
}
|
|
2531
2046
|
};
|
|
2532
2047
|
function getGitHubBaseUrl(remoteUrl) {
|
|
2533
|
-
const
|
|
2534
|
-
|
|
2535
|
-
return `https://github.com/${sshMatch[1]}/${sshMatch[2]}`;
|
|
2536
|
-
}
|
|
2537
|
-
const httpsMatch = remoteUrl.match(/https:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?$/);
|
|
2538
|
-
if (httpsMatch) {
|
|
2539
|
-
return `https://github.com/${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
2540
|
-
}
|
|
2541
|
-
return null;
|
|
2048
|
+
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
2049
|
+
return match ? `https://github.com/${match[1]}/${match[2]}` : null;
|
|
2542
2050
|
}
|
|
2543
2051
|
function isProtectedFile(file) {
|
|
2544
2052
|
return file.isIgnored || file.isPinned;
|
|
@@ -2546,8 +2054,7 @@ function isProtectedFile(file) {
|
|
|
2546
2054
|
function syncPredicates(config) {
|
|
2547
2055
|
return {
|
|
2548
2056
|
isIgnored: (path) => isIgnored(path, config),
|
|
2549
|
-
isPinned: (path) =>
|
|
2550
|
-
hard: config.hard
|
|
2057
|
+
isPinned: (path) => config.unpinned ? isManagedFile(path) : isPinned(path, config)
|
|
2551
2058
|
};
|
|
2552
2059
|
}
|
|
2553
2060
|
async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onProgress) {
|
|
@@ -2563,38 +2070,27 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2563
2070
|
const batchRestorePaths = [];
|
|
2564
2071
|
const batchRemovePaths = [];
|
|
2565
2072
|
for (const file of analyzedFiles) {
|
|
2566
|
-
|
|
2567
|
-
if (
|
|
2568
|
-
|
|
2569
|
-
if (ignored && existsInFork) {
|
|
2570
|
-
batchRestorePaths.push(filePath);
|
|
2571
|
-
} else if (ignored && !existsInFork) {
|
|
2572
|
-
batchRemovePaths.push(filePath);
|
|
2573
|
-
} else if (pinned && !file.renamedFrom && existsInFork) {
|
|
2574
|
-
batchRestorePaths.push(filePath);
|
|
2575
|
-
} else if (pinned && !file.renamedFrom && !existsInFork) {
|
|
2576
|
-
batchRemovePaths.push(filePath);
|
|
2073
|
+
if (file.status === "identical" || file.status === "ahead") continue;
|
|
2074
|
+
if (file.isIgnored || file.isPinned && !file.renamedFrom) {
|
|
2075
|
+
(file.existsInFork ? batchRestorePaths : batchRemovePaths).push(file.path);
|
|
2577
2076
|
}
|
|
2578
2077
|
}
|
|
2579
2078
|
onProgress?.("starting merge in fork...");
|
|
2580
2079
|
await withTemporarySyncBaseGraft(forkPath, "HEAD", mergeBaseRef, () => merge(forkPath, upstreamRef));
|
|
2581
2080
|
if (batchRestorePaths.length > 0) {
|
|
2582
2081
|
onProgress?.(`batch restoring ${batchRestorePaths.length} pinned/ignored files...`);
|
|
2583
|
-
await
|
|
2082
|
+
await checkoutFromRef(forkPath, "HEAD", batchRestorePaths);
|
|
2584
2083
|
}
|
|
2585
2084
|
if (batchRemovePaths.length > 0) {
|
|
2586
2085
|
onProgress?.(`batch removing ${batchRemovePaths.length} ignored files...`);
|
|
2587
|
-
await
|
|
2086
|
+
await gitRm(forkPath, batchRemovePaths);
|
|
2588
2087
|
for (const filePath of batchRemovePaths) {
|
|
2589
2088
|
await removeFileFromWorktree(forkPath, filePath);
|
|
2590
2089
|
}
|
|
2591
2090
|
}
|
|
2592
2091
|
for (const file of analyzedFiles) {
|
|
2593
2092
|
const { path: filePath, status, existsInFork } = file;
|
|
2594
|
-
if (status === "identical" || status === "ahead")
|
|
2595
|
-
continue;
|
|
2596
|
-
}
|
|
2597
|
-
if (isProtectedFile(file)) continue;
|
|
2093
|
+
if (status === "identical" || status === "ahead" || isProtectedFile(file)) continue;
|
|
2598
2094
|
if (status === "diverged") {
|
|
2599
2095
|
onProgress?.(`\u2192 ${filePath}: using git merge result (diverged)`);
|
|
2600
2096
|
continue;
|
|
@@ -2602,13 +2098,13 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2602
2098
|
if (status === "behind") {
|
|
2603
2099
|
if (!existsInFork) {
|
|
2604
2100
|
onProgress?.(`\u2192 ${filePath}: adding from upstream (new file)`);
|
|
2605
|
-
await checkoutFromRef(forkPath, upstreamRef, filePath);
|
|
2101
|
+
await checkoutFromRef(forkPath, upstreamRef, [filePath]);
|
|
2606
2102
|
} else if (!file.existsInUpstream) {
|
|
2607
2103
|
onProgress?.(`\u2192 ${filePath}: removing (deleted in upstream)`);
|
|
2608
2104
|
await removeFileFully(forkPath, filePath);
|
|
2609
2105
|
} else {
|
|
2610
2106
|
onProgress?.(`\u2192 ${filePath}: accepting upstream (behind)`);
|
|
2611
|
-
await checkoutFromRef(forkPath, upstreamRef, filePath);
|
|
2107
|
+
await checkoutFromRef(forkPath, upstreamRef, [filePath]);
|
|
2612
2108
|
}
|
|
2613
2109
|
continue;
|
|
2614
2110
|
}
|
|
@@ -2626,31 +2122,25 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2626
2122
|
await gitMv(forkPath, oldPath, filePath);
|
|
2627
2123
|
} catch {
|
|
2628
2124
|
await removeFileFully(forkPath, oldPath);
|
|
2629
|
-
await checkoutFromRef(forkPath, upstreamRef, filePath);
|
|
2125
|
+
await checkoutFromRef(forkPath, upstreamRef, [filePath]);
|
|
2630
2126
|
}
|
|
2631
2127
|
} else if (oldPathExists && newPathExists) {
|
|
2632
2128
|
onProgress?.(`\u2192 ${oldPath} \u2192 ${filePath}: completing rename (removing old)`);
|
|
2633
2129
|
await removeFileFully(forkPath, oldPath);
|
|
2634
|
-
await checkoutFromRef(forkPath, upstreamRef, filePath);
|
|
2130
|
+
await checkoutFromRef(forkPath, upstreamRef, [filePath]);
|
|
2635
2131
|
} else {
|
|
2636
2132
|
onProgress?.(`\u2192 ${filePath}: updating from upstream (renamed)`);
|
|
2637
|
-
await checkoutFromRef(forkPath, upstreamRef, filePath);
|
|
2133
|
+
await checkoutFromRef(forkPath, upstreamRef, [filePath]);
|
|
2638
2134
|
}
|
|
2639
2135
|
}
|
|
2640
2136
|
}
|
|
2641
|
-
const protectedConflicts = analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path);
|
|
2642
|
-
const
|
|
2643
|
-
|
|
2644
|
-
for (const filePath of gitConflicts) {
|
|
2645
|
-
const isProtected = isIgnored(filePath, config) || isPinnedForSync(filePath, config, config.unpinned);
|
|
2646
|
-
if (!isProtected) continue;
|
|
2137
|
+
const protectedConflicts = new Set(analyzedFiles.filter((file) => file.upstreamChanged).map((file) => file.path));
|
|
2138
|
+
for (const filePath of await getConflictedFiles(forkPath)) {
|
|
2139
|
+
if (!isIgnored(filePath, config) && !syncPredicates(config).isPinned(filePath)) continue;
|
|
2647
2140
|
if (await fileExistsAtRef(forkPath, "HEAD", filePath)) {
|
|
2648
2141
|
onProgress?.(`\u2192 ${filePath}: keeping fork (protected conflict)`);
|
|
2649
|
-
await
|
|
2650
|
-
|
|
2651
|
-
protectedConflictSet.add(filePath);
|
|
2652
|
-
protectedConflicts.push(filePath);
|
|
2653
|
-
}
|
|
2142
|
+
await checkoutFromRef(forkPath, "HEAD", [filePath]);
|
|
2143
|
+
protectedConflicts.add(filePath);
|
|
2654
2144
|
} else {
|
|
2655
2145
|
onProgress?.(`\u2192 ${filePath}: removing (protected conflict, not in fork)`);
|
|
2656
2146
|
await removeFileFully(forkPath, filePath);
|
|
@@ -2663,12 +2153,12 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
|
|
|
2663
2153
|
const staleIgnoredFiles = stagedNewFiles.filter((f) => isIgnored(f, config));
|
|
2664
2154
|
if (staleIgnoredFiles.length > 0) {
|
|
2665
2155
|
onProgress?.(`cleaning up ${staleIgnoredFiles.length} ignored files still in index...`);
|
|
2666
|
-
await
|
|
2156
|
+
await gitRm(forkPath, staleIgnoredFiles, { cached: true });
|
|
2667
2157
|
for (const filePath of staleIgnoredFiles) {
|
|
2668
2158
|
await removeFileFromWorktree(forkPath, filePath);
|
|
2669
2159
|
}
|
|
2670
2160
|
}
|
|
2671
|
-
return { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts };
|
|
2161
|
+
return { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts: [...protectedConflicts] };
|
|
2672
2162
|
}
|
|
2673
2163
|
function calculateSummary(files) {
|
|
2674
2164
|
const summary = {
|
|
@@ -2756,14 +2246,13 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2756
2246
|
onProgress?.("checking sync base...");
|
|
2757
2247
|
await ensureSyncBase(forkPath, "HEAD", upstreamRef);
|
|
2758
2248
|
const upstreamGitHubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2759
|
-
const
|
|
2760
|
-
const
|
|
2761
|
-
const mergeBase = aggressive ? await getMergeBase(forkPath, "HEAD", upstreamRef) : syncPoint;
|
|
2249
|
+
const mergeBase = (await getEffectiveMergeBase(forkPath, "HEAD", upstreamRef)).base;
|
|
2250
|
+
const mergeRef = config.unpinned ? await getMergeBase(forkPath, "HEAD", upstreamRef) : mergeBase;
|
|
2762
2251
|
const upstreamCommit = await getCommitInfo(forkPath, upstreamRef);
|
|
2763
|
-
if (upstreamCommit.hash !==
|
|
2252
|
+
if (upstreamCommit.hash !== mergeBase && await isAncestor(forkPath, upstreamCommit.hash, mergeBase)) {
|
|
2764
2253
|
const [upstreamShort, syncShort] = await Promise.all([
|
|
2765
2254
|
getShortSha(forkPath, upstreamCommit.hash),
|
|
2766
|
-
getShortSha(forkPath,
|
|
2255
|
+
getShortSha(forkPath, mergeBase)
|
|
2767
2256
|
]);
|
|
2768
2257
|
const hint = config.ref ? "Pick a newer --ref." : releaseTag ? `Nothing to sync until a release past ${syncShort}; to follow the tip instead, run with --track branch.` : `Nothing to sync until '${branchRef}' moves past ${syncShort}.`;
|
|
2769
2258
|
throw new BehindSyncPointError(
|
|
@@ -2772,14 +2261,13 @@ ${hint}`
|
|
|
2772
2261
|
);
|
|
2773
2262
|
}
|
|
2774
2263
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2775
|
-
const
|
|
2776
|
-
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, { skip: commitSkip, limit: COMMIT_LIST_MAX }) : [];
|
|
2264
|
+
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, COMMIT_LIST_MAX) : [];
|
|
2777
2265
|
onStep?.("fetched upstream", formatFetchedUpstreamDetail(commitCount, upstreamCommits, upstreamGitHubUrl));
|
|
2778
|
-
return { upstreamRef, releaseTag, mergeBase,
|
|
2266
|
+
return { upstreamRef, releaseTag, mergeBase, mergeRef, upstreamGitHubUrl, upstreamCommit };
|
|
2779
2267
|
}
|
|
2780
2268
|
async function assertSyncConfigFollowed(config, ctx) {
|
|
2781
2269
|
if (config.keepConfig) return;
|
|
2782
|
-
const report = await compareUpstreamOverrides(config.forkPath, ctx.
|
|
2270
|
+
const report = await compareUpstreamOverrides(config.forkPath, ctx.mergeBase, ctx.upstreamRef, config).catch(
|
|
2783
2271
|
() => void 0
|
|
2784
2272
|
);
|
|
2785
2273
|
if (report?.kind === "changes") throw new UpstreamConfigChangedError(report);
|
|
@@ -2790,8 +2278,7 @@ function resultMeta(config, ctx) {
|
|
|
2790
2278
|
upstreamRef: ctx.upstreamRef,
|
|
2791
2279
|
upstreamTag: ctx.releaseTag,
|
|
2792
2280
|
upstreamGitHubUrl: ctx.upstreamGitHubUrl,
|
|
2793
|
-
upstreamCommit: ctx.upstreamCommit
|
|
2794
|
-
upstreamCommits: ctx.upstreamCommits
|
|
2281
|
+
upstreamCommit: ctx.upstreamCommit
|
|
2795
2282
|
};
|
|
2796
2283
|
}
|
|
2797
2284
|
async function upstreamOnlyReports(config, ctx, files, protectedConflicts) {
|
|
@@ -2818,8 +2305,8 @@ async function readNotesInRange(config, ctx) {
|
|
|
2818
2305
|
try {
|
|
2819
2306
|
const [total, arrived, pending] = await Promise.all([
|
|
2820
2307
|
listNoteIds(forkPath, ctx.upstreamRef),
|
|
2821
|
-
arrivedNoteIds(forkPath, ctx.
|
|
2822
|
-
readPending(forkPath
|
|
2308
|
+
arrivedNoteIds(forkPath, ctx.mergeBase, ctx.upstreamRef),
|
|
2309
|
+
readPending(forkPath)
|
|
2823
2310
|
]);
|
|
2824
2311
|
return { total: total.length, arrived, pending: [.../* @__PURE__ */ new Set([...pending, ...arrived])] };
|
|
2825
2312
|
} catch {
|
|
@@ -2830,20 +2317,20 @@ function notesResult(notes) {
|
|
|
2830
2317
|
return notes ? { migrationNotes: { total: notes.total, arrived: notes.arrived, open: notes.pending.length } } : {};
|
|
2831
2318
|
}
|
|
2832
2319
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2833
|
-
if (!
|
|
2320
|
+
if (!existsSync6(join7(forkPath, NOTES_DIR))) return;
|
|
2834
2321
|
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", NOTES_DIR], forkPath);
|
|
2835
|
-
await rm2(
|
|
2322
|
+
await rm2(join7(forkPath, NOTES_DIR), { recursive: true, force: true });
|
|
2836
2323
|
onStep?.("migration notes", `removed ${NOTES_DIR}/: notes stay upstream, list them with pnpm cella migrate`);
|
|
2837
2324
|
}
|
|
2838
2325
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2839
2326
|
const { forkPath } = config;
|
|
2840
|
-
const { upstreamRef, releaseTag,
|
|
2327
|
+
const { upstreamRef, releaseTag, mergeRef, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2841
2328
|
await assertSyncConfigFollowed(config, ctx);
|
|
2842
2329
|
const notes = await readNotesInRange(config, ctx);
|
|
2843
2330
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2844
2331
|
forkPath,
|
|
2845
2332
|
upstreamRef,
|
|
2846
|
-
|
|
2333
|
+
mergeRef,
|
|
2847
2334
|
config,
|
|
2848
2335
|
onProgress
|
|
2849
2336
|
);
|
|
@@ -2898,20 +2385,13 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2898
2385
|
}
|
|
2899
2386
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
2900
2387
|
const { forkPath } = config;
|
|
2901
|
-
const worktreePath = getWorktreePath(forkPath);
|
|
2902
2388
|
const notes = await readNotesInRange(config, ctx);
|
|
2903
|
-
onProgress?.("creating worktree in temp directory...");
|
|
2904
|
-
await createWorktree(forkPath, worktreePath, "HEAD");
|
|
2905
|
-
onStep?.("worktree created", worktreePath);
|
|
2906
|
-
onProgress?.("performing merge in worktree...");
|
|
2907
|
-
await withTemporarySyncBaseGraft(worktreePath, "HEAD", ctx.mergeBase, () => merge(worktreePath, ctx.upstreamRef));
|
|
2908
|
-
onStep?.("merge complete", "upstream merged into worktree");
|
|
2909
2389
|
onProgress?.("analyzing files...");
|
|
2910
2390
|
const analyzedFiles = await analyzeRefs(
|
|
2911
2391
|
forkPath,
|
|
2912
2392
|
"HEAD",
|
|
2913
2393
|
ctx.upstreamRef,
|
|
2914
|
-
ctx.
|
|
2394
|
+
ctx.mergeRef,
|
|
2915
2395
|
syncPredicates(config),
|
|
2916
2396
|
onProgress
|
|
2917
2397
|
);
|
|
@@ -2919,8 +2399,6 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2919
2399
|
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2920
2400
|
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles, protectedConflicts);
|
|
2921
2401
|
onStep?.("analysis complete", `${analyzedFiles.length} files analyzed, dry run, no changes applied`);
|
|
2922
|
-
onProgress?.("cleaning up worktree...");
|
|
2923
|
-
await cleanupWorktree(forkPath, worktreePath);
|
|
2924
2402
|
return {
|
|
2925
2403
|
success: true,
|
|
2926
2404
|
files: analyzedFiles,
|
|
@@ -2933,42 +2411,25 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2933
2411
|
...notesResult(notes)
|
|
2934
2412
|
};
|
|
2935
2413
|
}
|
|
2936
|
-
async function runMergeEngine(config, options) {
|
|
2937
|
-
const { forkPath } = config;
|
|
2938
|
-
const worktreePath = getWorktreePath(forkPath);
|
|
2939
|
-
const { apply, onProgress, onStep } = options;
|
|
2940
|
-
await cleanupLeftoverWorktrees(forkPath);
|
|
2941
|
-
registerWorktree(forkPath, worktreePath);
|
|
2942
|
-
try {
|
|
2943
|
-
const ctx = await prepareUpstream(config, onProgress, onStep);
|
|
2944
|
-
return apply ? await runSyncMerge(config, ctx, onProgress, onStep) : await runAnalyzePreview(config, ctx, onProgress, onStep);
|
|
2945
|
-
} catch (error) {
|
|
2946
|
-
await cleanupWorktree(forkPath, worktreePath);
|
|
2947
|
-
throw error;
|
|
2948
|
-
}
|
|
2949
|
-
}
|
|
2950
2414
|
async function runEngineWithSpinner(config, apply) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
createSpinner("...");
|
|
2959
|
-
}
|
|
2960
|
-
});
|
|
2415
|
+
const onProgress = (message) => spinnerText(message);
|
|
2416
|
+
const onStep = (label, detail) => {
|
|
2417
|
+
spinnerSuccess(label, detail);
|
|
2418
|
+
createSpinner("...");
|
|
2419
|
+
};
|
|
2420
|
+
const ctx = await prepareUpstream(config, onProgress, onStep);
|
|
2421
|
+
return apply ? runSyncMerge(config, ctx, onProgress, onStep) : runAnalyzePreview(config, ctx, onProgress, onStep);
|
|
2961
2422
|
}
|
|
2962
2423
|
|
|
2963
2424
|
// src/services/migrate.ts
|
|
2964
|
-
import { execFileSync
|
|
2425
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
2965
2426
|
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2966
|
-
import { dirname as dirname4, join as
|
|
2427
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
2967
2428
|
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2968
2429
|
var RESTORED_LIST_MAX = 20;
|
|
2969
2430
|
async function ensureUpstreamCommit(config, sha) {
|
|
2970
2431
|
const { forkPath } = config;
|
|
2971
|
-
const present = async () => !!await
|
|
2432
|
+
const present = async () => !!await revParse(forkPath, `${sha}^{commit}`);
|
|
2972
2433
|
if (await present()) return;
|
|
2973
2434
|
await ensureRemote(forkPath, DEFAULT_UPSTREAM_REMOTE, config.settings.upstreamUrl);
|
|
2974
2435
|
await fetchRemote(forkPath, DEFAULT_UPSTREAM_REMOTE);
|
|
@@ -2986,7 +2447,7 @@ async function readNotesStatus(config) {
|
|
|
2986
2447
|
try {
|
|
2987
2448
|
const ref = await resolveNotesRef(config);
|
|
2988
2449
|
if (!ref) return null;
|
|
2989
|
-
const [ids, pending] = await Promise.all([listNoteIds(config.forkPath, ref), readPending(config.forkPath
|
|
2450
|
+
const [ids, pending] = await Promise.all([listNoteIds(config.forkPath, ref), readPending(config.forkPath)]);
|
|
2990
2451
|
return ids.length > 0 || pending.length > 0 ? { total: ids.length, open: pending.length } : null;
|
|
2991
2452
|
} catch {
|
|
2992
2453
|
return null;
|
|
@@ -3007,9 +2468,6 @@ async function printMigrationNotesLine(config, result) {
|
|
|
3007
2468
|
console.info();
|
|
3008
2469
|
console.info(colors_default.dim(formatNotesLine(status.total, status.open)));
|
|
3009
2470
|
}
|
|
3010
|
-
function toJson(note, url, open) {
|
|
3011
|
-
return { ...note, kind: note.codemod ? "codemod" : "manual", open, url: url ?? null };
|
|
3012
|
-
}
|
|
3013
2471
|
function printNote(index, note, url) {
|
|
3014
2472
|
const tags = [
|
|
3015
2473
|
note.codemod ? "codemod" : "manual",
|
|
@@ -3029,13 +2487,13 @@ async function writeNoteFolder(config, ref, id) {
|
|
|
3029
2487
|
const note = await readNote(forkPath, ref, id);
|
|
3030
2488
|
if (!note) throw new Error(`no migration note '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
3031
2489
|
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${NOTES_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
3032
|
-
const target =
|
|
2490
|
+
const target = join8(EXTRACT_DIR, id);
|
|
3033
2491
|
for (const file of files) {
|
|
3034
|
-
const content =
|
|
2492
|
+
const content = execFileSync("git", ["show", `${ref}:${NOTES_DIR}/${id}/${file}`], {
|
|
3035
2493
|
cwd: forkPath,
|
|
3036
2494
|
maxBuffer: MAX_BUFFER
|
|
3037
2495
|
});
|
|
3038
|
-
const path =
|
|
2496
|
+
const path = join8(forkPath, target, file);
|
|
3039
2497
|
await mkdir4(dirname4(path), { recursive: true });
|
|
3040
2498
|
await writeFile3(path, content);
|
|
3041
2499
|
}
|
|
@@ -3050,31 +2508,15 @@ async function extractNote(config, ref, id) {
|
|
|
3050
2508
|
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella migrate --run ${id}`));
|
|
3051
2509
|
}
|
|
3052
2510
|
}
|
|
3053
|
-
function
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
throw new Error(`migration note '${id}' has no file '${script}'. its files: ${files.join(", ")}`);
|
|
3058
|
-
}
|
|
3059
|
-
return name;
|
|
3060
|
-
}
|
|
3061
|
-
const scripts = files.filter((file) => !file.includes("/") && isCodemodFile(file));
|
|
3062
|
-
if (scripts.length === 0) {
|
|
2511
|
+
async function runNote(config, ref, id) {
|
|
2512
|
+
const { forkPath } = config;
|
|
2513
|
+
const { note, target } = await writeNoteFolder(config, ref, id);
|
|
2514
|
+
if (!note.codemod) {
|
|
3063
2515
|
throw new Error(
|
|
3064
2516
|
`migration note '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella migrate --show ${id}\`.`
|
|
3065
2517
|
);
|
|
3066
2518
|
}
|
|
3067
|
-
|
|
3068
|
-
throw new Error(
|
|
3069
|
-
`migration note '${id}' ships ${scripts.length} scripts (${scripts.join(", ")}): name the one to run with --script <file>.`
|
|
3070
|
-
);
|
|
3071
|
-
}
|
|
3072
|
-
return scripts[0];
|
|
3073
|
-
}
|
|
3074
|
-
async function runNote(config, ref, id) {
|
|
3075
|
-
const { forkPath } = config;
|
|
3076
|
-
const { note, target, files } = await writeNoteFolder(config, ref, id);
|
|
3077
|
-
const script = join11(target, resolveCodemod(id, files, config.script));
|
|
2519
|
+
const script = join8(target, note.codemod);
|
|
3078
2520
|
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...note.roots];
|
|
3079
2521
|
const upstreamCommit = await readManifestBase(forkPath) ?? ref;
|
|
3080
2522
|
await ensureUpstreamCommit(config, upstreamCommit);
|
|
@@ -3121,7 +2563,7 @@ async function runMigrate(config) {
|
|
|
3121
2563
|
console.info(colors_default.dim("no sync point recorded yet: migration notes start with the first `pnpm cella sync`."));
|
|
3122
2564
|
return;
|
|
3123
2565
|
}
|
|
3124
|
-
const pending = await readPending(forkPath
|
|
2566
|
+
const pending = await readPending(forkPath);
|
|
3125
2567
|
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
3126
2568
|
const urlOf = (id) => noteUrl(githubUrl, ref, id);
|
|
3127
2569
|
if (config.mark?.length) return markNotes(config, ref, pending, config.mark);
|
|
@@ -3156,7 +2598,12 @@ async function runMigrate(config) {
|
|
|
3156
2598
|
upstreamCommit: ref,
|
|
3157
2599
|
total,
|
|
3158
2600
|
open: pending.length,
|
|
3159
|
-
notes: notes.map((note) =>
|
|
2601
|
+
notes: notes.map((note) => ({
|
|
2602
|
+
...note,
|
|
2603
|
+
kind: note.codemod ? "codemod" : "manual",
|
|
2604
|
+
open: open.has(note.id),
|
|
2605
|
+
url: urlOf(note.id) ?? null
|
|
2606
|
+
}))
|
|
3160
2607
|
};
|
|
3161
2608
|
writeStdout(JSON.stringify(out, null, 2));
|
|
3162
2609
|
return;
|
|
@@ -3185,29 +2632,11 @@ function filterByScope(result, scope) {
|
|
|
3185
2632
|
(f) => statuses.has(f.status) || f.upstreamChanged === true || f.upstreamOnly === true || overridesChanged && f.path === CONFIG_FILE
|
|
3186
2633
|
);
|
|
3187
2634
|
}
|
|
3188
|
-
function findTargetFile(files, targetPath) {
|
|
3189
|
-
const exact = files.find((f) => f.path === targetPath);
|
|
3190
|
-
if (exact) return exact;
|
|
3191
|
-
const normalized = targetPath.replace(/^\.\//, "");
|
|
3192
|
-
return files.find((f) => f.path === normalized);
|
|
3193
|
-
}
|
|
3194
|
-
function printUnifiedDiff(config, filePath) {
|
|
3195
|
-
const diff = gitDiffFile(config.forkPath, `${config.upstreamRef}..HEAD`, filePath, {
|
|
3196
|
-
dstPrefix: basename4(config.forkPath)
|
|
3197
|
-
});
|
|
3198
|
-
writeStdout(diff.toString());
|
|
3199
|
-
}
|
|
3200
2635
|
async function runAnalyze(config) {
|
|
3201
2636
|
createSpinner("starting analysis...");
|
|
3202
2637
|
const result = await runEngineWithSpinner(config, false);
|
|
3203
2638
|
spinnerSuccess();
|
|
3204
2639
|
const scopedFiles = filterByScope(result, config.scope ?? "all");
|
|
3205
|
-
if (config.diff) {
|
|
3206
|
-
const file = findTargetFile(scopedFiles, config.diff) ?? findTargetFile(result.files, config.diff);
|
|
3207
|
-
if (!file) throw new Error(`file not found in analysis results: ${config.diff}`);
|
|
3208
|
-
printUnifiedDiff(config, file.path);
|
|
3209
|
-
return result;
|
|
3210
|
-
}
|
|
3211
2640
|
if (config.json) {
|
|
3212
2641
|
const overrides = result.upstreamOverrides?.kind === "changes" ? result.upstreamOverrides : void 0;
|
|
3213
2642
|
const out = scopedFiles.map((f) => ({
|
|
@@ -3233,28 +2662,6 @@ async function runAnalyze(config) {
|
|
|
3233
2662
|
}
|
|
3234
2663
|
return result;
|
|
3235
2664
|
}
|
|
3236
|
-
if (config.openDiff) {
|
|
3237
|
-
const file = findTargetFile(scopedFiles, config.openDiff) ?? findTargetFile(result.files, config.openDiff);
|
|
3238
|
-
if (!file) throw new Error(`file not found in analysis results: ${config.openDiff}`);
|
|
3239
|
-
const patch = gitDiffFile(config.forkPath, config.upstreamRef, file.path);
|
|
3240
|
-
if (patch.length === 0) {
|
|
3241
|
-
console.info(`${checkMark} ${file.path} is identical to upstream`);
|
|
3242
|
-
return result;
|
|
3243
|
-
}
|
|
3244
|
-
const pagePath = await openDiffInBrowser(
|
|
3245
|
-
patch.toString(),
|
|
3246
|
-
{
|
|
3247
|
-
filePath: file.path,
|
|
3248
|
-
srcLabel: "cella",
|
|
3249
|
-
dstLabel: basename4(config.forkPath),
|
|
3250
|
-
note: "local working tree"
|
|
3251
|
-
},
|
|
3252
|
-
config.forkPath
|
|
3253
|
-
);
|
|
3254
|
-
console.info(`${checkMark} opened browser diff for ${file.path}`);
|
|
3255
|
-
console.info(colors_default.dim(` ${pagePath}`));
|
|
3256
|
-
return result;
|
|
3257
|
-
}
|
|
3258
2665
|
const linkOptions = {
|
|
3259
2666
|
upstreamGitHubUrl: result.upstreamGitHubUrl,
|
|
3260
2667
|
upstreamBranch: result.upstreamBranch,
|
|
@@ -3264,67 +2671,23 @@ async function runAnalyze(config) {
|
|
|
3264
2671
|
printAnalysisFileGroups(result.files, linkOptions, result);
|
|
3265
2672
|
printEngineReports(result, "analysis summary");
|
|
3266
2673
|
await printMigrationNotesLine(config, result);
|
|
3267
|
-
printLogFileReport(config, result.files);
|
|
3268
2674
|
console.info();
|
|
3269
2675
|
return result;
|
|
3270
2676
|
}
|
|
3271
2677
|
|
|
3272
2678
|
// src/services/audit.ts
|
|
3273
|
-
import { spawnSync as
|
|
2679
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
3274
2680
|
import { readFileSync as readFileSync3 } from "fs";
|
|
3275
|
-
import { basename as
|
|
3276
|
-
import { checkbox, confirm, Separator } from "@inquirer/prompts";
|
|
2681
|
+
import { basename as basename2, join as join10, relative as relative3 } from "path";
|
|
2682
|
+
import { checkbox, confirm as confirm2, Separator } from "@inquirer/prompts";
|
|
3277
2683
|
|
|
3278
2684
|
// src/utils/audit-utils.ts
|
|
3279
2685
|
import { execFile as execFile2 } from "child_process";
|
|
3280
|
-
import { existsSync as existsSync8, readFileSync as readFileSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3281
|
-
import { join as join12 } from "path";
|
|
3282
2686
|
import { promisify as promisify2 } from "util";
|
|
3283
2687
|
var execFileAsync2 = promisify2(execFile2);
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
var DEFAULT_BRANCHES = ["main", "master"];
|
|
3288
|
-
function loadCache() {
|
|
3289
|
-
try {
|
|
3290
|
-
if (existsSync8(CACHE_FILE)) {
|
|
3291
|
-
return JSON.parse(readFileSync2(CACHE_FILE, "utf8"));
|
|
3292
|
-
}
|
|
3293
|
-
} catch {
|
|
3294
|
-
}
|
|
3295
|
-
return {};
|
|
3296
|
-
}
|
|
3297
|
-
function saveCache(cache) {
|
|
3298
|
-
try {
|
|
3299
|
-
writeFileSync3(CACHE_FILE, `${JSON.stringify(cache, null, 2)}
|
|
3300
|
-
`);
|
|
3301
|
-
} catch {
|
|
3302
|
-
}
|
|
3303
|
-
}
|
|
3304
|
-
function clearCache() {
|
|
3305
|
-
try {
|
|
3306
|
-
if (existsSync8(CACHE_FILE)) {
|
|
3307
|
-
unlinkSync(CACHE_FILE);
|
|
3308
|
-
console.info(colors_default.green("\u2713 cache cleared successfully"));
|
|
3309
|
-
} else {
|
|
3310
|
-
console.info(colors_default.yellow("no cache file found"));
|
|
3311
|
-
}
|
|
3312
|
-
} catch (err) {
|
|
3313
|
-
console.error(colors_default.red("failed to clear cache:"), err);
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
async function fetchNpmMetadata(packageName) {
|
|
3317
|
-
try {
|
|
3318
|
-
const response = await fetch(`https://registry.npmjs.org/${packageName}`);
|
|
3319
|
-
if (!response.ok) return null;
|
|
3320
|
-
return await response.json();
|
|
3321
|
-
} catch {
|
|
3322
|
-
return null;
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
function getRepoUrl(data) {
|
|
3326
|
-
if (!data?.repository?.url) return null;
|
|
3327
|
-
let url = data.repository.url;
|
|
2688
|
+
function getRepoUrl(repositoryUrl) {
|
|
2689
|
+
if (!repositoryUrl) return null;
|
|
2690
|
+
let url = repositoryUrl;
|
|
3328
2691
|
if (url.startsWith("git+")) {
|
|
3329
2692
|
url = url.slice(4);
|
|
3330
2693
|
}
|
|
@@ -3345,110 +2708,43 @@ function isGitHubRepoUrl(repoUrl) {
|
|
|
3345
2708
|
return false;
|
|
3346
2709
|
}
|
|
3347
2710
|
}
|
|
3348
|
-
async function findGitHubFile(repoUrl, filePath) {
|
|
3349
|
-
if (!isGitHubRepoUrl(repoUrl)) return null;
|
|
3350
|
-
for (const branch of DEFAULT_BRANCHES) {
|
|
3351
|
-
const rawUrl = repoUrl.replace("github.com", "raw.githubusercontent.com").concat(`/${branch}/${filePath}`);
|
|
3352
|
-
try {
|
|
3353
|
-
const response = await fetch(rawUrl, { method: "HEAD" });
|
|
3354
|
-
if (response.ok) return branch;
|
|
3355
|
-
} catch {
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
return null;
|
|
3359
|
-
}
|
|
3360
|
-
async function findChangelogUrl(repoUrl, packageName, cache) {
|
|
3361
|
-
if (!isGitHubRepoUrl(repoUrl)) return null;
|
|
3362
|
-
const cached = cache[packageName];
|
|
3363
|
-
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL) {
|
|
3364
|
-
return cached.changelogUrl;
|
|
3365
|
-
}
|
|
3366
|
-
for (const changelogPath of CHANGELOG_PATHS) {
|
|
3367
|
-
const branch = await findGitHubFile(repoUrl, changelogPath);
|
|
3368
|
-
if (branch) {
|
|
3369
|
-
const blobUrl = `${repoUrl}/blob/${branch}/${changelogPath}`;
|
|
3370
|
-
cache[packageName] = {
|
|
3371
|
-
repoUrl,
|
|
3372
|
-
changelogUrl: blobUrl,
|
|
3373
|
-
fetchedAt: Date.now()
|
|
3374
|
-
};
|
|
3375
|
-
return blobUrl;
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
cache[packageName] = {
|
|
3379
|
-
repoUrl,
|
|
3380
|
-
changelogUrl: null,
|
|
3381
|
-
fetchedAt: Date.now()
|
|
3382
|
-
};
|
|
3383
|
-
return null;
|
|
3384
|
-
}
|
|
3385
2711
|
function getReleasesUrl(repoUrl) {
|
|
3386
2712
|
if (!isGitHubRepoUrl(repoUrl)) return null;
|
|
3387
2713
|
return `${repoUrl}/releases`;
|
|
3388
2714
|
}
|
|
3389
2715
|
function isMajorVersionChange(current, latest) {
|
|
3390
|
-
const
|
|
3391
|
-
const
|
|
3392
|
-
|
|
3393
|
-
return Number.parseInt(latestMajor, 10) > Number.parseInt(currentMajor, 10);
|
|
2716
|
+
const from = parseComparableVersion(current);
|
|
2717
|
+
const to = parseComparableVersion(latest);
|
|
2718
|
+
return !!from && !!to && to.major > from.major;
|
|
3394
2719
|
}
|
|
3395
|
-
async function
|
|
3396
|
-
const parse = (stdout) => {
|
|
3397
|
-
if (!stdout || stdout.trim() === "") return null;
|
|
3398
|
-
try {
|
|
3399
|
-
return JSON.parse(stdout);
|
|
3400
|
-
} catch {
|
|
3401
|
-
return null;
|
|
3402
|
-
}
|
|
3403
|
-
};
|
|
2720
|
+
async function runPnpm(args, cwd) {
|
|
3404
2721
|
try {
|
|
3405
|
-
const { stdout } = await execFileAsync2("pnpm", args, {
|
|
2722
|
+
const { stdout, stderr } = await execFileAsync2("pnpm", args, {
|
|
3406
2723
|
encoding: "utf8",
|
|
3407
2724
|
maxBuffer: 10 * 1024 * 1024,
|
|
3408
|
-
// 10MB buffer for large outputs
|
|
3409
2725
|
cwd
|
|
3410
2726
|
});
|
|
3411
|
-
return
|
|
2727
|
+
return { ok: true, stdout, stderr };
|
|
3412
2728
|
} catch (error) {
|
|
3413
|
-
|
|
2729
|
+
const { stdout = "", stderr = "" } = error;
|
|
2730
|
+
return { ok: false, stdout, stderr: stdout || stderr ? stderr : String(error) };
|
|
3414
2731
|
}
|
|
3415
2732
|
}
|
|
3416
|
-
async function
|
|
3417
|
-
|
|
2733
|
+
async function runPnpmJson(args, cwd) {
|
|
2734
|
+
try {
|
|
2735
|
+
return JSON.parse((await runPnpm([...args, "--loglevel=error"], cwd)).stdout);
|
|
2736
|
+
} catch {
|
|
2737
|
+
return null;
|
|
2738
|
+
}
|
|
3418
2739
|
}
|
|
3419
2740
|
async function runPnpmAudit(cwd) {
|
|
3420
|
-
|
|
2741
|
+
const result = await runPnpmJson(["audit", "--json"], cwd);
|
|
2742
|
+
if (!result || typeof result.advisories !== "object" || result.advisories === null) return null;
|
|
2743
|
+
return result;
|
|
3421
2744
|
}
|
|
3422
|
-
function
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
if (parts.length >= 2) {
|
|
3426
|
-
return {
|
|
3427
|
-
workspace: parts[0],
|
|
3428
|
-
directDependency: parts.length > 2 ? parts[1] : null
|
|
3429
|
-
};
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
return { workspace: null, directDependency: null };
|
|
3433
|
-
}
|
|
3434
|
-
function buildVulnerabilityMap(auditResult) {
|
|
3435
|
-
const map = /* @__PURE__ */ new Map();
|
|
3436
|
-
if (!auditResult?.advisories) return map;
|
|
3437
|
-
for (const advisory of Object.values(auditResult.advisories)) {
|
|
3438
|
-
const allPaths = advisory.findings?.flatMap((f) => f.paths) || [];
|
|
3439
|
-
const { workspace, directDependency } = parseDependencyPath(allPaths);
|
|
3440
|
-
const existing = map.get(advisory.module_name) || [];
|
|
3441
|
-
existing.push({
|
|
3442
|
-
title: advisory.title,
|
|
3443
|
-
severity: advisory.severity,
|
|
3444
|
-
vulnerableVersions: advisory.vulnerable_versions,
|
|
3445
|
-
cves: advisory.cves || [],
|
|
3446
|
-
workspace,
|
|
3447
|
-
directDependency
|
|
3448
|
-
});
|
|
3449
|
-
map.set(advisory.module_name, existing);
|
|
3450
|
-
}
|
|
3451
|
-
return map;
|
|
2745
|
+
async function fetchPublishTimes(name, cwd) {
|
|
2746
|
+
const times = await runPnpmJson(["view", name, "time", "--json"], cwd);
|
|
2747
|
+
return times && typeof times === "object" ? times : null;
|
|
3452
2748
|
}
|
|
3453
2749
|
var vulnIcons = {
|
|
3454
2750
|
critical: colors_default.red("\u25CF"),
|
|
@@ -3460,14 +2756,6 @@ var vulnIcons = {
|
|
|
3460
2756
|
function getVulnIcon(severity) {
|
|
3461
2757
|
return vulnIcons[severity] ?? colors_default.gray("\u25CF");
|
|
3462
2758
|
}
|
|
3463
|
-
function getHighestSeverity(vulns) {
|
|
3464
|
-
if (vulns.length === 0) return null;
|
|
3465
|
-
const order = ["critical", "high", "moderate", "low", "info"];
|
|
3466
|
-
for (const severity of order) {
|
|
3467
|
-
if (vulns.some((v) => v.severity === severity)) return severity;
|
|
3468
|
-
}
|
|
3469
|
-
return null;
|
|
3470
|
-
}
|
|
3471
2759
|
function middleTruncate(str2, maxLen) {
|
|
3472
2760
|
if (str2.length <= maxLen) return str2;
|
|
3473
2761
|
const ellipsis = "\u2026";
|
|
@@ -3482,51 +2770,503 @@ function formatDependents(dependents) {
|
|
|
3482
2770
|
return `${dependents[0]} +${dependents.length - 1}`;
|
|
3483
2771
|
}
|
|
3484
2772
|
|
|
3485
|
-
// src/utils/
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
2773
|
+
// src/utils/security-plan.ts
|
|
2774
|
+
var SEVERITIES = ["critical", "high", "moderate", "low", "info"];
|
|
2775
|
+
function isAuditOverride(key) {
|
|
2776
|
+
if (/[^ |@]>/.test(key)) return false;
|
|
2777
|
+
return key.indexOf("@", 1) > 0;
|
|
2778
|
+
}
|
|
2779
|
+
function overrideModule(key) {
|
|
2780
|
+
const child = key.split(/(?<=[^ |@])>/).at(-1) ?? key;
|
|
2781
|
+
const at = child.indexOf("@", 1);
|
|
2782
|
+
return at > 0 ? child.slice(0, at) : child;
|
|
2783
|
+
}
|
|
2784
|
+
function parseVersionedExclude(entry) {
|
|
2785
|
+
const at = entry.lastIndexOf("@");
|
|
2786
|
+
if (at <= 0) return null;
|
|
2787
|
+
const version = entry.slice(at + 1);
|
|
2788
|
+
return /^\d+\.\d+\.\d+[0-9A-Za-z.+-]*$/.test(version) ? { name: entry.slice(0, at), version } : null;
|
|
2789
|
+
}
|
|
2790
|
+
function advisoriesFromAudit(audit) {
|
|
2791
|
+
return Object.values(audit.advisories).map((advisory) => {
|
|
2792
|
+
const paths = (advisory.findings ?? []).flatMap((finding) => finding.paths ?? []).map((path) => path.split(">"));
|
|
2793
|
+
return {
|
|
2794
|
+
ghsa: advisory.github_advisory_id ?? advisory.url ?? String(advisory.id),
|
|
2795
|
+
module: advisory.module_name,
|
|
2796
|
+
severity: advisory.severity,
|
|
2797
|
+
range: advisory.vulnerable_versions,
|
|
2798
|
+
installed: [...new Set((advisory.findings ?? []).map((finding) => finding.version))],
|
|
2799
|
+
via: [...new Set(paths.map((parts) => parts[parts.length > 2 ? 1 : 0]))],
|
|
2800
|
+
direct: paths.some((parts) => parts.length === 2)
|
|
2801
|
+
};
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
var publishedAgo = (name, version, input) => input.now - Date.parse(input.published[name]?.[version] ?? "");
|
|
2805
|
+
function isInstallable(name, version, input) {
|
|
2806
|
+
const excludes = input.previous.releaseAgeExcludes;
|
|
2807
|
+
if (excludes.includes(name) || excludes.includes(`${name}@${version}`)) return true;
|
|
2808
|
+
const age = publishedAgo(name, version, input);
|
|
2809
|
+
return Number.isNaN(age) || age >= input.releaseAgeMinutes * 6e4;
|
|
2810
|
+
}
|
|
2811
|
+
function admits(target, version) {
|
|
2812
|
+
const base = target.replace(/^[\^~>=<\s]+/, "");
|
|
2813
|
+
if (compareVersionStrings(base, version) > 0) return false;
|
|
2814
|
+
return target.trim().startsWith(">") || !isBreakingJump(base, version);
|
|
2815
|
+
}
|
|
2816
|
+
var previousOverrides = (module, previous) => Object.entries(previous.overrides).filter(([key]) => isAuditOverride(key) && overrideModule(key) === module);
|
|
2817
|
+
function standingOverrides(module, input) {
|
|
2818
|
+
const versions = Object.keys(input.published[module] ?? {});
|
|
2819
|
+
return Object.fromEntries(
|
|
2820
|
+
previousOverrides(module, input.previous).filter(([, target]) => versions.some((v) => admits(target, v)))
|
|
2821
|
+
);
|
|
2822
|
+
}
|
|
2823
|
+
function planSecurity(input) {
|
|
2824
|
+
const ignored = new Set(input.previous.ignoredGhsas);
|
|
2825
|
+
const byModule = Map.groupBy(input.advisories, (advisory) => advisory.module);
|
|
2826
|
+
const items = [];
|
|
2827
|
+
for (const [module, advisories] of byModule) {
|
|
2828
|
+
const versions = Object.keys(input.published[module] ?? {}).filter((v) => /^\d/.test(v));
|
|
2829
|
+
const stable = versions.filter((v) => !isPrerelease(v)).sort(compareVersionStrings);
|
|
2830
|
+
const escapesAll = (v) => !advisories.some((advisory) => satisfiesComparators(v, advisory.range));
|
|
2831
|
+
const fixOf = (advisory) => {
|
|
2832
|
+
const highest = [...advisory.installed].sort(compareVersionStrings).at(-1) ?? "0.0.0";
|
|
2833
|
+
const candidates = stable.filter(
|
|
2834
|
+
(v) => compareVersionStrings(v, highest) > 0 && !satisfiesComparators(v, advisory.range)
|
|
2835
|
+
);
|
|
2836
|
+
const installable = candidates.filter((v) => isInstallable(module, v, input));
|
|
2837
|
+
const pick = (among) => among.find(escapesAll) ?? among[0];
|
|
2838
|
+
return pick(installable) ?? pick(candidates);
|
|
2839
|
+
};
|
|
2840
|
+
for (const [fix, group] of Map.groupBy(advisories, fixOf)) {
|
|
2841
|
+
const installed = [...new Set(group.flatMap((advisory) => advisory.installed))].sort(compareVersionStrings);
|
|
2842
|
+
const ghsas = group.map((advisory) => advisory.ghsa);
|
|
2843
|
+
const allIgnored = ghsas.every((ghsa) => ignored.has(ghsa));
|
|
2844
|
+
const breaking = !!fix && installed.some((v) => isBreakingJump(v, fix));
|
|
2845
|
+
let remedy = { kind: "none" };
|
|
2846
|
+
let standing = allIgnored ? "ignored" : "new";
|
|
2847
|
+
let overrides = {};
|
|
2848
|
+
if (fix) {
|
|
2849
|
+
const hoursOld = Math.max(0, Math.floor(publishedAgo(module, fix, input) / 36e5));
|
|
2850
|
+
remedy = isInstallable(module, fix, input) ? { kind: "override", target: fix, breaking } : { kind: "fresh", target: fix, breaking, hoursOld };
|
|
2851
|
+
overrides = Object.fromEntries(
|
|
2852
|
+
[...new Set(group.map((advisory) => advisory.range))].map((range) => {
|
|
2853
|
+
const key = `${module}@${range}`;
|
|
2854
|
+
const kept = input.previous.overrides[key];
|
|
2855
|
+
return [key, kept && admits(kept, fix) ? kept : `^${fix}`];
|
|
2856
|
+
})
|
|
2857
|
+
);
|
|
2858
|
+
if (remedy.kind === "override") {
|
|
2859
|
+
if (previousOverrides(module, input.previous).some(([, target]) => admits(target, fix))) standing = "carried";
|
|
2860
|
+
else if (allIgnored && !breaking) standing = "new";
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
items.push({
|
|
2864
|
+
module,
|
|
2865
|
+
severity: SEVERITIES.find((severity) => group.some((advisory) => advisory.severity === severity)) ?? "info",
|
|
2866
|
+
ghsas,
|
|
2867
|
+
installed: installed[0] ?? "",
|
|
2868
|
+
via: [...new Set(group.flatMap((advisory) => advisory.via))],
|
|
2869
|
+
direct: group.some((advisory) => advisory.direct),
|
|
2870
|
+
remedy,
|
|
2871
|
+
standing,
|
|
2872
|
+
overrides
|
|
2873
|
+
});
|
|
3493
2874
|
}
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
return
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
2875
|
+
}
|
|
2876
|
+
return items.sort(
|
|
2877
|
+
(a, b) => SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity) || a.module.localeCompare(b.module)
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
function liveExcludes(input) {
|
|
2881
|
+
return input.previous.releaseAgeExcludes.filter((entry) => {
|
|
2882
|
+
const versioned = parseVersionedExclude(entry);
|
|
2883
|
+
if (!versioned || !input.published[versioned.name]) return true;
|
|
2884
|
+
return publishedAgo(versioned.name, versioned.version, input) < input.releaseAgeMinutes * 6e4;
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/utils/workspace-yaml.ts
|
|
2889
|
+
import { isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
2890
|
+
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2891
|
+
var stringRecord = (value) => Object.fromEntries(Object.entries(record(value)).map(([key, entry]) => [key, String(entry)]));
|
|
2892
|
+
var stringList = (value) => Array.isArray(value) ? value.map(String) : [];
|
|
2893
|
+
function readAuditSettings(content) {
|
|
2894
|
+
const doc = parseDocument(content);
|
|
2895
|
+
if (doc.errors.length > 0) throw new Error(doc.errors[0].message);
|
|
2896
|
+
const settings = record(doc.toJS());
|
|
2897
|
+
return {
|
|
2898
|
+
overrides: stringRecord(settings.overrides),
|
|
2899
|
+
releaseAgeExcludes: stringList(settings.minimumReleaseAgeExclude),
|
|
2900
|
+
ignoredGhsas: stringList(record(settings.auditConfig).ignoreGhsas)
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
function readTopLevel(content, key) {
|
|
2904
|
+
return record(parseDocument(content).toJS())[key];
|
|
2905
|
+
}
|
|
2906
|
+
function readPatchFiles(content) {
|
|
2907
|
+
return Object.values(stringRecord(record(parseDocument(content).toJS()).patchedDependencies));
|
|
2908
|
+
}
|
|
2909
|
+
var keyOf = (pair) => String(isScalar(pair.key) ? pair.key.value : pair.key);
|
|
2910
|
+
var itemText = (item) => String(isScalar(item) ? item.value : item);
|
|
2911
|
+
function keepingOrder(map, change) {
|
|
2912
|
+
const keys = map.items.map(keyOf);
|
|
2913
|
+
const wasSorted = keys.every((key, i) => i === 0 || keys[i - 1].localeCompare(key) <= 0);
|
|
2914
|
+
change();
|
|
2915
|
+
if (wasSorted) map.items.sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
|
|
2916
|
+
}
|
|
2917
|
+
function mapAt(doc, path) {
|
|
2918
|
+
if (path.length === 0) {
|
|
2919
|
+
if (!isMap(doc.contents)) doc.contents = doc.createNode({});
|
|
2920
|
+
return doc.contents;
|
|
2921
|
+
}
|
|
2922
|
+
const parent = mapAt(doc, path.slice(0, -1));
|
|
2923
|
+
const key = path[path.length - 1];
|
|
2924
|
+
const existing = parent.get(key, true);
|
|
2925
|
+
if (isMap(existing)) {
|
|
2926
|
+
if (existing.items.length === 0) existing.flow = false;
|
|
2927
|
+
return existing;
|
|
2928
|
+
}
|
|
2929
|
+
const created = doc.createNode({});
|
|
2930
|
+
keepingOrder(parent, () => parent.set(key, created));
|
|
2931
|
+
return created;
|
|
2932
|
+
}
|
|
2933
|
+
function syncMap(doc, path, entries) {
|
|
2934
|
+
if (!isMap(doc.getIn(path)) && Object.keys(entries).length === 0) return;
|
|
2935
|
+
const map = mapAt(doc, path);
|
|
2936
|
+
keepingOrder(map, () => {
|
|
2937
|
+
for (const key of map.items.map(keyOf)) if (!(key in entries)) map.delete(key);
|
|
2938
|
+
for (const [key, value] of Object.entries(entries)) map.set(key, value);
|
|
2939
|
+
});
|
|
2940
|
+
if (map.items.length === 0) doc.deleteIn(path);
|
|
2941
|
+
}
|
|
2942
|
+
function syncList(doc, path, entries) {
|
|
2943
|
+
const current = doc.getIn(path);
|
|
2944
|
+
if (!isSeq(current)) {
|
|
2945
|
+
if (entries.length === 0) return;
|
|
2946
|
+
const parent = mapAt(doc, path.slice(0, -1));
|
|
2947
|
+
keepingOrder(parent, () => parent.set(path[path.length - 1], doc.createNode(entries)));
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
current.items = current.items.filter((item) => entries.includes(itemText(item)));
|
|
2951
|
+
const present = new Set(current.items.map(itemText));
|
|
2952
|
+
for (const entry of entries) if (!present.has(entry)) current.add(doc.createNode(entry));
|
|
2953
|
+
if (current.items.length === 0) doc.deleteIn(path);
|
|
2954
|
+
else current.flow = false;
|
|
2955
|
+
}
|
|
2956
|
+
function writeAuditSettings(content, next) {
|
|
2957
|
+
const doc = parseDocument(content);
|
|
2958
|
+
syncMap(doc, ["overrides"], next.overrides);
|
|
2959
|
+
syncList(doc, ["minimumReleaseAgeExclude"], next.releaseAgeExcludes);
|
|
2960
|
+
syncList(doc, ["auditConfig", "ignoreGhsas"], next.ignoredGhsas);
|
|
2961
|
+
const auditConfig = doc.getIn(["auditConfig"]);
|
|
2962
|
+
if (isMap(auditConfig) && auditConfig.items.length === 0) doc.deleteIn(["auditConfig"]);
|
|
2963
|
+
return doc.toString({ lineWidth: 0, singleQuote: true });
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
// src/services/audit-security.ts
|
|
2967
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2968
|
+
import { cpSync, existsSync as existsSync7, mkdirSync, mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
|
|
2969
|
+
import { tmpdir } from "os";
|
|
2970
|
+
import { dirname as dirname5, join as join9, relative as relative2 } from "path";
|
|
2971
|
+
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
2972
|
+
var WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
2973
|
+
var LOCK_FILE = "pnpm-lock.yaml";
|
|
2974
|
+
var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
|
|
2975
|
+
var readOr = (path, fallback = "") => existsSync7(path) ? readFileSync2(path, "utf8") : fallback;
|
|
2976
|
+
var count = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
2977
|
+
var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
|
|
2978
|
+
var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
|
|
2979
|
+
var sameEntries = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([key, value]) => b[key] === value);
|
|
2980
|
+
async function createStaging(forkPath, workspaceYaml) {
|
|
2981
|
+
const staging = mkdtempSync(join9(tmpdir(), "cella-audit-"));
|
|
2982
|
+
const projects = await runPnpmJson(["ls", "-r", "--depth", "-1", "--json"], forkPath) ?? [];
|
|
2983
|
+
const files = [
|
|
2984
|
+
...RESOLUTION_FILES,
|
|
2985
|
+
...readPatchFiles(workspaceYaml),
|
|
2986
|
+
...projects.map((project) => join9(relative2(forkPath, project.path), "package.json"))
|
|
2987
|
+
];
|
|
2988
|
+
for (const file of new Set(files)) {
|
|
2989
|
+
if (file.startsWith("..") || !existsSync7(join9(forkPath, file))) continue;
|
|
2990
|
+
mkdirSync(dirname5(join9(staging, file)), { recursive: true });
|
|
2991
|
+
cpSync(join9(forkPath, file), join9(staging, file));
|
|
2992
|
+
}
|
|
2993
|
+
return staging;
|
|
2994
|
+
}
|
|
2995
|
+
async function resolveStaging(staging, workspaceYaml, settings) {
|
|
2996
|
+
writeFileSync(join9(staging, WORKSPACE_FILE), writeAuditSettings(workspaceYaml, settings));
|
|
2997
|
+
const result = await runPnpm(["install", "--lockfile-only", "--no-frozen-lockfile", "--ignore-scripts"], staging);
|
|
2998
|
+
return result.ok ? { lock: readOr(join9(staging, LOCK_FILE)) } : { error: `${result.stdout}${result.stderr}`.trim() };
|
|
2999
|
+
}
|
|
3000
|
+
function lockedVersions(lock, name) {
|
|
3001
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
3002
|
+
const versions = [...lock.matchAll(new RegExp(`^ {2}'?${escaped}@(\\d[^:'()\\s]*)`, "gm"))].map((match) => match[1]);
|
|
3003
|
+
return [...new Set(versions)];
|
|
3004
|
+
}
|
|
3005
|
+
function versionsText(item) {
|
|
3006
|
+
if (item.remedy.kind === "none") return item.installed;
|
|
3007
|
+
return `${item.installed} \u2192 ${item.remedy.target}${isBreaking(item) ? " \u26A0" : ""}`;
|
|
3008
|
+
}
|
|
3009
|
+
function noteText(item, releaseAgeHours) {
|
|
3010
|
+
if (item.remedy.kind === "none") return "no fix published";
|
|
3011
|
+
if (item.remedy.kind === "fresh")
|
|
3012
|
+
return `fix published ${item.remedy.hoursOld}h ago, inside the ${releaseAgeHours}h release age`;
|
|
3013
|
+
if (item.standing === "ignored") return "the fix is a breaking jump";
|
|
3014
|
+
return "";
|
|
3015
|
+
}
|
|
3016
|
+
function printGroup(title, items, releaseAgeHours) {
|
|
3017
|
+
if (items.length === 0) return;
|
|
3018
|
+
const nameWidth = Math.min(30, Math.max(...items.map((item) => item.module.length)));
|
|
3019
|
+
const versionWidth = Math.max(...items.map((item) => versionsText(item).length));
|
|
3020
|
+
console.info(colors_default.bold(`${title} (${items.length})`));
|
|
3021
|
+
for (const item of items) {
|
|
3022
|
+
const name = colors_default.white(middleTruncate(item.module, nameWidth).padEnd(nameWidth));
|
|
3023
|
+
const source = `${formatDependents(item.via)}${item.direct ? " (direct)" : ""}`;
|
|
3024
|
+
const tail = [source, noteText(item, releaseAgeHours)].filter(Boolean).join(" \xB7 ");
|
|
3025
|
+
console.info(
|
|
3026
|
+
`${getVulnIcon(item.severity)} ${name} ${item.severity.padEnd(8)} ${versionsText(item).padEnd(versionWidth)} ${colors_default.dim(tail)}`
|
|
3027
|
+
);
|
|
3028
|
+
}
|
|
3029
|
+
console.info();
|
|
3030
|
+
}
|
|
3031
|
+
function printDropped(title, rows) {
|
|
3032
|
+
if (rows.length === 0) return;
|
|
3033
|
+
const nameWidth = Math.min(40, Math.max(...rows.map(([name]) => name.length)));
|
|
3034
|
+
console.info(colors_default.bold(`${title} (${rows.length})`));
|
|
3035
|
+
for (const [name, reason] of rows) {
|
|
3036
|
+
console.info(`${colors_default.green("\u25CB")} ${colors_default.white(middleTruncate(name, nameWidth).padEnd(nameWidth))} ${colors_default.dim(reason)}`);
|
|
3037
|
+
}
|
|
3038
|
+
console.info();
|
|
3039
|
+
}
|
|
3040
|
+
function droppedReason(key, items, bareLock) {
|
|
3041
|
+
const module = overrideModule(key);
|
|
3042
|
+
const item = items.find((candidate) => candidate.module === module);
|
|
3043
|
+
if (item) return item.remedy.kind === "none" ? "no published version fixes it" : "replaced by another range";
|
|
3044
|
+
const versions = lockedVersions(bareLock, module);
|
|
3045
|
+
return versions.length > 0 ? `resolves to ${versions.join(", ")} on its own` : "no longer installed";
|
|
3046
|
+
}
|
|
3047
|
+
async function decide(items, releaseAgeHours) {
|
|
3048
|
+
const decisions = [];
|
|
3049
|
+
for (const item of items) {
|
|
3050
|
+
const { remedy } = item;
|
|
3051
|
+
const jump = remedy.kind === "none" ? "" : `${item.installed} \u2192 ${remedy.target}`;
|
|
3052
|
+
if (!needsDecision(item)) {
|
|
3053
|
+
decisions.push({ item, action: item.standing === "ignored" ? "ignore" : "override" });
|
|
3054
|
+
} else if (remedy.kind === "none") {
|
|
3055
|
+
const ignore = await confirm({
|
|
3056
|
+
message: `${item.module}: no fix is published. ignore ${item.ghsas.join(", ")} until one is?`,
|
|
3057
|
+
default: true
|
|
3058
|
+
});
|
|
3059
|
+
decisions.push({ item, action: ignore ? "ignore" : "open" });
|
|
3060
|
+
} else if (remedy.kind === "fresh") {
|
|
3061
|
+
const action = await select2({
|
|
3062
|
+
message: `${item.module}: fix ${remedy.target} was published ${remedy.hoursOld}h ago, inside the ${releaseAgeHours}h release age`,
|
|
3063
|
+
choices: [
|
|
3064
|
+
{ name: "wait and re-run later", value: "open" },
|
|
3065
|
+
{ name: "install now, with an exclude that a later audit removes", value: "override" }
|
|
3066
|
+
]
|
|
3067
|
+
});
|
|
3068
|
+
decisions.push({ item, action, exclude: action === "override" ? `${item.module}@${remedy.target}` : void 0 });
|
|
3069
|
+
} else {
|
|
3070
|
+
const message = item.direct ? `the fix is ${jump}, and ${item.via[0]} depends on it directly` : `the fix jumps ${jump}, past what ${formatDependents(item.via)} asks for`;
|
|
3071
|
+
const action = await select2({
|
|
3072
|
+
message: `${item.module}: ${message}`,
|
|
3073
|
+
choices: [
|
|
3074
|
+
{ name: item.direct ? "leave open, update it in package.json" : "leave open", value: "open" },
|
|
3075
|
+
{ name: "apply the override", value: "override" },
|
|
3076
|
+
{ name: "ignore the advisory", value: "ignore" }
|
|
3077
|
+
]
|
|
3078
|
+
});
|
|
3079
|
+
decisions.push({ item, action });
|
|
3508
3080
|
}
|
|
3509
3081
|
}
|
|
3510
|
-
return
|
|
3082
|
+
return decisions;
|
|
3511
3083
|
}
|
|
3512
|
-
function
|
|
3513
|
-
const
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3084
|
+
function settingsFor(decisions, kept, excludes) {
|
|
3085
|
+
const overriding = decisions.filter((decision) => decision.action === "override");
|
|
3086
|
+
return {
|
|
3087
|
+
overrides: { ...kept, ...Object.assign({}, ...overriding.map((decision) => decision.item.overrides)) },
|
|
3088
|
+
releaseAgeExcludes: [.../* @__PURE__ */ new Set([...excludes, ...overriding.flatMap((decision) => decision.exclude ?? [])])],
|
|
3089
|
+
ignoredGhsas: decisions.filter((decision) => decision.action === "ignore").flatMap((decision) => decision.item.ghsas)
|
|
3090
|
+
};
|
|
3091
|
+
}
|
|
3092
|
+
function changeLine(label, before, after) {
|
|
3093
|
+
const dropped = before.filter((entry) => !after.includes(entry)).length;
|
|
3094
|
+
const added = after.filter((entry) => !before.includes(entry)).length;
|
|
3095
|
+
const moved = [dropped ? `${dropped} dropped` : "", added ? `${added} added` : ""].filter(Boolean).join(", ");
|
|
3096
|
+
const counts = moved ? `${before.length} \u2192 ${after.length}` : String(after.length);
|
|
3097
|
+
return ` ${label.padEnd(9)} ${counts.padEnd(7)} ${colors_default.dim(moved || "unchanged")}`;
|
|
3098
|
+
}
|
|
3099
|
+
async function runSecurityPass(forkPath, options) {
|
|
3100
|
+
const workspaceYaml = readOr(join9(forkPath, WORKSPACE_FILE));
|
|
3101
|
+
const workspaceLock = readOr(join9(forkPath, LOCK_FILE));
|
|
3102
|
+
let previous;
|
|
3103
|
+
try {
|
|
3104
|
+
previous = readAuditSettings(workspaceYaml);
|
|
3105
|
+
} catch (error) {
|
|
3106
|
+
console.error(
|
|
3107
|
+
`${colors_default.red("\u2717")} ${WORKSPACE_FILE} could not be read: ${error instanceof Error ? error.message : error}`
|
|
3108
|
+
);
|
|
3109
|
+
return;
|
|
3110
|
+
}
|
|
3111
|
+
const auditKeys = Object.keys(previous.overrides).filter(isAuditOverride);
|
|
3112
|
+
const lasting = Object.fromEntries(Object.entries(previous.overrides).filter(([key]) => !isAuditOverride(key)));
|
|
3113
|
+
const spinner = createSpinner(
|
|
3114
|
+
auditKeys.length > 0 ? `resolving without the ${count(auditKeys.length, "audit override")}...` : "resolving the workspace..."
|
|
3115
|
+
);
|
|
3116
|
+
const fail = (message, detail = "") => {
|
|
3117
|
+
spinner.stop();
|
|
3118
|
+
console.error(`${colors_default.red("\u2717")} ${message}`);
|
|
3119
|
+
if (detail) console.error(colors_default.dim(detail.split("\n").slice(-12).join("\n")));
|
|
3120
|
+
console.info(colors_default.dim(" the workspace is unchanged"));
|
|
3121
|
+
console.info();
|
|
3122
|
+
};
|
|
3123
|
+
const staging = await createStaging(forkPath, workspaceYaml);
|
|
3124
|
+
try {
|
|
3125
|
+
const bare = await resolveStaging(staging, workspaceYaml, { ...previous, overrides: lasting, ignoredGhsas: [] });
|
|
3126
|
+
if ("error" in bare) return fail("could not resolve the workspace without its audit overrides", bare.error);
|
|
3127
|
+
spinnerText("checking for vulnerabilities...");
|
|
3128
|
+
const audit = await runPnpmAudit(staging);
|
|
3129
|
+
if (!audit) return fail("pnpm audit gave no answer, so nothing can be decided");
|
|
3130
|
+
const advisories = advisoriesFromAudit(audit);
|
|
3131
|
+
spinnerText(`checking ${count(advisories.length, "advisory", "advisories")} against the registry...`);
|
|
3132
|
+
const versionedExcludes = previous.releaseAgeExcludes.flatMap((entry) => parseVersionedExclude(entry)?.name ?? []);
|
|
3133
|
+
const names = [.../* @__PURE__ */ new Set([...advisories.map((advisory) => advisory.module), ...versionedExcludes])];
|
|
3134
|
+
const times = await Promise.all(names.map((name) => fetchPublishTimes(name, forkPath)));
|
|
3135
|
+
const published = Object.fromEntries(
|
|
3136
|
+
names.map((name, i) => [name, times[i]]).filter(([, answer]) => answer)
|
|
3137
|
+
);
|
|
3138
|
+
const unanswered = advisories.map((advisory) => advisory.module).filter((name) => !published[name]);
|
|
3139
|
+
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
|
+
const releaseAgeHours = Math.round(releaseAgeMinutes / 60);
|
|
3143
|
+
const planInput = { advisories, published, previous, releaseAgeMinutes, now: Date.now() };
|
|
3144
|
+
const items = planSecurity(planInput);
|
|
3145
|
+
const excludes = liveExcludes(planInput);
|
|
3146
|
+
spinner.stop();
|
|
3147
|
+
const changes = printPlan({
|
|
3148
|
+
items,
|
|
3149
|
+
previous,
|
|
3150
|
+
auditKeys,
|
|
3151
|
+
lasting,
|
|
3152
|
+
advisories,
|
|
3153
|
+
bareLock: bare.lock,
|
|
3154
|
+
releaseAgeHours
|
|
3155
|
+
});
|
|
3156
|
+
if (!options.apply) {
|
|
3157
|
+
if (changes) console.info(colors_default.dim("run without --list to apply"));
|
|
3158
|
+
console.info();
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
let decisions;
|
|
3162
|
+
try {
|
|
3163
|
+
decisions = await decide(items, releaseAgeHours);
|
|
3164
|
+
} catch {
|
|
3165
|
+
console.info(colors_default.dim(" cancelled, the workspace is unchanged"));
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
const open = decisions.filter((decision) => decision.action === "open").map((decision) => decision.item.module);
|
|
3169
|
+
const kept = Object.assign({}, lasting, ...open.map((module) => standingOverrides(module, planInput)));
|
|
3170
|
+
const next = settingsFor(decisions, kept, excludes);
|
|
3171
|
+
const nextYaml = writeAuditSettings(workspaceYaml, next);
|
|
3172
|
+
if (nextYaml === workspaceYaml) {
|
|
3173
|
+
const state = `${count(auditKeys.length, "override")} \xB7 ${next.ignoredGhsas.length} ignored`;
|
|
3174
|
+
console.info(`${checkMark} security settings are current ${colors_default.dim(`\xB7 ${state}`)}`);
|
|
3175
|
+
printOpen(open);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
const summarize = () => {
|
|
3179
|
+
console.info(colors_default.bold(WORKSPACE_FILE));
|
|
3180
|
+
console.info(changeLine("overrides", auditKeys, Object.keys(next.overrides).filter(isAuditOverride)));
|
|
3181
|
+
console.info(changeLine("excludes", previous.releaseAgeExcludes, next.releaseAgeExcludes));
|
|
3182
|
+
console.info(changeLine("ignored", previous.ignoredGhsas, next.ignoredGhsas));
|
|
3183
|
+
console.info();
|
|
3184
|
+
};
|
|
3185
|
+
if (sameEntries(next.overrides, previous.overrides)) {
|
|
3186
|
+
writeFileSync(join9(forkPath, WORKSPACE_FILE), nextYaml);
|
|
3187
|
+
console.info(`${checkMark} ${WORKSPACE_FILE} updated ${colors_default.dim("\xB7 nothing to install")}`);
|
|
3188
|
+
printOpen(open);
|
|
3189
|
+
summarize();
|
|
3190
|
+
return;
|
|
3191
|
+
}
|
|
3192
|
+
const stagingSpinner = createSpinner("resolving with the new settings...");
|
|
3193
|
+
const staged = await resolveStaging(staging, workspaceYaml, next);
|
|
3194
|
+
stagingSpinner.stop();
|
|
3195
|
+
if ("error" in staged) return fail("could not resolve the workspace with the new settings", staged.error);
|
|
3196
|
+
writeFileSync(join9(forkPath, WORKSPACE_FILE), nextYaml);
|
|
3197
|
+
writeFileSync(join9(forkPath, LOCK_FILE), staged.lock);
|
|
3198
|
+
const install = spawnSync2("pnpm", ["install"], { cwd: forkPath, stdio: "inherit" });
|
|
3199
|
+
console.info();
|
|
3200
|
+
if (install.status !== 0) {
|
|
3201
|
+
writeFileSync(join9(forkPath, WORKSPACE_FILE), workspaceYaml);
|
|
3202
|
+
writeFileSync(join9(forkPath, LOCK_FILE), workspaceLock);
|
|
3203
|
+
console.error(`${colors_default.red("\u2717")} pnpm install failed (exit code ${install.status})`);
|
|
3204
|
+
console.info(colors_default.dim(` ${WORKSPACE_FILE} and ${LOCK_FILE} are back as they were; run pnpm install to match`));
|
|
3205
|
+
console.info();
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
const verified = await runPnpmAudit(forkPath);
|
|
3209
|
+
const reported = verified ? [...new Set(advisoriesFromAudit(verified).map((advisory) => advisory.module))] : open;
|
|
3210
|
+
console.info(`${checkMark} installed${reported.length === 0 ? ` ${colors_default.dim("\xB7")} audit clean` : ""}`);
|
|
3211
|
+
printOpen(reported);
|
|
3212
|
+
summarize();
|
|
3213
|
+
} finally {
|
|
3214
|
+
spinner.stop();
|
|
3215
|
+
rmSync(staging, { recursive: true, force: true });
|
|
3517
3216
|
}
|
|
3518
|
-
|
|
3217
|
+
}
|
|
3218
|
+
function printOpen(modules) {
|
|
3219
|
+
if (modules.length > 0) {
|
|
3220
|
+
console.info(`${warningMark} ${count(modules.length, "package")} still reported: ${modules.join(", ")}`);
|
|
3221
|
+
}
|
|
3222
|
+
console.info();
|
|
3223
|
+
}
|
|
3224
|
+
function printPlan(plan) {
|
|
3225
|
+
const { items, previous, releaseAgeHours } = plan;
|
|
3226
|
+
const kept = items.filter((item) => item.standing === "carried");
|
|
3227
|
+
const undecided = items.filter(needsDecision);
|
|
3228
|
+
const added = items.filter((item) => item.standing === "new" && !needsDecision(item));
|
|
3229
|
+
const ignored = items.filter((item) => item.standing === "ignored");
|
|
3230
|
+
const plannedKeys = new Set(
|
|
3231
|
+
items.flatMap((item) => item.standing === "ignored" ? [] : Object.keys(item.overrides))
|
|
3232
|
+
);
|
|
3233
|
+
const pending = new Set(undecided.map((item) => item.module));
|
|
3234
|
+
const droppedOverrides = plan.auditKeys.filter((key) => !plannedKeys.has(key) && !pending.has(overrideModule(key))).map((key) => [key, droppedReason(key, items, plan.bareLock)]);
|
|
3235
|
+
const reportedGhsas = new Set(plan.advisories.map((advisory) => advisory.ghsa));
|
|
3236
|
+
const fixedGhsas = new Set([...kept, ...added].flatMap((item) => item.ghsas));
|
|
3237
|
+
const droppedIgnores = previous.ignoredGhsas.filter((ghsa) => !reportedGhsas.has(ghsa) || fixedGhsas.has(ghsa)).map((ghsa) => [ghsa, fixedGhsas.has(ghsa) ? "a fix is published" : "no longer applies"]);
|
|
3238
|
+
console.info(colors_default.bold("security"));
|
|
3239
|
+
console.info(DIVIDER);
|
|
3240
|
+
if (items.length === 0 && droppedOverrides.length === 0 && droppedIgnores.length === 0) {
|
|
3241
|
+
console.info(`${checkMark} no vulnerabilities found`);
|
|
3242
|
+
console.info();
|
|
3243
|
+
return false;
|
|
3244
|
+
}
|
|
3245
|
+
printGroup("override kept", kept, releaseAgeHours);
|
|
3246
|
+
printGroup("fix available", added, releaseAgeHours);
|
|
3247
|
+
printGroup("needs a decision", undecided, releaseAgeHours);
|
|
3248
|
+
printGroup("ignored", ignored, releaseAgeHours);
|
|
3249
|
+
printDropped("override dropped", droppedOverrides);
|
|
3250
|
+
printDropped("ignore dropped", droppedIgnores);
|
|
3251
|
+
const lastingCount = Object.keys(plan.lasting).length;
|
|
3252
|
+
if (lastingCount > 0) {
|
|
3253
|
+
console.info(colors_default.dim(`${count(lastingCount, "override")} without a version selector left as written`));
|
|
3254
|
+
}
|
|
3255
|
+
if (items.some(isBreaking)) {
|
|
3256
|
+
console.info(colors_default.dim("\u26A0 a breaking jump: another major, or another minor below 1.0"));
|
|
3257
|
+
}
|
|
3258
|
+
console.info();
|
|
3259
|
+
return added.length + undecided.length + droppedOverrides.length + droppedIgnores.length > 0;
|
|
3519
3260
|
}
|
|
3520
3261
|
|
|
3521
3262
|
// src/services/audit.ts
|
|
3522
|
-
var AUDIT_FIX_VALUE = "__audit_fix__";
|
|
3523
3263
|
function createDependentPkgReader() {
|
|
3524
3264
|
const cache = /* @__PURE__ */ new Map();
|
|
3525
3265
|
return (location) => {
|
|
3526
3266
|
let cached = cache.get(location);
|
|
3527
3267
|
if (cached === void 0) {
|
|
3528
3268
|
try {
|
|
3529
|
-
cached = JSON.parse(readFileSync3(
|
|
3269
|
+
cached = JSON.parse(readFileSync3(join10(location, "package.json"), "utf8"));
|
|
3530
3270
|
} catch {
|
|
3531
3271
|
cached = null;
|
|
3532
3272
|
}
|
|
@@ -3535,106 +3275,74 @@ function createDependentPkgReader() {
|
|
|
3535
3275
|
return cached ?? null;
|
|
3536
3276
|
};
|
|
3537
3277
|
}
|
|
3538
|
-
function formatSeverityCounts(counts, suffix = "") {
|
|
3539
|
-
const parts = [];
|
|
3540
|
-
if (counts.critical) parts.push(colors_default.red(`${counts.critical} critical${suffix}`));
|
|
3541
|
-
if (counts.high) parts.push(colors_default.red(`${counts.high} high${suffix}`));
|
|
3542
|
-
if (counts.moderate) parts.push(colors_default.yellow(`${counts.moderate} moderate${suffix}`));
|
|
3543
|
-
if (counts.low) parts.push(colors_default.blue(`${counts.low} low`));
|
|
3544
|
-
return parts;
|
|
3545
|
-
}
|
|
3546
3278
|
async function runAudit(config) {
|
|
3547
3279
|
const { forkPath } = config;
|
|
3548
|
-
|
|
3549
|
-
await checkOverrides(forkPath);
|
|
3550
|
-
return;
|
|
3551
|
-
}
|
|
3552
|
-
const spinner = createSpinner("checking for outdated packages & vulnerabilities...");
|
|
3280
|
+
const spinner = createSpinner("checking for outdated packages...");
|
|
3553
3281
|
try {
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
});
|
|
3561
|
-
|
|
3282
|
+
const [outdated, auditResult] = await Promise.all([
|
|
3283
|
+
runPnpmJson(["-r", "outdated", "--json", "--long"], forkPath),
|
|
3284
|
+
runPnpmAudit(forkPath)
|
|
3285
|
+
]);
|
|
3286
|
+
if (!outdated) {
|
|
3287
|
+
spinnerFail("pnpm outdated gave no answer: run pnpm -r outdated to see why");
|
|
3288
|
+
console.info(`${warningMark} security pass skipped: it runs after the update check`);
|
|
3289
|
+
return;
|
|
3562
3290
|
}
|
|
3563
|
-
const
|
|
3291
|
+
const outdatedPackages = outdated;
|
|
3564
3292
|
const packageNames = Object.keys(outdatedPackages);
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3293
|
+
if (packageNames.length === 0) {
|
|
3294
|
+
spinnerSuccess("all packages are up to date");
|
|
3295
|
+
console.info();
|
|
3296
|
+
await runSecurityPass(forkPath, { apply: !config.list });
|
|
3568
3297
|
return;
|
|
3569
3298
|
}
|
|
3570
|
-
const
|
|
3571
|
-
|
|
3299
|
+
const advisories = auditResult ? advisoriesFromAudit(auditResult) : [];
|
|
3300
|
+
const severityOf = (name) => SEVERITIES.find((severity) => advisories.some((a) => a.module === name && a.severity === severity)) ?? null;
|
|
3572
3301
|
const readDependentPkg = createDependentPkgReader();
|
|
3573
|
-
const
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
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);
|
|
3578
3310
|
}
|
|
3579
3311
|
}
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
const spec = readDependentPkg(d.location)?.[pkg.dependencyType]?.[name] || "";
|
|
3594
|
-
if (spec && !spec.startsWith("^") && !spec.startsWith("~") && !spec.startsWith(">")) {
|
|
3595
|
-
pinnedIn.push(d.name);
|
|
3596
|
-
}
|
|
3597
|
-
}
|
|
3598
|
-
return {
|
|
3599
|
-
name,
|
|
3600
|
-
current: pkg.current,
|
|
3601
|
-
latest: pkg.latest,
|
|
3602
|
-
dependents: pkg.dependentPackages.map((d) => d.name),
|
|
3603
|
-
dependentLocations: pkg.dependentPackages.map((d) => d.location),
|
|
3604
|
-
isDev: pkg.dependencyType === "devDependencies",
|
|
3605
|
-
isMajorUpdate: isMajorVersionChange(pkg.current, pkg.latest),
|
|
3606
|
-
pinnedIn,
|
|
3607
|
-
repoUrl,
|
|
3608
|
-
changelogUrl,
|
|
3609
|
-
releasesUrl: getReleasesUrl(repoUrl),
|
|
3610
|
-
vulnerabilities: vulnMap.get(name) || []
|
|
3611
|
-
};
|
|
3612
|
-
})
|
|
3613
|
-
);
|
|
3614
|
-
enhancedPackages.push(...results);
|
|
3615
|
-
}
|
|
3616
|
-
saveCache(cache);
|
|
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
|
+
});
|
|
3617
3325
|
enhancedPackages.sort((a, b) => {
|
|
3618
|
-
const
|
|
3619
|
-
const depB = formatDependents(b.dependents);
|
|
3620
|
-
const depCompare = depA.localeCompare(depB);
|
|
3326
|
+
const depCompare = dependentsText(a).localeCompare(dependentsText(b));
|
|
3621
3327
|
if (depCompare !== 0) return depCompare;
|
|
3622
3328
|
return a.name.localeCompare(b.name);
|
|
3623
3329
|
});
|
|
3624
3330
|
spinner.stop();
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3331
|
+
printOutdatedResults(enhancedPackages, forkPath);
|
|
3332
|
+
console.info();
|
|
3333
|
+
const outcome = config.list ? "skipped" : await promptForUpdates(enhancedPackages, forkPath);
|
|
3334
|
+
if (outcome === "cancelled") return;
|
|
3335
|
+
if (outcome === "failed") {
|
|
3336
|
+
console.info(`${warningMark} security pass skipped: it needs a workspace that installs`);
|
|
3337
|
+
return;
|
|
3632
3338
|
}
|
|
3339
|
+
await runSecurityPass(forkPath, { apply: !config.list });
|
|
3633
3340
|
} finally {
|
|
3634
3341
|
spinner.stop();
|
|
3635
3342
|
}
|
|
3636
3343
|
}
|
|
3637
|
-
|
|
3344
|
+
var dependentsText = (pkg) => formatDependents(pkg.dependentPackages.map((d) => d.name));
|
|
3345
|
+
function printOutdatedResults(enhancedPackages, forkPath) {
|
|
3638
3346
|
const DEV_TAG = " (dev)";
|
|
3639
3347
|
const MAX_NAME_LEN = 35;
|
|
3640
3348
|
const MAX_DEPENDENTS_LEN = 15;
|
|
@@ -3646,7 +3354,7 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3646
3354
|
const maxLatestLen = Math.max(6, ...enhancedPackages.map((p) => p.latest.length));
|
|
3647
3355
|
const maxDependentsLen = Math.min(
|
|
3648
3356
|
MAX_DEPENDENTS_LEN,
|
|
3649
|
-
Math.max(10, ...enhancedPackages.map((p) =>
|
|
3357
|
+
Math.max(10, ...enhancedPackages.map((p) => dependentsText(p).length))
|
|
3650
3358
|
);
|
|
3651
3359
|
const header = [
|
|
3652
3360
|
" ",
|
|
@@ -3658,20 +3366,15 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3658
3366
|
].join(" ");
|
|
3659
3367
|
console.info(header);
|
|
3660
3368
|
for (const pkg of enhancedPackages) {
|
|
3661
|
-
const
|
|
3662
|
-
const vulnIndicator = highestSeverity ? getVulnIcon(highestSeverity) : " ";
|
|
3369
|
+
const vulnIndicator = pkg.severity ? getVulnIcon(pkg.severity) : " ";
|
|
3663
3370
|
const fullName = pkg.isDev ? `${pkg.name}${DEV_TAG}` : pkg.name;
|
|
3664
3371
|
const displayName = middleTruncate(fullName, maxNameLen);
|
|
3665
3372
|
const name = colors_default.white(displayName.padEnd(maxNameLen));
|
|
3666
3373
|
const current = colors_default.red(pkg.current.padEnd(maxCurrentLen));
|
|
3667
3374
|
const latestText = pkg.latest.padEnd(maxLatestLen);
|
|
3668
3375
|
const latest = pkg.isMajorUpdate ? colors_default.bold(colors_default.green(latestText)) : colors_default.green(latestText);
|
|
3669
|
-
const
|
|
3670
|
-
const dependents = colors_default.dim(middleTruncate(dependentsText, maxDependentsLen).padEnd(maxDependentsLen));
|
|
3376
|
+
const dependents = colors_default.dim(middleTruncate(dependentsText(pkg), maxDependentsLen).padEnd(maxDependentsLen));
|
|
3671
3377
|
const links = [];
|
|
3672
|
-
if (pkg.changelogUrl) {
|
|
3673
|
-
links.push(hyperlink(colors_default.magenta("log"), pkg.changelogUrl));
|
|
3674
|
-
}
|
|
3675
3378
|
if (pkg.releasesUrl) {
|
|
3676
3379
|
links.push(hyperlink(colors_default.blue("rel"), pkg.releasesUrl));
|
|
3677
3380
|
}
|
|
@@ -3684,28 +3387,28 @@ function printOutdatedResults(enhancedPackages, dependentNameCache, forkPath) {
|
|
|
3684
3387
|
console.info();
|
|
3685
3388
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
3686
3389
|
for (const pkg of enhancedPackages) {
|
|
3687
|
-
for (const
|
|
3688
|
-
const dependentName =
|
|
3390
|
+
for (const { name, location } of pkg.dependentPackages) {
|
|
3391
|
+
const dependentName = name || basename2(location);
|
|
3689
3392
|
const existing = dependentMap.get(dependentName);
|
|
3690
3393
|
if (existing) {
|
|
3691
3394
|
existing.count++;
|
|
3692
3395
|
} else {
|
|
3693
|
-
dependentMap.set(dependentName, { location
|
|
3396
|
+
dependentMap.set(dependentName, { location, count: 1 });
|
|
3694
3397
|
}
|
|
3695
3398
|
}
|
|
3696
3399
|
}
|
|
3697
3400
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
3698
3401
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
3699
|
-
for (const [dependentName, { location, count }] of sortedDependents) {
|
|
3700
|
-
const packageJsonPath2 =
|
|
3701
|
-
const relativePath =
|
|
3402
|
+
for (const [dependentName, { location, count: count2 }] of sortedDependents) {
|
|
3403
|
+
const packageJsonPath2 = join10(location, "package.json");
|
|
3404
|
+
const relativePath = relative3(forkPath, packageJsonPath2);
|
|
3702
3405
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
3703
3406
|
console.info(
|
|
3704
|
-
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${
|
|
3407
|
+
` ${colors_default.dim("\u2022")} ${paddedName} ${hyperlink(relativePath, `file://${packageJsonPath2}`)} ${colors_default.dim(`${count2}`)}`
|
|
3705
3408
|
);
|
|
3706
3409
|
}
|
|
3707
3410
|
}
|
|
3708
|
-
function buildUpdateChoices(packages
|
|
3411
|
+
function buildUpdateChoices(packages) {
|
|
3709
3412
|
const makeChoice = (pkg) => {
|
|
3710
3413
|
const devTag = pkg.isDev ? colors_default.dim(" (dev)") : "";
|
|
3711
3414
|
const version = `${colors_default.red(pkg.current)} \u2192 ${colors_default.green(pkg.latest)}`;
|
|
@@ -3719,49 +3422,26 @@ function buildUpdateChoices(packages, auditResult, hasVulnerabilities) {
|
|
|
3719
3422
|
const pinned = packages.filter((p) => p.pinnedIn.length > 0);
|
|
3720
3423
|
const regular = packages.filter((p) => p.pinnedIn.length === 0);
|
|
3721
3424
|
const choices = [];
|
|
3722
|
-
if (hasVulnerabilities) {
|
|
3723
|
-
const parts = formatSeverityCounts(auditResult?.metadata?.vulnerabilities || {});
|
|
3724
|
-
const vulnSummary = parts.length > 0 ? ` ${colors_default.dim("\xB7")} ${parts.join(" ")}` : "";
|
|
3725
|
-
choices.push(new Separator(colors_default.red(`\u2500\u2500 vulnerabilities${vulnSummary} \u2500\u2500`)));
|
|
3726
|
-
choices.push({
|
|
3727
|
-
name: `${colors_default.cyan("pnpm audit --fix override")} ${colors_default.dim("add overrides for non-vulnerable versions")}`,
|
|
3728
|
-
value: AUDIT_FIX_VALUE,
|
|
3729
|
-
checked: false
|
|
3730
|
-
});
|
|
3731
|
-
}
|
|
3732
3425
|
if (pinned.length > 0) {
|
|
3733
3426
|
choices.push(new Separator(colors_default.yellow(`\u2500\u2500 pinned (${pinned.length}) \u2500\u2500`)));
|
|
3734
3427
|
choices.push(...pinned.map(makeChoice));
|
|
3735
3428
|
}
|
|
3736
3429
|
if (regular.length > 0) {
|
|
3737
|
-
if (pinned.length > 0
|
|
3738
|
-
choices.push(new Separator(colors_default.dim(`\u2500\u2500 packages (${regular.length}) \u2500\u2500`)));
|
|
3739
|
-
}
|
|
3430
|
+
if (pinned.length > 0) choices.push(new Separator(colors_default.dim(`\u2500\u2500 packages (${regular.length}) \u2500\u2500`)));
|
|
3740
3431
|
choices.push(...regular.map(makeChoice));
|
|
3741
3432
|
}
|
|
3742
3433
|
return choices;
|
|
3743
3434
|
}
|
|
3744
|
-
async function promptForUpdates(packages, forkPath
|
|
3745
|
-
const hasVulnerabilities = auditResult !== null && Object.values(auditResult.metadata?.vulnerabilities || {}).some((c) => c > 0);
|
|
3746
|
-
const hasPackages = packages.length > 0;
|
|
3747
|
-
let confirmMessage = "update packages?";
|
|
3748
|
-
if (hasVulnerabilities && hasPackages) {
|
|
3749
|
-
confirmMessage = "update packages and fix vulnerabilities?";
|
|
3750
|
-
} else if (hasVulnerabilities) {
|
|
3751
|
-
confirmMessage = "fix vulnerabilities?";
|
|
3752
|
-
}
|
|
3435
|
+
async function promptForUpdates(packages, forkPath) {
|
|
3753
3436
|
let wantsUpdate;
|
|
3754
3437
|
try {
|
|
3755
|
-
wantsUpdate = await
|
|
3756
|
-
message: confirmMessage,
|
|
3757
|
-
default: true
|
|
3758
|
-
});
|
|
3438
|
+
wantsUpdate = await confirm2({ message: "update packages?", default: true });
|
|
3759
3439
|
} catch {
|
|
3760
|
-
return;
|
|
3440
|
+
return "cancelled";
|
|
3761
3441
|
}
|
|
3762
|
-
if (!wantsUpdate) return;
|
|
3763
3442
|
console.info();
|
|
3764
|
-
|
|
3443
|
+
if (!wantsUpdate) return "skipped";
|
|
3444
|
+
const choices = buildUpdateChoices(packages);
|
|
3765
3445
|
let selected;
|
|
3766
3446
|
try {
|
|
3767
3447
|
selected = await checkbox({
|
|
@@ -3785,37 +3465,29 @@ async function promptForUpdates(packages, forkPath, auditResult) {
|
|
|
3785
3465
|
}
|
|
3786
3466
|
});
|
|
3787
3467
|
} catch {
|
|
3788
|
-
return;
|
|
3468
|
+
return "cancelled";
|
|
3789
3469
|
}
|
|
3790
|
-
|
|
3791
|
-
const selectedPackages = selected.filter((s) => s !== AUDIT_FIX_VALUE);
|
|
3792
|
-
await executeUpdates(packages, forkPath, selectedPackages, runAuditFix);
|
|
3470
|
+
return executeUpdates(packages, forkPath, selected);
|
|
3793
3471
|
}
|
|
3794
|
-
async function executeUpdates(packages, forkPath, selectedPackages
|
|
3795
|
-
const summaryParts = [];
|
|
3796
|
-
if (selectedPackages.length > 0) {
|
|
3797
|
-
summaryParts.push(`updating ${selectedPackages.length} package${selectedPackages.length !== 1 ? "s" : ""}`);
|
|
3798
|
-
}
|
|
3799
|
-
if (runAuditFix) {
|
|
3800
|
-
summaryParts.push("then updating dependencies");
|
|
3801
|
-
}
|
|
3472
|
+
async function executeUpdates(packages, forkPath, selectedPackages) {
|
|
3802
3473
|
console.info();
|
|
3803
|
-
console.info(colors_default.green(`\u2713 ${
|
|
3474
|
+
console.info(colors_default.green(`\u2713 updating ${selectedPackages.length} package${selectedPackages.length !== 1 ? "s" : ""}`));
|
|
3804
3475
|
console.info();
|
|
3805
3476
|
const selectedMajor = packages.filter((p) => selectedPackages.includes(p.name) && p.isMajorUpdate);
|
|
3806
3477
|
if (selectedMajor.length > 0) {
|
|
3807
3478
|
const majorNames = selectedMajor.map((p) => colors_default.bold(p.name)).join(", ");
|
|
3808
3479
|
try {
|
|
3809
|
-
const proceed = await
|
|
3480
|
+
const proceed = await confirm2({
|
|
3810
3481
|
message: `${selectedMajor.length} major update${selectedMajor.length > 1 ? "s" : ""} selected (${majorNames}). proceed?`,
|
|
3811
3482
|
default: true
|
|
3812
3483
|
});
|
|
3813
3484
|
if (!proceed) {
|
|
3814
3485
|
console.info(colors_default.dim(" update cancelled"));
|
|
3815
|
-
|
|
3486
|
+
console.info();
|
|
3487
|
+
return "skipped";
|
|
3816
3488
|
}
|
|
3817
3489
|
} catch {
|
|
3818
|
-
return;
|
|
3490
|
+
return "cancelled";
|
|
3819
3491
|
}
|
|
3820
3492
|
}
|
|
3821
3493
|
const catalogPackages = readCatalogPackageNames(forkPath);
|
|
@@ -3823,194 +3495,49 @@ async function executeUpdates(packages, forkPath, selectedPackages, runAuditFix)
|
|
|
3823
3495
|
for (const pkgName of selectedPackages) {
|
|
3824
3496
|
const pkg = packages.find((p) => p.name === pkgName);
|
|
3825
3497
|
if (!pkg) continue;
|
|
3826
|
-
for (const dependent of pkg.
|
|
3498
|
+
for (const { name: dependent } of pkg.dependentPackages) {
|
|
3827
3499
|
const group = workspacePackages.get(dependent);
|
|
3828
3500
|
if (group) group.push(pkgName);
|
|
3829
3501
|
else workspacePackages.set(dependent, [pkgName]);
|
|
3830
3502
|
}
|
|
3831
3503
|
}
|
|
3832
|
-
if (runAuditFix) {
|
|
3833
|
-
console.info(colors_default.dim("running pnpm audit --fix override..."));
|
|
3834
|
-
const auditFixResult = spawnSync2("pnpm", ["audit", "--fix", "override"], {
|
|
3835
|
-
cwd: forkPath,
|
|
3836
|
-
stdio: "inherit"
|
|
3837
|
-
});
|
|
3838
|
-
console.info();
|
|
3839
|
-
if (auditFixResult.status === 0) {
|
|
3840
|
-
console.info(colors_default.green("\u2713 pnpm audit --fix override completed"));
|
|
3841
|
-
} else {
|
|
3842
|
-
console.error(colors_default.red(`\u2717 pnpm audit --fix override failed (exit code ${auditFixResult.status})`));
|
|
3843
|
-
}
|
|
3844
|
-
}
|
|
3845
3504
|
for (const [workspace, pkgs] of workspacePackages) {
|
|
3846
3505
|
const pkgList = pkgs.join(", ");
|
|
3847
3506
|
console.info(`${colors_default.cyan("\u2191")} ${colors_default.bold(workspace)}: ${colors_default.dim(pkgList)}`);
|
|
3848
3507
|
}
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
const normalizeResult = spawnSync2("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
3855
|
-
cwd: forkPath,
|
|
3856
|
-
stdio: "inherit"
|
|
3857
|
-
});
|
|
3858
|
-
console.info();
|
|
3859
|
-
if (normalizeResult.status !== 0) {
|
|
3860
|
-
console.error(colors_default.red(`\u2717 catalog normalization failed (exit code ${normalizeResult.status})`));
|
|
3861
|
-
return;
|
|
3862
|
-
}
|
|
3863
|
-
}
|
|
3864
|
-
const result = spawnSync2("pnpm", ["-r", "up", "--latest", ...selectedPackages], {
|
|
3508
|
+
console.info();
|
|
3509
|
+
const selectedCatalogPackages = selectedPackages.filter((pkgName) => catalogPackages.has(pkgName));
|
|
3510
|
+
if (selectedCatalogPackages.length > 0) {
|
|
3511
|
+
console.info(colors_default.dim(`normalizing catalog consumers: ${selectedCatalogPackages.join(", ")}`));
|
|
3512
|
+
const normalizeResult = spawnSync3("pnpm", ["-r", "up", ...selectedCatalogPackages], {
|
|
3865
3513
|
cwd: forkPath,
|
|
3866
3514
|
stdio: "inherit"
|
|
3867
3515
|
});
|
|
3868
3516
|
console.info();
|
|
3869
|
-
if (
|
|
3870
|
-
console.
|
|
3871
|
-
|
|
3872
|
-
console.error(colors_default.red(`\u2717 update failed (exit code ${result.status})`));
|
|
3517
|
+
if (normalizeResult.status !== 0) {
|
|
3518
|
+
console.error(colors_default.red(`\u2717 catalog normalization failed (exit code ${normalizeResult.status})`));
|
|
3519
|
+
return "failed";
|
|
3873
3520
|
}
|
|
3874
3521
|
}
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
if (
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
console.info(
|
|
3881
|
-
`${colors_default.red("\u26A0")} ${colors_default.bold("vulnerabilities")} ${colors_default.dim("\xB7")} ${formatSeverityCounts(vulnMeta, " ").join("")}`
|
|
3882
|
-
);
|
|
3883
|
-
console.info(DIVIDER);
|
|
3884
|
-
for (const [pkgName, vulns] of vulnMap.entries()) {
|
|
3885
|
-
for (const vuln of vulns) {
|
|
3886
|
-
const severityIcon = getVulnIcon(vuln.severity);
|
|
3887
|
-
const cveStr = vuln.cves.length > 0 ? colors_default.dim(` ${vuln.cves[0]}`) : "";
|
|
3888
|
-
let sourceStr = "";
|
|
3889
|
-
if (vuln.workspace) {
|
|
3890
|
-
sourceStr = colors_default.dim(` in ${vuln.workspace}`);
|
|
3891
|
-
if (vuln.directDependency) {
|
|
3892
|
-
sourceStr += colors_default.dim(` via ${vuln.directDependency}`);
|
|
3893
|
-
}
|
|
3894
|
-
}
|
|
3895
|
-
const title = middleTruncate(vuln.title, 60);
|
|
3896
|
-
console.info(
|
|
3897
|
-
`${severityIcon} ${colors_default.white(pkgName)} ${colors_default.dim(vuln.vulnerableVersions)}${sourceStr} ${colors_default.dim("\xB7")} ${title}${cveStr}`
|
|
3898
|
-
);
|
|
3899
|
-
}
|
|
3900
|
-
}
|
|
3901
|
-
} else {
|
|
3902
|
-
console.info();
|
|
3903
|
-
console.info(`${checkMark} no vulnerabilities found`);
|
|
3522
|
+
const result = spawnSync3("pnpm", ["-r", "up", "--latest", ...selectedPackages], { cwd: forkPath, stdio: "inherit" });
|
|
3523
|
+
console.info();
|
|
3524
|
+
if (result.status !== 0) {
|
|
3525
|
+
console.error(colors_default.red(`\u2717 update failed (exit code ${result.status})`));
|
|
3526
|
+
return "failed";
|
|
3904
3527
|
}
|
|
3528
|
+
console.info(colors_default.green(`\u2713 ${selectedPackages.length} package${selectedPackages.length > 1 ? "s" : ""} updated`));
|
|
3905
3529
|
console.info();
|
|
3530
|
+
return "updated";
|
|
3906
3531
|
}
|
|
3907
3532
|
function readCatalogPackageNames(forkPath) {
|
|
3908
|
-
const workspacePath =
|
|
3533
|
+
const workspacePath = join10(forkPath, "pnpm-workspace.yaml");
|
|
3909
3534
|
try {
|
|
3910
3535
|
const content = readFileSync3(workspacePath, "utf8");
|
|
3911
|
-
return new Set(Object.keys(
|
|
3536
|
+
return new Set(Object.keys(readTopLevel(content, "catalog") ?? {}));
|
|
3912
3537
|
} catch {
|
|
3913
3538
|
return /* @__PURE__ */ new Set();
|
|
3914
3539
|
}
|
|
3915
3540
|
}
|
|
3916
|
-
function readOverrides(forkPath) {
|
|
3917
|
-
const overrides = /* @__PURE__ */ new Map();
|
|
3918
|
-
const pkgJsonPath = join13(forkPath, "package.json");
|
|
3919
|
-
try {
|
|
3920
|
-
const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
|
|
3921
|
-
const pnpmOverrides = pkgJson.pnpm?.overrides || {};
|
|
3922
|
-
for (const [key, value] of Object.entries(pnpmOverrides)) {
|
|
3923
|
-
overrides.set(key, { target: value, source: "package.json" });
|
|
3924
|
-
}
|
|
3925
|
-
} catch {
|
|
3926
|
-
}
|
|
3927
|
-
const workspacePath = join13(forkPath, "pnpm-workspace.yaml");
|
|
3928
|
-
try {
|
|
3929
|
-
const content = readFileSync3(workspacePath, "utf8");
|
|
3930
|
-
const wsOverrides = parseYamlBlockMap(content, "overrides");
|
|
3931
|
-
for (const [key, value] of Object.entries(wsOverrides)) {
|
|
3932
|
-
overrides.set(key, { target: value, source: "pnpm-workspace.yaml" });
|
|
3933
|
-
}
|
|
3934
|
-
} catch {
|
|
3935
|
-
}
|
|
3936
|
-
return overrides;
|
|
3937
|
-
}
|
|
3938
|
-
async function checkOverrides(forkPath) {
|
|
3939
|
-
const spinner = createSpinner("checking overrides against current audit...");
|
|
3940
|
-
const overrides = readOverrides(forkPath);
|
|
3941
|
-
if (overrides.size === 0) {
|
|
3942
|
-
spinnerSuccess("no pnpm.overrides found");
|
|
3943
|
-
return;
|
|
3944
|
-
}
|
|
3945
|
-
const auditResult = await runPnpmAudit(forkPath);
|
|
3946
|
-
const activeAdvisoryPackages = /* @__PURE__ */ new Set();
|
|
3947
|
-
if (auditResult?.advisories) {
|
|
3948
|
-
for (const advisory of Object.values(auditResult.advisories)) {
|
|
3949
|
-
activeAdvisoryPackages.add(advisory.module_name);
|
|
3950
|
-
}
|
|
3951
|
-
}
|
|
3952
|
-
spinner.stop();
|
|
3953
|
-
const securityOverrides = [];
|
|
3954
|
-
const pinOverrides = [];
|
|
3955
|
-
for (const [key, { target, source }] of overrides) {
|
|
3956
|
-
const atIndex = key.indexOf("@", key.startsWith("@") ? 1 : 0);
|
|
3957
|
-
if (atIndex > 0) {
|
|
3958
|
-
const pkg = key.slice(0, atIndex);
|
|
3959
|
-
const hasActiveAdvisory = activeAdvisoryPackages.has(pkg);
|
|
3960
|
-
const status = hasActiveAdvisory ? "active" : "likely stale";
|
|
3961
|
-
securityOverrides.push({ key, pkg, target, source, status });
|
|
3962
|
-
} else {
|
|
3963
|
-
pinOverrides.push({ key, target, source });
|
|
3964
|
-
}
|
|
3965
|
-
}
|
|
3966
|
-
const allKeys = Array.from(overrides.keys());
|
|
3967
|
-
const allValues = Array.from(overrides.values());
|
|
3968
|
-
const maxKeyLen = Math.max(8, ...allKeys.map((k) => k.length));
|
|
3969
|
-
const maxTargetLen = Math.max(6, ...allValues.map((v) => v.target.length));
|
|
3970
|
-
if (securityOverrides.length > 0) {
|
|
3971
|
-
console.info(colors_default.bold(`security overrides (${securityOverrides.length})`));
|
|
3972
|
-
console.info(DIVIDER);
|
|
3973
|
-
for (const o of securityOverrides) {
|
|
3974
|
-
const icon = o.status === "active" ? colors_default.red("\u25CF") : colors_default.green("\u25CB");
|
|
3975
|
-
const statusText = o.status === "active" ? colors_default.red("active") : colors_default.green("likely stale");
|
|
3976
|
-
const keyText = colors_default.white(o.key.padEnd(maxKeyLen));
|
|
3977
|
-
const targetText = colors_default.dim(o.target.padEnd(maxTargetLen));
|
|
3978
|
-
const sourceText = colors_default.dim(o.source);
|
|
3979
|
-
console.info(` ${icon} ${keyText} \u2192 ${targetText} ${statusText} ${sourceText}`);
|
|
3980
|
-
}
|
|
3981
|
-
console.info();
|
|
3982
|
-
}
|
|
3983
|
-
if (pinOverrides.length > 0) {
|
|
3984
|
-
console.info(colors_default.bold(`version pins (${pinOverrides.length})`));
|
|
3985
|
-
console.info(DIVIDER);
|
|
3986
|
-
for (const o of pinOverrides) {
|
|
3987
|
-
const keyText = colors_default.white(o.key.padEnd(maxKeyLen));
|
|
3988
|
-
const targetText = colors_default.dim(o.target.padEnd(maxTargetLen));
|
|
3989
|
-
const sourceText = colors_default.dim(o.source);
|
|
3990
|
-
console.info(` ${colors_default.blue("\u25C6")} ${keyText} \u2192 ${targetText} ${sourceText}`);
|
|
3991
|
-
}
|
|
3992
|
-
console.info();
|
|
3993
|
-
}
|
|
3994
|
-
const staleCount = securityOverrides.filter((o) => o.status === "likely stale").length;
|
|
3995
|
-
const activeCount = securityOverrides.filter((o) => o.status === "active").length;
|
|
3996
|
-
if (staleCount > 0) {
|
|
3997
|
-
console.info(
|
|
3998
|
-
`${colors_default.green("\u25CB")} ${staleCount} override${staleCount !== 1 ? "s" : ""} likely stale: no matching advisory found in current audit`
|
|
3999
|
-
);
|
|
4000
|
-
console.info(colors_default.dim(" overrides mask vulnerabilities: verify by temporarily removing and running pnpm audit"));
|
|
4001
|
-
}
|
|
4002
|
-
if (activeCount > 0) {
|
|
4003
|
-
console.info(
|
|
4004
|
-
`${colors_default.red("\u25CF")} ${activeCount} override${activeCount !== 1 ? "s" : ""} still active: advisory present without override`
|
|
4005
|
-
);
|
|
4006
|
-
}
|
|
4007
|
-
if (pinOverrides.length > 0) {
|
|
4008
|
-
console.info(
|
|
4009
|
-
`${colors_default.blue("\u25C6")} ${pinOverrides.length} version pin${pinOverrides.length !== 1 ? "s" : ""}: not security-related, review manually`
|
|
4010
|
-
);
|
|
4011
|
-
}
|
|
4012
|
-
console.info();
|
|
4013
|
-
}
|
|
4014
3541
|
|
|
4015
3542
|
// src/services/contributions.ts
|
|
4016
3543
|
import {
|
|
@@ -4023,10 +3550,113 @@ import {
|
|
|
4023
3550
|
usePagination,
|
|
4024
3551
|
useState
|
|
4025
3552
|
} from "@inquirer/core";
|
|
4026
|
-
import { select as
|
|
3553
|
+
import { select as select3 } from "@inquirer/prompts";
|
|
3554
|
+
|
|
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
|
+
}
|
|
4027
3656
|
|
|
4028
3657
|
// src/services/contrib-core.ts
|
|
4029
|
-
import {
|
|
3658
|
+
import { rmSync as rmSync2 } from "fs";
|
|
3659
|
+
import { dirname as dirname6, join as join12, resolve as resolve4 } from "path";
|
|
4030
3660
|
async function listFilesAtRef(repoPath, ref) {
|
|
4031
3661
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
4032
3662
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -4042,10 +3672,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4042
3672
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
4043
3673
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
4044
3674
|
for (const f of baseFiles) {
|
|
4045
|
-
let dir =
|
|
3675
|
+
let dir = dirname6(f);
|
|
4046
3676
|
while (dir !== ".") {
|
|
4047
3677
|
baseDirs.add(dir);
|
|
4048
|
-
dir =
|
|
3678
|
+
dir = dirname6(dir);
|
|
4049
3679
|
}
|
|
4050
3680
|
}
|
|
4051
3681
|
const files = [];
|
|
@@ -4058,7 +3688,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4058
3688
|
if (inCella && !inFork) {
|
|
4059
3689
|
kind = "deleted";
|
|
4060
3690
|
} else if (!inCella && inFork) {
|
|
4061
|
-
if (!baseDirs.has(
|
|
3691
|
+
if (!baseDirs.has(dirname6(file.path))) continue;
|
|
4062
3692
|
kind = "created";
|
|
4063
3693
|
} else {
|
|
4064
3694
|
kind = "modified";
|
|
@@ -4067,15 +3697,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4067
3697
|
const changedTs = file.status === "diverged" ? file.upstreamChangedTs : file.changedTs;
|
|
4068
3698
|
files.push({ path: file.path, kind, status: file.status, changedAt, changedTs });
|
|
4069
3699
|
}
|
|
4070
|
-
return
|
|
4071
|
-
modified: files.filter((f) => f.kind === "modified").map((f) => f.path),
|
|
4072
|
-
created: files.filter((f) => f.kind === "created").map((f) => f.path),
|
|
4073
|
-
deleted: files.filter((f) => f.kind === "deleted").map((f) => f.path),
|
|
4074
|
-
files
|
|
4075
|
-
};
|
|
4076
|
-
}
|
|
4077
|
-
function countDetection(detection) {
|
|
4078
|
-
return detection.modified.length + detection.created.length + detection.deleted.length;
|
|
3700
|
+
return files;
|
|
4079
3701
|
}
|
|
4080
3702
|
async function readBlobInfo(repoPath, ref, filePath) {
|
|
4081
3703
|
const line = await git(["ls-tree", ref, "--", filePath], repoPath, { ignoreErrors: true });
|
|
@@ -4087,29 +3709,25 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
4087
3709
|
if (mode !== "100644" && mode !== "100755") return null;
|
|
4088
3710
|
return { mode, hash };
|
|
4089
3711
|
}
|
|
4090
|
-
async function buildContribBranch(repoPath, baseRef, forkRef,
|
|
3712
|
+
async function buildContribBranch(repoPath, baseRef, forkRef, files, forkName) {
|
|
4091
3713
|
const branch = `contrib/${forkName}`;
|
|
4092
|
-
const gitDir =
|
|
4093
|
-
const tmpIndex =
|
|
3714
|
+
const gitDir = resolve4(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
3715
|
+
const tmpIndex = join12(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
4094
3716
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
4095
3717
|
try {
|
|
4096
3718
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
4097
3719
|
const appliedFiles = [];
|
|
4098
|
-
for (const
|
|
4099
|
-
|
|
3720
|
+
for (const { path, kind } of files) {
|
|
3721
|
+
if (kind === "deleted") {
|
|
3722
|
+
await git(["update-index", "--remove", path], repoPath, { env: indexEnv, ignoreErrors: true });
|
|
3723
|
+
appliedFiles.push(`(deleted) ${path}`);
|
|
3724
|
+
continue;
|
|
3725
|
+
}
|
|
3726
|
+
const blob = await readBlobInfo(repoPath, forkRef, path);
|
|
4100
3727
|
if (!blob) continue;
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
{
|
|
4105
|
-
env: indexEnv
|
|
4106
|
-
}
|
|
4107
|
-
);
|
|
4108
|
-
appliedFiles.push(filePath);
|
|
4109
|
-
}
|
|
4110
|
-
for (const filePath of detection.deleted) {
|
|
4111
|
-
await git(["update-index", "--remove", filePath], repoPath, { env: indexEnv, ignoreErrors: true });
|
|
4112
|
-
appliedFiles.push(`(deleted) ${filePath}`);
|
|
3728
|
+
const cacheinfo = `${blob.mode},${blob.hash},${path}`;
|
|
3729
|
+
await git(["update-index", "--add", "--replace", "--cacheinfo", cacheinfo], repoPath, { env: indexEnv });
|
|
3730
|
+
appliedFiles.push(path);
|
|
4113
3731
|
}
|
|
4114
3732
|
if (appliedFiles.length === 0) {
|
|
4115
3733
|
return { branch, appliedFiles };
|
|
@@ -4124,29 +3742,25 @@ ${commitBody}`;
|
|
|
4124
3742
|
await git(["update-ref", `refs/heads/${branch}`, commitHash], repoPath);
|
|
4125
3743
|
return { branch, appliedFiles };
|
|
4126
3744
|
} finally {
|
|
4127
|
-
|
|
4128
|
-
const { unlinkSync: unlinkSync2 } = await import("fs");
|
|
4129
|
-
unlinkSync2(tmpIndex);
|
|
4130
|
-
} catch {
|
|
4131
|
-
}
|
|
3745
|
+
rmSync2(tmpIndex, { force: true });
|
|
4132
3746
|
}
|
|
4133
3747
|
}
|
|
4134
3748
|
|
|
4135
3749
|
// src/services/fork-utils.ts
|
|
4136
|
-
import { existsSync as
|
|
4137
|
-
import { dirname as
|
|
3750
|
+
import { existsSync as existsSync8 } from "fs";
|
|
3751
|
+
import { dirname as dirname7, resolve as resolve5 } from "path";
|
|
4138
3752
|
async function resolveForkBasePath(repoPath) {
|
|
4139
3753
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
4140
|
-
return commonDir ?
|
|
3754
|
+
return commonDir ? dirname7(resolve5(repoPath, commonDir)) : repoPath;
|
|
4141
3755
|
}
|
|
4142
3756
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
4143
|
-
const resolvedPath =
|
|
4144
|
-
if (!resolvedPath.startsWith(
|
|
3757
|
+
const resolvedPath = resolve5(basePath, fork.localPath);
|
|
3758
|
+
if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
|
|
4145
3759
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
4146
3760
|
}
|
|
4147
|
-
if (!
|
|
4148
|
-
if (!
|
|
4149
|
-
if (requireConfig && !
|
|
3761
|
+
if (!existsSync8(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
3762
|
+
if (!existsSync8(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
3763
|
+
if (requireConfig && !existsSync8(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
4150
3764
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
4151
3765
|
}
|
|
4152
3766
|
return { fork, resolvedPath, valid: true };
|
|
@@ -4158,13 +3772,8 @@ function printNoForksHint(action) {
|
|
|
4158
3772
|
}
|
|
4159
3773
|
|
|
4160
3774
|
// src/services/contributions.ts
|
|
4161
|
-
async function showContribDiff(item, baseRef, cwd, forkName) {
|
|
4162
|
-
const patch = gitDiffFile(cwd, `${baseRef}..${item.ref}`, item.path);
|
|
4163
|
-
if (patch.length === 0) return null;
|
|
4164
|
-
return openDiffInBrowser(patch.toString(), { filePath: item.path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
4165
|
-
}
|
|
4166
3775
|
var contribPrompt = createPrompt((config, done) => {
|
|
4167
|
-
const { pageSize = 20, baseRef, cwd, forkName } = config;
|
|
3776
|
+
const { pageSize = 20, baseRef, branch, cwd, forkName } = config;
|
|
4168
3777
|
const [items, setItems] = useState(config.items);
|
|
4169
3778
|
const [active, setActive] = useState(0);
|
|
4170
3779
|
const [statusMsg, setStatusMsg] = useState("");
|
|
@@ -4218,9 +3827,12 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4218
3827
|
return;
|
|
4219
3828
|
}
|
|
4220
3829
|
if (key.name === "d") {
|
|
3830
|
+
const { path } = items[active];
|
|
4221
3831
|
try {
|
|
4222
|
-
const
|
|
4223
|
-
|
|
3832
|
+
const patch = gitDiffFile(cwd, `${baseRef}..${branch}`, path);
|
|
3833
|
+
if (patch.length === 0) return setStatusMsg(`no changes for ${path}`);
|
|
3834
|
+
await openDiffInBrowser(patch.toString(), { filePath: path, srcLabel: "cella", dstLabel: forkName }, cwd);
|
|
3835
|
+
setStatusMsg(`opened ${path} in browser`);
|
|
4224
3836
|
} catch (error) {
|
|
4225
3837
|
setStatusMsg(`diff failed: ${errorMessage(error)}`);
|
|
4226
3838
|
}
|
|
@@ -4250,8 +3862,8 @@ var contribPrompt = createPrompt((config, done) => {
|
|
|
4250
3862
|
renderItem({ item, isActive }) {
|
|
4251
3863
|
const checkbox2 = item.checked ? colors_default.green("\u25CF") : colors_default.dim("\u25CB");
|
|
4252
3864
|
const cursor = isActive ? colors_default.cyan("\u276F") : " ";
|
|
4253
|
-
const newLabel = item.created ? colors_default.yellow(" \xB7 not in cella") : "";
|
|
4254
|
-
const delLabel = item.deleted ? colors_default.yellow(" [del]") : "";
|
|
3865
|
+
const newLabel = item.kind === "created" ? colors_default.yellow(" \xB7 not in cella") : "";
|
|
3866
|
+
const delLabel = item.kind === "deleted" ? colors_default.yellow(" [del]") : "";
|
|
4255
3867
|
const statusTag = item.status === "diverged" ? colors_default.yellow(" \u26A0 diverged") : "";
|
|
4256
3868
|
const dateLabel = item.changedAt ? colors_default.dim(` \xB7 ${item.changedAt}`) : "";
|
|
4257
3869
|
const line = `${cursor} ${checkbox2} ${item.path}${delLabel}${statusTag}${newLabel}${dateLabel}`;
|
|
@@ -4283,14 +3895,10 @@ async function selectFork(config, validated) {
|
|
|
4283
3895
|
}
|
|
4284
3896
|
return match;
|
|
4285
3897
|
}
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
if (validForks.length === 0)
|
|
4289
|
-
|
|
4290
|
-
}
|
|
4291
|
-
if (validForks.length > 1) {
|
|
4292
|
-
throw new Error("multiple forks configured; pass --fork <name> to choose one");
|
|
4293
|
-
}
|
|
3898
|
+
if (config.json) {
|
|
3899
|
+
const validForks = validated.filter((v) => v.valid);
|
|
3900
|
+
if (validForks.length === 0) throw new Error("no valid forks configured");
|
|
3901
|
+
if (validForks.length > 1) throw new Error("multiple forks configured; pass --fork <name> to choose one");
|
|
4294
3902
|
return validForks[0];
|
|
4295
3903
|
}
|
|
4296
3904
|
const choices = validated.map((v) => ({
|
|
@@ -4298,7 +3906,7 @@ async function selectFork(config, validated) {
|
|
|
4298
3906
|
name: v.valid ? `${v.fork.name} ${colors_default.dim(`[${v.fork.pullBranch}] ${v.fork.localPath}`)}` : `${v.fork.name} ${colors_default.dim(v.fork.localPath)}`,
|
|
4299
3907
|
disabled: v.valid ? false : v.error ?? "invalid"
|
|
4300
3908
|
}));
|
|
4301
|
-
const picked = await
|
|
3909
|
+
const picked = await select3({
|
|
4302
3910
|
message: "select fork to pull contributions from:",
|
|
4303
3911
|
choices,
|
|
4304
3912
|
loop: false
|
|
@@ -4307,10 +3915,6 @@ async function selectFork(config, validated) {
|
|
|
4307
3915
|
if (!selected) console.info(colors_default.dim("no fork selected."));
|
|
4308
3916
|
return selected;
|
|
4309
3917
|
}
|
|
4310
|
-
async function fetchForkBranch(cellaPath, fetchSource, pullBranch) {
|
|
4311
|
-
await git(["fetch", fetchSource, pullBranch], cellaPath);
|
|
4312
|
-
return git(["rev-parse", "FETCH_HEAD"], cellaPath);
|
|
4313
|
-
}
|
|
4314
3918
|
async function forkRefMeta(cellaPath, forkRef) {
|
|
4315
3919
|
const out = await git(["log", "-1", "--format=%h%x09%cr", forkRef], cellaPath, { ignoreErrors: true });
|
|
4316
3920
|
const [sha = forkRef.slice(0, 7), date = "unknown"] = out.split(" ");
|
|
@@ -4341,9 +3945,10 @@ async function runContributions(config) {
|
|
|
4341
3945
|
createSpinner("pulling fork contributions...");
|
|
4342
3946
|
const allItems = [];
|
|
4343
3947
|
let forkBanner = null;
|
|
3948
|
+
let branch = "";
|
|
4344
3949
|
try {
|
|
4345
|
-
|
|
4346
|
-
const forkRef = await
|
|
3950
|
+
await git(["fetch", fork.remoteUrl ?? resolvedPath, fork.pullBranch], config.forkPath);
|
|
3951
|
+
const forkRef = await git(["rev-parse", "FETCH_HEAD"], config.forkPath);
|
|
4347
3952
|
const meta = await forkRefMeta(config.forkPath, forkRef);
|
|
4348
3953
|
forkBanner = { pullBranch: fork.pullBranch, sha: meta.sha, date: meta.date };
|
|
4349
3954
|
let forkTerritory = [];
|
|
@@ -4352,29 +3957,16 @@ async function runContributions(config) {
|
|
|
4352
3957
|
forkTerritory = forkConfig.overrides?.ignored ?? [];
|
|
4353
3958
|
} catch {
|
|
4354
3959
|
}
|
|
4355
|
-
const
|
|
4356
|
-
if (
|
|
4357
|
-
const
|
|
4358
|
-
if (appliedFiles.length > 0) {
|
|
3960
|
+
const files = await detectContributableFiles(config.forkPath, baseRef, forkRef, config, forkTerritory);
|
|
3961
|
+
if (files.length > 0) {
|
|
3962
|
+
const built = await buildContribBranch(config.forkPath, baseRef, forkRef, files, forkName);
|
|
3963
|
+
if (built.appliedFiles.length > 0) {
|
|
3964
|
+
branch = built.branch;
|
|
4359
3965
|
const stat = await getDiffStat(config.forkPath, baseRef, branch);
|
|
4360
|
-
const
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
path,
|
|
4365
|
-
ref: branch,
|
|
4366
|
-
deleted,
|
|
4367
|
-
...deleted ? {} : { created: fileMeta?.kind === "created" },
|
|
4368
|
-
status: fileMeta?.status,
|
|
4369
|
-
changedAt: fileMeta?.changedAt,
|
|
4370
|
-
changedTs: fileMeta?.changedTs,
|
|
4371
|
-
additions: stat.get(path)?.additions ?? null,
|
|
4372
|
-
deletions: stat.get(path)?.deletions ?? null,
|
|
4373
|
-
checked: false
|
|
4374
|
-
};
|
|
4375
|
-
};
|
|
4376
|
-
for (const path of [...detection.modified, ...detection.created]) allItems.push(toItem(path, false));
|
|
4377
|
-
for (const path of detection.deleted) allItems.push(toItem(path, true));
|
|
3966
|
+
for (const file of files) {
|
|
3967
|
+
const { additions = null, deletions = null } = stat.get(file.path) ?? {};
|
|
3968
|
+
allItems.push({ ...file, additions, deletions, checked: false });
|
|
3969
|
+
}
|
|
4378
3970
|
}
|
|
4379
3971
|
}
|
|
4380
3972
|
} catch (error) {
|
|
@@ -4388,45 +3980,25 @@ async function runContributions(config) {
|
|
|
4388
3980
|
}
|
|
4389
3981
|
allItems.sort((a, b) => (b.changedTs ?? 0) - (a.changedTs ?? 0) || a.path.localeCompare(b.path));
|
|
4390
3982
|
spinnerSuccess(`${allItems.length} files from ${forkName}`);
|
|
4391
|
-
if (
|
|
3983
|
+
if (forkBanner) {
|
|
4392
3984
|
console.info(
|
|
4393
3985
|
` ${colors_default.dim(`${forkName}: comparing committed '${forkBanner.pullBranch}' @ ${forkBanner.sha} (${forkBanner.date}); uncommitted fork changes are not included`)}`
|
|
4394
3986
|
);
|
|
4395
3987
|
}
|
|
4396
|
-
if (config.diff) {
|
|
4397
|
-
const matches = allItems.filter((i) => i.path === config.diff);
|
|
4398
|
-
if (matches.length === 0) {
|
|
4399
|
-
throw new Error(`no contribution found for path: ${config.diff}`);
|
|
4400
|
-
}
|
|
4401
|
-
for (const item of matches) {
|
|
4402
|
-
const diff = gitDiffFile(config.forkPath, `${baseRef}..${item.ref}`, item.path, { dstPrefix: forkName });
|
|
4403
|
-
writeStdout(diff.toString());
|
|
4404
|
-
}
|
|
4405
|
-
return;
|
|
4406
|
-
}
|
|
4407
3988
|
if (config.json) {
|
|
4408
3989
|
const out = allItems.map((item) => ({
|
|
4409
3990
|
fork: forkName,
|
|
4410
3991
|
path: item.path,
|
|
4411
|
-
status: item.status
|
|
4412
|
-
kind: item.deleted ? "deleted" : "modified",
|
|
3992
|
+
status: item.status,
|
|
3993
|
+
kind: item.kind === "deleted" ? "deleted" : "modified",
|
|
4413
3994
|
changedAt: item.changedAt ?? null,
|
|
4414
3995
|
changedTs: item.changedTs ?? null,
|
|
4415
|
-
additions: item.additions
|
|
4416
|
-
deletions: item.deletions
|
|
3996
|
+
additions: item.additions,
|
|
3997
|
+
deletions: item.deletions
|
|
4417
3998
|
}));
|
|
4418
3999
|
writeStdout(JSON.stringify(out, null, 2));
|
|
4419
4000
|
return;
|
|
4420
4001
|
}
|
|
4421
|
-
if (config.list) {
|
|
4422
|
-
for (const item of allItems) {
|
|
4423
|
-
const status = item.status ?? "behind";
|
|
4424
|
-
const kind = item.deleted ? "deleted" : "modified";
|
|
4425
|
-
const changedAt = item.changedAt ?? "-";
|
|
4426
|
-
writeStdout(`${forkName} ${status} ${kind} ${changedAt} ${item.path}`);
|
|
4427
|
-
}
|
|
4428
|
-
return;
|
|
4429
|
-
}
|
|
4430
4002
|
console.info();
|
|
4431
4003
|
console.info(DIVIDER);
|
|
4432
4004
|
const selected = await contribPrompt({
|
|
@@ -4434,6 +4006,7 @@ async function runContributions(config) {
|
|
|
4434
4006
|
items: allItems,
|
|
4435
4007
|
forkName,
|
|
4436
4008
|
baseRef,
|
|
4009
|
+
branch,
|
|
4437
4010
|
cwd: config.forkPath,
|
|
4438
4011
|
pageSize: 20
|
|
4439
4012
|
});
|
|
@@ -4451,17 +4024,17 @@ async function runContributions(config) {
|
|
|
4451
4024
|
errors.push(`${item.path}: rejected (path traversal)`);
|
|
4452
4025
|
continue;
|
|
4453
4026
|
}
|
|
4454
|
-
if (item.deleted) {
|
|
4027
|
+
if (item.kind === "deleted") {
|
|
4455
4028
|
if (applyUnstaged) {
|
|
4456
4029
|
await removeFileFromWorktree(config.forkPath, item.path);
|
|
4457
4030
|
} else {
|
|
4458
|
-
await
|
|
4031
|
+
await gitRm(config.forkPath, [item.path]);
|
|
4459
4032
|
}
|
|
4460
4033
|
} else {
|
|
4461
4034
|
if (applyUnstaged) {
|
|
4462
|
-
await restoreWorktreeFromRef(config.forkPath,
|
|
4035
|
+
await restoreWorktreeFromRef(config.forkPath, branch, item.path);
|
|
4463
4036
|
} else {
|
|
4464
|
-
await
|
|
4037
|
+
await checkoutFromRef(config.forkPath, branch, [item.path]);
|
|
4465
4038
|
}
|
|
4466
4039
|
}
|
|
4467
4040
|
applied++;
|
|
@@ -4487,27 +4060,24 @@ async function runContributions(config) {
|
|
|
4487
4060
|
}
|
|
4488
4061
|
|
|
4489
4062
|
// src/services/forks.ts
|
|
4490
|
-
import { resolve as
|
|
4491
|
-
import
|
|
4492
|
-
import { Separator as Separator2, select as
|
|
4063
|
+
import { resolve as resolve6 } from "path";
|
|
4064
|
+
import process7 from "process";
|
|
4065
|
+
import { Separator as Separator2, select as select5 } from "@inquirer/prompts";
|
|
4493
4066
|
|
|
4494
4067
|
// src/services/sync.ts
|
|
4495
|
-
import { spawnSync as
|
|
4496
|
-
import { select as
|
|
4068
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
4069
|
+
import { select as select4 } from "@inquirer/prompts";
|
|
4497
4070
|
|
|
4498
4071
|
// src/utils/gh.ts
|
|
4499
|
-
import { spawnSync as
|
|
4072
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
4500
4073
|
function ghAvailable() {
|
|
4501
|
-
return
|
|
4074
|
+
return spawnSync4("gh", ["--version"], { stdio: "ignore" }).status === 0;
|
|
4502
4075
|
}
|
|
4503
4076
|
function runGh(cwd, args) {
|
|
4504
|
-
const result =
|
|
4077
|
+
const result = spawnSync4("gh", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
4505
4078
|
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
4506
4079
|
return { ok: result.status === 0, output };
|
|
4507
4080
|
}
|
|
4508
|
-
function filterSyncPrs(prs, prefix) {
|
|
4509
|
-
return prs.filter((pr) => pr.headRefName.startsWith(`${prefix}/`));
|
|
4510
|
-
}
|
|
4511
4081
|
function listOpenSyncPrs(cwd, prefix) {
|
|
4512
4082
|
const { ok, output } = runGh(cwd, [
|
|
4513
4083
|
"pr",
|
|
@@ -4526,27 +4096,22 @@ function listOpenSyncPrs(cwd, prefix) {
|
|
|
4526
4096
|
} catch {
|
|
4527
4097
|
return [];
|
|
4528
4098
|
}
|
|
4529
|
-
return
|
|
4530
|
-
}
|
|
4531
|
-
function buildMergeArgs(ref, options = {}) {
|
|
4532
|
-
const args = ["pr", "merge", ref, "--squash"];
|
|
4533
|
-
if (options.deleteBranch) args.push("--delete-branch");
|
|
4534
|
-
return args;
|
|
4099
|
+
return parsed.filter((pr) => pr.headRefName.startsWith(`${prefix}/`)).sort((a, b) => b.number - a.number);
|
|
4535
4100
|
}
|
|
4536
4101
|
function mergePrSquash(cwd, ref, options = {}) {
|
|
4537
|
-
return runGh(cwd,
|
|
4102
|
+
return runGh(cwd, ["pr", "merge", String(ref), "--squash", ...options.deleteBranch ? ["--delete-branch"] : []]);
|
|
4538
4103
|
}
|
|
4539
4104
|
function closePr(cwd, ref) {
|
|
4540
4105
|
return runGh(cwd, ["pr", "close", String(ref)]);
|
|
4541
4106
|
}
|
|
4542
4107
|
|
|
4543
4108
|
// src/services/packages.ts
|
|
4544
|
-
import { existsSync as
|
|
4545
|
-
import { join as
|
|
4109
|
+
import { existsSync as existsSync10, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
4110
|
+
import { join as join14 } from "path";
|
|
4546
4111
|
|
|
4547
4112
|
// src/services/package-usage.ts
|
|
4548
|
-
import { existsSync as
|
|
4549
|
-
import { join as
|
|
4113
|
+
import { existsSync as existsSync9, readFileSync as readFileSync4, statSync } from "fs";
|
|
4114
|
+
import { join as join13 } from "path";
|
|
4550
4115
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4551
4116
|
function escapeRegExp(value) {
|
|
4552
4117
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4562,7 +4127,7 @@ function readSearchable(filePath) {
|
|
|
4562
4127
|
}
|
|
4563
4128
|
function fileSource(forkPath, path) {
|
|
4564
4129
|
let text;
|
|
4565
|
-
return { path, label: path, text: () => text ??= readSearchable(
|
|
4130
|
+
return { path, label: path, text: () => text ??= readSearchable(join13(forkPath, path)) };
|
|
4566
4131
|
}
|
|
4567
4132
|
async function readScriptsAtRef(forkPath, ref, path) {
|
|
4568
4133
|
try {
|
|
@@ -4574,7 +4139,7 @@ async function readScriptsAtRef(forkPath, ref, path) {
|
|
|
4574
4139
|
async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
4575
4140
|
let forkScripts;
|
|
4576
4141
|
try {
|
|
4577
|
-
forkScripts = JSON.parse(readFileSync4(
|
|
4142
|
+
forkScripts = JSON.parse(readFileSync4(join13(forkPath, path), "utf8")).scripts ?? {};
|
|
4578
4143
|
} catch {
|
|
4579
4144
|
return [];
|
|
4580
4145
|
}
|
|
@@ -4584,9 +4149,9 @@ async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
|
4584
4149
|
}
|
|
4585
4150
|
function binNames(forkPath, location, name) {
|
|
4586
4151
|
const bareName = name.split("/").pop();
|
|
4587
|
-
for (const dir of [
|
|
4588
|
-
const manifest =
|
|
4589
|
-
if (!
|
|
4152
|
+
for (const dir of [join13(forkPath, location), forkPath]) {
|
|
4153
|
+
const manifest = join13(dir, "node_modules", name, "package.json");
|
|
4154
|
+
if (!existsSync9(manifest)) continue;
|
|
4590
4155
|
try {
|
|
4591
4156
|
const { bin } = JSON.parse(readFileSync4(manifest, "utf8"));
|
|
4592
4157
|
if (typeof bin === "string") return [bareName];
|
|
@@ -4641,7 +4206,7 @@ var DEPENDENCY_KEYS = [
|
|
|
4641
4206
|
"peerDependencies",
|
|
4642
4207
|
"optionalDependencies"
|
|
4643
4208
|
];
|
|
4644
|
-
function mergeRecord(forkRecord, upstreamRecord, baseRecord, { bumpHigher = true, findUse } = {}) {
|
|
4209
|
+
function mergeRecord(forkRecord, upstreamRecord, baseRecord, { bumpHigher = true, findUse, upstreamOrder = false } = {}) {
|
|
4645
4210
|
if (!upstreamRecord) return void 0;
|
|
4646
4211
|
const merged = { ...forkRecord || {} };
|
|
4647
4212
|
const base = baseRecord || {};
|
|
@@ -4675,9 +4240,26 @@ function mergeRecord(forkRecord, upstreamRecord, baseRecord, { bumpHigher = true
|
|
|
4675
4240
|
delete merged[name];
|
|
4676
4241
|
removed.push(name);
|
|
4677
4242
|
}
|
|
4678
|
-
const
|
|
4243
|
+
const keys = upstreamOrder ? orderLikeUpstream(Object.keys(merged), Object.keys(upstreamRecord)) : Object.keys(merged).sort((a, b) => a.localeCompare(b));
|
|
4244
|
+
const ordered = Object.fromEntries(keys.map((name) => [name, merged[name]]));
|
|
4679
4245
|
const changed = added.length + updated.length + removed.length > 0;
|
|
4680
|
-
return { merged:
|
|
4246
|
+
return { merged: ordered, changed, added, updated, removed, kept };
|
|
4247
|
+
}
|
|
4248
|
+
function orderLikeUpstream(forkKeys, upstreamKeys) {
|
|
4249
|
+
const upstream = new Set(upstreamKeys);
|
|
4250
|
+
const forkOnlyAfter = /* @__PURE__ */ new Map();
|
|
4251
|
+
let anchor;
|
|
4252
|
+
for (const key of forkKeys) {
|
|
4253
|
+
if (upstream.has(key)) anchor = key;
|
|
4254
|
+
else forkOnlyAfter.set(anchor, [...forkOnlyAfter.get(anchor) ?? [], key]);
|
|
4255
|
+
}
|
|
4256
|
+
const present = new Set(forkKeys);
|
|
4257
|
+
const ordered = [...forkOnlyAfter.get(void 0) ?? []];
|
|
4258
|
+
for (const key of upstreamKeys) {
|
|
4259
|
+
if (present.has(key)) ordered.push(key);
|
|
4260
|
+
ordered.push(...forkOnlyAfter.get(key) ?? []);
|
|
4261
|
+
}
|
|
4262
|
+
return ordered;
|
|
4681
4263
|
}
|
|
4682
4264
|
function isSubpathMap(value) {
|
|
4683
4265
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -4695,26 +4277,10 @@ function safeMergePnpm(forkPnpm, upstreamPnpm, basePnpm) {
|
|
|
4695
4277
|
if (!upstreamPnpm) return void 0;
|
|
4696
4278
|
const merged = { ...forkPnpm || {} };
|
|
4697
4279
|
let changed = false;
|
|
4698
|
-
|
|
4699
|
-
const result = mergeRecord(
|
|
4700
|
-
merged.overrides,
|
|
4701
|
-
upstreamPnpm.overrides,
|
|
4702
|
-
basePnpm?.overrides
|
|
4703
|
-
);
|
|
4704
|
-
if (result?.changed) {
|
|
4705
|
-
merged.overrides = result.merged;
|
|
4706
|
-
changed = true;
|
|
4707
|
-
}
|
|
4708
|
-
}
|
|
4709
|
-
if (upstreamPnpm.patchedDependencies) {
|
|
4710
|
-
const result = mergeRecord(
|
|
4711
|
-
merged.patchedDependencies,
|
|
4712
|
-
upstreamPnpm.patchedDependencies,
|
|
4713
|
-
basePnpm?.patchedDependencies,
|
|
4714
|
-
{ bumpHigher: false }
|
|
4715
|
-
);
|
|
4280
|
+
for (const sub of ["overrides", "patchedDependencies"]) {
|
|
4281
|
+
const result = mergeRecord(merged[sub], upstreamPnpm[sub], basePnpm?.[sub], { bumpHigher: sub === "overrides" });
|
|
4716
4282
|
if (result?.changed) {
|
|
4717
|
-
merged
|
|
4283
|
+
merged[sub] = result.merged;
|
|
4718
4284
|
changed = true;
|
|
4719
4285
|
}
|
|
4720
4286
|
}
|
|
@@ -4766,17 +4332,13 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4766
4332
|
Object.assign(pkg, Object.fromEntries(entries));
|
|
4767
4333
|
}
|
|
4768
4334
|
function readPackageJson(filePath) {
|
|
4769
|
-
if (!
|
|
4335
|
+
if (!existsSync10(filePath)) return null;
|
|
4770
4336
|
try {
|
|
4771
4337
|
return JSON.parse(readFileSync5(filePath, "utf8"));
|
|
4772
|
-
} catch {
|
|
4773
|
-
|
|
4338
|
+
} catch (error) {
|
|
4339
|
+
throw new Error(`cannot parse ${filePath}: ${errorMessage(error)}`);
|
|
4774
4340
|
}
|
|
4775
4341
|
}
|
|
4776
|
-
function writePackageJson(filePath, data) {
|
|
4777
|
-
writeFileSync4(filePath, `${JSON.stringify(data, null, 2)}
|
|
4778
|
-
`, "utf8");
|
|
4779
|
-
}
|
|
4780
4342
|
function packageJsonPath(relativePath) {
|
|
4781
4343
|
return relativePath ? `${relativePath}/package.json` : "package.json";
|
|
4782
4344
|
}
|
|
@@ -4804,15 +4366,15 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4804
4366
|
const changes = [];
|
|
4805
4367
|
const kept = [];
|
|
4806
4368
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4807
|
-
const pkgPath =
|
|
4369
|
+
const pkgPath = join14(forkPath, pkgRelPath);
|
|
4808
4370
|
const forkPkg = readPackageJson(pkgPath);
|
|
4809
4371
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4810
4372
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4811
4373
|
if (!upstream) return { updated: false, changes, kept };
|
|
4812
4374
|
if (!forkPkg) {
|
|
4813
|
-
const workspaceArrived = relativePath !== "" &&
|
|
4375
|
+
const workspaceArrived = relativePath !== "" && existsSync10(join14(forkPath, relativePath));
|
|
4814
4376
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4815
|
-
|
|
4377
|
+
writeFileSync3(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4816
4378
|
`, "utf8");
|
|
4817
4379
|
return { updated: true, changes: ["copied from upstream (new workspace)"], kept };
|
|
4818
4380
|
}
|
|
@@ -4836,18 +4398,12 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4836
4398
|
continue;
|
|
4837
4399
|
}
|
|
4838
4400
|
if (key === "packageManager") {
|
|
4839
|
-
const
|
|
4840
|
-
const
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
if (baseValue2) continue;
|
|
4844
|
-
forkPkg[key] = upstreamValue2;
|
|
4845
|
-
updated = true;
|
|
4846
|
-
changes.push(`${key}: added`);
|
|
4847
|
-
} else if (upstreamValue2 && forkValue2 && upstreamValue2 !== forkValue2 && (forkValue2 === baseValue2 || isHigherVersion(upstreamValue2, forkValue2))) {
|
|
4848
|
-
forkPkg[key] = upstreamValue2;
|
|
4401
|
+
const asRecord = (pkg) => pkg?.[key] ? { [key]: String(pkg[key]) } : void 0;
|
|
4402
|
+
const result2 = mergeRecord(asRecord(forkPkg), asRecord(upstreamPkg), asRecord(basePkg));
|
|
4403
|
+
if (result2?.changed) {
|
|
4404
|
+
forkPkg[key] = result2.merged[key];
|
|
4849
4405
|
updated = true;
|
|
4850
|
-
changes.push(`${key}: updated`);
|
|
4406
|
+
changes.push(`${key}: ${result2.added.length > 0 ? "added" : "updated"}`);
|
|
4851
4407
|
}
|
|
4852
4408
|
continue;
|
|
4853
4409
|
}
|
|
@@ -4866,7 +4422,12 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4866
4422
|
let findUse;
|
|
4867
4423
|
if (usage && key === "scripts") findUse = (name) => usage.findScript(name);
|
|
4868
4424
|
else if (usage && DEPENDENCY_KEYS.includes(key)) findUse = (name) => usage.findPackage(relativePath, name);
|
|
4869
|
-
const
|
|
4425
|
+
const isScripts = key === "scripts";
|
|
4426
|
+
const result = mergeRecord(forkValue, upstreamValue, baseValue, {
|
|
4427
|
+
bumpHigher: !isScripts,
|
|
4428
|
+
findUse,
|
|
4429
|
+
upstreamOrder: isScripts
|
|
4430
|
+
});
|
|
4870
4431
|
kept.push(...(result?.kept ?? []).map(({ name, usedAt }) => `${key}.${name}: used in ${usedAt}`));
|
|
4871
4432
|
if (result?.changed) {
|
|
4872
4433
|
forkPkg[key] = result.merged;
|
|
@@ -4878,9 +4439,8 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4878
4439
|
);
|
|
4879
4440
|
}
|
|
4880
4441
|
}
|
|
4881
|
-
if (updated) {
|
|
4882
|
-
|
|
4883
|
-
}
|
|
4442
|
+
if (updated) writeFileSync3(pkgPath, `${JSON.stringify(forkPkg, null, 2)}
|
|
4443
|
+
`, "utf8");
|
|
4884
4444
|
return { updated, changes, kept };
|
|
4885
4445
|
}
|
|
4886
4446
|
async function runPackages(config, options = {}) {
|
|
@@ -4903,32 +4463,25 @@ async function runPackages(config, options = {}) {
|
|
|
4903
4463
|
if (updated || kept2.length > 0) reports.push({ path: pkgRelPath, changes, kept: kept2 });
|
|
4904
4464
|
}
|
|
4905
4465
|
const changedCount = reports.filter(({ changes }) => changes.length > 0).length;
|
|
4906
|
-
spinnerSuccess(
|
|
4907
|
-
"package sync complete",
|
|
4908
|
-
`${changedCount} package.json${changedCount === 1 ? " was" : "s were"} changed`
|
|
4909
|
-
);
|
|
4466
|
+
spinnerSuccess("package sync complete", `${plural(changedCount, "package.json was", "package.jsons were")} changed`);
|
|
4910
4467
|
for (const { path, changes } of reports) {
|
|
4911
4468
|
if (changes.length === 0) continue;
|
|
4912
4469
|
console.info(` ${path}`);
|
|
4913
4470
|
for (const change of changes) console.info(` ${colors_default.dim(change)}`);
|
|
4914
4471
|
}
|
|
4915
4472
|
const kept = reports.flatMap(({ path, kept: kept2 }) => kept2.map((entry) => `${path} ${entry}`));
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
);
|
|
4928
|
-
for (const path of skipped) {
|
|
4929
|
-
console.info(` ${colors_default.dim("\u2192")} ${path}`);
|
|
4930
|
-
}
|
|
4931
|
-
}
|
|
4473
|
+
const them = kept.length === 1 ? "it" : "them";
|
|
4474
|
+
printWarnList(`kept ${plural(kept.length, "entry", "entries")} upstream dropped, the fork still uses ${them}:`, kept);
|
|
4475
|
+
printWarnList(
|
|
4476
|
+
`skipped ${skipped.length} conflicted package.json file(s): resolve the conflicts, commit the merge, then rerun \`pnpm cella sync\`:`,
|
|
4477
|
+
skipped
|
|
4478
|
+
);
|
|
4479
|
+
}
|
|
4480
|
+
function printWarnList(header, entries) {
|
|
4481
|
+
if (entries.length === 0) return;
|
|
4482
|
+
console.info();
|
|
4483
|
+
console.info(`${warningMark} ${colors_default.yellow(header)}`);
|
|
4484
|
+
for (const entry of entries) console.info(` ${colors_default.dim("\u2192")} ${entry}`);
|
|
4932
4485
|
}
|
|
4933
4486
|
|
|
4934
4487
|
// src/services/sync.ts
|
|
@@ -5018,10 +4571,13 @@ async function runSync(config, options) {
|
|
|
5018
4571
|
spinnerFail("sync completed with conflicts");
|
|
5019
4572
|
}
|
|
5020
4573
|
printEngineReports(result, "merge summary");
|
|
5021
|
-
|
|
5022
|
-
|
|
4574
|
+
if (config.unpinned) {
|
|
4575
|
+
console.info(colors_default.yellow("\u26A0 --unpinned: every pinned entry was merged from upstream; keep only what you want."));
|
|
4576
|
+
console.info();
|
|
4577
|
+
}
|
|
4578
|
+
const staged = result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
4579
|
+
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && staged ? options.stagedBranch : void 0;
|
|
5023
4580
|
printSyncComplete(result, { stagedBranch });
|
|
5024
|
-
printFlagWarnings({ hard: config.hard, unpinned: config.unpinned });
|
|
5025
4581
|
await printMigrationNotesLine(config, result);
|
|
5026
4582
|
return result;
|
|
5027
4583
|
}
|
|
@@ -5076,16 +4632,13 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
5076
4632
|
const fromSha = await readManifestBaseAtRef(forkPath, base) ?? await getMergeBase(forkPath, base, toSha).catch(() => null);
|
|
5077
4633
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
5078
4634
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
5079
|
-
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha,
|
|
5080
|
-
skip: totalCount > COMMIT_LIST_MAX ? totalCount - COMMIT_LIST_MAX : 0,
|
|
5081
|
-
limit: COMMIT_LIST_MAX
|
|
5082
|
-
}) : [];
|
|
4635
|
+
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, COMMIT_LIST_MAX) : [];
|
|
5083
4636
|
const notes = await readPrBodyNotes(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
5084
4637
|
return buildSyncPrBody({ repoSlug: manifest.upstream.repo, version, fromSha, toSha, commits, totalCount, notes });
|
|
5085
4638
|
}
|
|
5086
4639
|
async function readPrBodyNotes(forkPath, toSha, repoSlug) {
|
|
5087
4640
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5088
|
-
const [ids, pending] = await Promise.all([listNoteIds(forkPath, toSha), readPending(forkPath
|
|
4641
|
+
const [ids, pending] = await Promise.all([listNoteIds(forkPath, toSha), readPending(forkPath)]);
|
|
5089
4642
|
const open = await Promise.all(
|
|
5090
4643
|
pending.map(async (id) => ({
|
|
5091
4644
|
id,
|
|
@@ -5098,16 +4651,6 @@ async function readPrBodyNotes(forkPath, toSha, repoSlug) {
|
|
|
5098
4651
|
function printPrCreateStep(branch, base, title = SYNC_PR_TITLE) {
|
|
5099
4652
|
console.info(colors_default.dim(` gh pr create --base ${base} --head ${branch} --title "${title}" --fill`));
|
|
5100
4653
|
}
|
|
5101
|
-
function printShipSteps(temporaryBranch, base, title) {
|
|
5102
|
-
console.info(colors_default.dim(` git push -u origin ${temporaryBranch}`));
|
|
5103
|
-
printPrCreateStep(temporaryBranch, base, title);
|
|
5104
|
-
}
|
|
5105
|
-
function hasStagedSyncChanges(result) {
|
|
5106
|
-
return result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
5107
|
-
}
|
|
5108
|
-
function extractFirstUrl(output) {
|
|
5109
|
-
return output.match(/https?:\/\/\S+/)?.[0];
|
|
5110
|
-
}
|
|
5111
4654
|
async function flattenSyncBranch(forkPath, branch, base) {
|
|
5112
4655
|
const mergeCommits = await listBranchMergeCommits(forkPath, base);
|
|
5113
4656
|
if (mergeCommits.length === 0) return false;
|
|
@@ -5121,9 +4664,6 @@ async function flattenSyncBranch(forkPath, branch, base) {
|
|
|
5121
4664
|
await flattenBranch(forkPath, base, message);
|
|
5122
4665
|
return true;
|
|
5123
4666
|
}
|
|
5124
|
-
function indentLines(text) {
|
|
5125
|
-
return text.split("\n").map((line) => ` ${line}`).join("\n");
|
|
5126
|
-
}
|
|
5127
4667
|
async function shipSyncBranch(config, branch) {
|
|
5128
4668
|
const { forkPath, settings } = config;
|
|
5129
4669
|
const base = resolveReleaseBase(settings);
|
|
@@ -5131,27 +4671,28 @@ async function shipSyncBranch(config, branch) {
|
|
|
5131
4671
|
const flattened = await flattenSyncBranch(forkPath, branch, cutBase);
|
|
5132
4672
|
let prUrl;
|
|
5133
4673
|
let prOpened = false;
|
|
5134
|
-
const
|
|
5135
|
-
const prTitle =
|
|
4674
|
+
const upstreamCommit = (await readSyncManifest(forkPath))?.upstream.commit;
|
|
4675
|
+
const prTitle = upstreamCommit ? await buildSyncCommitMessage(forkPath, upstreamCommit) : SYNC_PR_TITLE;
|
|
5136
4676
|
console.info(colors_default.dim(`pushing '${branch}' to origin...`));
|
|
5137
4677
|
try {
|
|
5138
4678
|
await pushBranch(forkPath, "origin", branch, { forceWithLease: flattened });
|
|
5139
4679
|
} catch (error) {
|
|
5140
4680
|
console.info(colors_default.yellow(`push failed (${errorMessage(error).split("\n")[0]}). finish manually:`));
|
|
5141
|
-
|
|
4681
|
+
console.info(colors_default.dim(` git push -u origin ${branch}`));
|
|
4682
|
+
printPrCreateStep(branch, base, prTitle);
|
|
5142
4683
|
return;
|
|
5143
4684
|
}
|
|
5144
4685
|
if (ghAvailable()) {
|
|
5145
4686
|
console.info(colors_default.dim("opening a pull request..."));
|
|
5146
4687
|
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
5147
4688
|
const bodyArgs = prBody ? ["--body", prBody] : ["--fill"];
|
|
5148
|
-
const pr =
|
|
4689
|
+
const pr = spawnSync5("gh", ["pr", "create", "--base", base, "--head", branch, "--title", prTitle, ...bodyArgs], {
|
|
5149
4690
|
cwd: forkPath,
|
|
5150
4691
|
encoding: "utf8",
|
|
5151
4692
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5152
4693
|
});
|
|
5153
|
-
prUrl =
|
|
5154
|
-
${pr.stderr ?? ""}
|
|
4694
|
+
prUrl = `${pr.stdout ?? ""}
|
|
4695
|
+
${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
5155
4696
|
prOpened = pr.status === 0;
|
|
5156
4697
|
if (!prOpened) {
|
|
5157
4698
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
@@ -5176,7 +4717,7 @@ ${pr.stderr ?? ""}`);
|
|
|
5176
4717
|
function finalizeWorkspace(forkPath) {
|
|
5177
4718
|
for (const args of [["install"], ["check"]]) {
|
|
5178
4719
|
console.info(colors_default.dim(`running pnpm ${args.join(" ")}...`));
|
|
5179
|
-
const result =
|
|
4720
|
+
const result = spawnSync5("pnpm", args, { cwd: forkPath, stdio: "inherit" });
|
|
5180
4721
|
if (result.status !== 0) return false;
|
|
5181
4722
|
}
|
|
5182
4723
|
return true;
|
|
@@ -5240,13 +4781,10 @@ async function commitSyncMerge(config, branch) {
|
|
|
5240
4781
|
await commitSquash(forkPath, message);
|
|
5241
4782
|
console.info();
|
|
5242
4783
|
console.info(colors_default.green(`committed the sync on '${branch}' as '${message}'.`));
|
|
5243
|
-
printTriageSteps(branch);
|
|
5244
|
-
await printMigrationNotesLine(config);
|
|
5245
|
-
}
|
|
5246
|
-
function printTriageSteps(branch) {
|
|
5247
4784
|
console.info(colors_default.dim(`staying on '${branch}' without pushing.`));
|
|
5248
4785
|
console.info(colors_default.dim(" run drift triage (`pnpm cella analyze`), commit any follow-ups, then:"));
|
|
5249
4786
|
console.info(colors_default.dim(" pnpm cella sync (pushes the branch and opens the PR)"));
|
|
4787
|
+
await printMigrationNotesLine(config);
|
|
5250
4788
|
}
|
|
5251
4789
|
async function mergeOpenSyncPrs(config, open) {
|
|
5252
4790
|
const { forkPath, settings } = config;
|
|
@@ -5258,7 +4796,7 @@ async function mergeOpenSyncPrs(config, open) {
|
|
|
5258
4796
|
if (!merged.ok) {
|
|
5259
4797
|
console.info();
|
|
5260
4798
|
console.info(colors_default.yellow(`could not merge #${newest.number}: resolve it first, then re-run \`pnpm cella sync\`:`));
|
|
5261
|
-
if (merged.output) console.info(colors_default.dim(
|
|
4799
|
+
if (merged.output) console.info(colors_default.dim(merged.output.replace(/^/gm, " ")));
|
|
5262
4800
|
console.info(colors_default.dim(` ${newest.url}`));
|
|
5263
4801
|
return "cancel";
|
|
5264
4802
|
}
|
|
@@ -5287,7 +4825,7 @@ async function guardAgainstOpenSyncPr(config) {
|
|
|
5287
4825
|
}
|
|
5288
4826
|
console.info(colors_default.dim(` starting a new sync now produces a PR that re-includes ${many ? "these" : "those"} changes.`));
|
|
5289
4827
|
console.info();
|
|
5290
|
-
const choice = await
|
|
4828
|
+
const choice = await select4({
|
|
5291
4829
|
message: "how do you want to proceed?",
|
|
5292
4830
|
choices: [
|
|
5293
4831
|
{
|
|
@@ -5325,7 +4863,9 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5325
4863
|
await shipSyncBranch(config, currentBranch);
|
|
5326
4864
|
return;
|
|
5327
4865
|
}
|
|
5328
|
-
await
|
|
4866
|
+
if (!await isClean(forkPath)) {
|
|
4867
|
+
throw new Error("working directory has uncommitted changes. please commit or stash before syncing.");
|
|
4868
|
+
}
|
|
5329
4869
|
if (await guardAgainstOpenSyncPr(config) === "cancel") return;
|
|
5330
4870
|
const outcome = await runSyncCycle(config);
|
|
5331
4871
|
console.info();
|
|
@@ -5407,21 +4947,16 @@ async function syncFork(config, fork, forkPath) {
|
|
|
5407
4947
|
console.info(colors_default.dim(`path: ${forkPath}`));
|
|
5408
4948
|
console.info();
|
|
5409
4949
|
const forkConfig = await loadConfig(forkPath);
|
|
5410
|
-
|
|
5411
|
-
const upstreamRef = branchRef;
|
|
5412
|
-
const forkRuntimeConfig = {
|
|
4950
|
+
await runSyncCommand({
|
|
5413
4951
|
...forkConfig,
|
|
5414
4952
|
forkPath,
|
|
5415
|
-
upstreamRef,
|
|
4953
|
+
upstreamRef: resolveUpstream(forkConfig.settings).branchRef,
|
|
5416
4954
|
service: "sync",
|
|
5417
|
-
logFile: config.logFile,
|
|
5418
4955
|
list: false,
|
|
5419
4956
|
json: false,
|
|
5420
|
-
verbose:
|
|
5421
|
-
hard: config.hard,
|
|
4957
|
+
verbose: false,
|
|
5422
4958
|
keepConfig: config.keepConfig
|
|
5423
|
-
};
|
|
5424
|
-
await runSyncCommand(forkRuntimeConfig);
|
|
4959
|
+
});
|
|
5425
4960
|
}
|
|
5426
4961
|
async function runForks(config) {
|
|
5427
4962
|
const forks = config.forks ?? [];
|
|
@@ -5434,7 +4969,7 @@ async function runForks(config) {
|
|
|
5434
4969
|
if (!match) {
|
|
5435
4970
|
throw new Error(`fork '${config.fork}' not found in config`);
|
|
5436
4971
|
}
|
|
5437
|
-
const resolvedPath =
|
|
4972
|
+
const resolvedPath = resolve6(await resolveForkBasePath(config.forkPath), match.localPath);
|
|
5438
4973
|
await syncFork(config, match, resolvedPath);
|
|
5439
4974
|
return;
|
|
5440
4975
|
}
|
|
@@ -5442,15 +4977,15 @@ async function runForks(config) {
|
|
|
5442
4977
|
while (true) {
|
|
5443
4978
|
const choices = await buildForkChoices(forks, forkBasePath);
|
|
5444
4979
|
const forkChoices = [...choices, new Separator2(MENU_DIVIDER), { value: "_exit", name: colors_default.dim("exit") }];
|
|
5445
|
-
const selectedPath = await
|
|
4980
|
+
const selectedPath = await select5({
|
|
5446
4981
|
message: "select fork to sync:",
|
|
5447
4982
|
choices: forkChoices,
|
|
5448
4983
|
loop: false
|
|
5449
4984
|
});
|
|
5450
4985
|
if (selectedPath === "_exit") {
|
|
5451
|
-
|
|
4986
|
+
process7.exit(0);
|
|
5452
4987
|
}
|
|
5453
|
-
const resolvedForkPath =
|
|
4988
|
+
const resolvedForkPath = resolve6(forkBasePath, selectedPath);
|
|
5454
4989
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
5455
4990
|
if (!selectedFork) {
|
|
5456
4991
|
console.error(colors_default.red(`fork '${selectedPath}' not found in config`));
|
|
@@ -5463,16 +4998,12 @@ async function runForks(config) {
|
|
|
5463
4998
|
|
|
5464
4999
|
// src/services/stats.ts
|
|
5465
5000
|
import { readFile as readFile3 } from "fs/promises";
|
|
5466
|
-
import { join as
|
|
5001
|
+
import { join as join16 } from "path";
|
|
5467
5002
|
|
|
5468
5003
|
// src/utils/coverage-utils.ts
|
|
5469
|
-
import {
|
|
5470
|
-
import {
|
|
5471
|
-
import { join as join17, relative as relative3, sep as sep2 } from "path";
|
|
5004
|
+
import { existsSync as existsSync11, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
|
|
5005
|
+
import { join as join15, relative as relative4, sep as sep2 } from "path";
|
|
5472
5006
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5473
|
-
function summaryPath(forkPath) {
|
|
5474
|
-
return join17(forkPath, ".coverage", "coverage-summary.json");
|
|
5475
|
-
}
|
|
5476
5007
|
function colorPct(pct) {
|
|
5477
5008
|
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
5478
5009
|
const label = `${pct.toFixed(2)}%`;
|
|
@@ -5488,34 +5019,22 @@ function formatAge(ms) {
|
|
|
5488
5019
|
const mins = Math.floor(ms / (60 * 1e3));
|
|
5489
5020
|
return `${mins} minute${mins === 1 ? "" : "s"} ago`;
|
|
5490
5021
|
}
|
|
5491
|
-
function
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5022
|
+
function printCoverageSummary(forkPath) {
|
|
5023
|
+
const file = join15(forkPath, ".coverage", "coverage-summary.json");
|
|
5024
|
+
let summary;
|
|
5025
|
+
let mtimeMs = 0;
|
|
5026
|
+
let problem = "no coverage data found";
|
|
5027
|
+
if (existsSync11(file)) {
|
|
5028
|
+
try {
|
|
5029
|
+
mtimeMs = statSync2(file).mtimeMs;
|
|
5030
|
+
summary = JSON.parse(readFileSync6(file, "utf8"));
|
|
5031
|
+
} catch {
|
|
5032
|
+
problem = "could not read coverage-summary.json";
|
|
5502
5033
|
}
|
|
5503
|
-
console.info();
|
|
5504
|
-
}
|
|
5505
|
-
if (!existsSync12(file)) {
|
|
5506
|
-
console.info(colors_default.bold("coverage"));
|
|
5507
|
-
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to generate it"));
|
|
5508
|
-
console.info();
|
|
5509
|
-
return;
|
|
5510
5034
|
}
|
|
5511
|
-
|
|
5512
|
-
let mtimeMs;
|
|
5513
|
-
try {
|
|
5514
|
-
summary = JSON.parse(readFileSync6(file, "utf8"));
|
|
5515
|
-
mtimeMs = statSync2(file).mtimeMs;
|
|
5516
|
-
} catch {
|
|
5035
|
+
if (!summary || !Object.keys(summary).some((key) => key !== "total")) {
|
|
5517
5036
|
console.info(colors_default.bold("coverage"));
|
|
5518
|
-
console.info(colors_default.yellow(
|
|
5037
|
+
console.info(colors_default.yellow(` ${problem}: run \`pnpm test\` to generate it`));
|
|
5519
5038
|
console.info();
|
|
5520
5039
|
return;
|
|
5521
5040
|
}
|
|
@@ -5524,7 +5043,7 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5524
5043
|
const perPackage = /* @__PURE__ */ new Map();
|
|
5525
5044
|
for (const [key, entry] of Object.entries(summary)) {
|
|
5526
5045
|
if (key === "total") continue;
|
|
5527
|
-
const rel =
|
|
5046
|
+
const rel = relative4(forkPath, key);
|
|
5528
5047
|
if (rel.startsWith("..")) continue;
|
|
5529
5048
|
const pkg = rel.split(sep2)[0];
|
|
5530
5049
|
const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
|
|
@@ -5537,11 +5056,6 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5537
5056
|
if (stale) {
|
|
5538
5057
|
console.info(colors_default.yellow(" \u26A0 data is older than 7 days: run `pnpm test` to refresh"));
|
|
5539
5058
|
}
|
|
5540
|
-
if (!Object.keys(summary).some((key) => key !== "total")) {
|
|
5541
|
-
console.info(colors_default.yellow(" no coverage data found: run `pnpm test` to regenerate"));
|
|
5542
|
-
console.info();
|
|
5543
|
-
return;
|
|
5544
|
-
}
|
|
5545
5059
|
const total = summary.total;
|
|
5546
5060
|
console.info(
|
|
5547
5061
|
` ${colors_default.bold("overall")} lines ${colorPct(total.lines.pct)} ${colors_default.dim(`(${total.lines.covered}/${total.lines.total})`)} funcs ${colorPct(total.functions.pct)} branches ${colorPct(total.branches.pct)}`
|
|
@@ -5557,8 +5071,9 @@ function printCoverageSummary(forkPath, options = {}) {
|
|
|
5557
5071
|
// src/services/stats.ts
|
|
5558
5072
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
5559
5073
|
async function getWorkspacePackages(forkPath) {
|
|
5560
|
-
const content = await readFile3(
|
|
5561
|
-
|
|
5074
|
+
const content = await readFile3(join16(forkPath, "pnpm-workspace.yaml"), "utf8");
|
|
5075
|
+
const patterns = readTopLevel(content, "packages");
|
|
5076
|
+
return (Array.isArray(patterns) ? patterns.map(String) : []).map((pattern) => {
|
|
5562
5077
|
if (pattern.endsWith("/*")) {
|
|
5563
5078
|
const base = pattern.slice(0, -2);
|
|
5564
5079
|
return { name: `${base}/*`, prefix: `${base}/` };
|
|
@@ -5600,8 +5115,6 @@ function matchPackage(filePath, packages) {
|
|
|
5600
5115
|
var sourcePathspecs = [...sourceExtensions].map((ext) => `*.${ext}`);
|
|
5601
5116
|
async function collectStats(forkPath) {
|
|
5602
5117
|
const output = await git(["grep", "-c", "", "--", ...sourcePathspecs], forkPath, { ignoreErrors: true });
|
|
5603
|
-
const allFilesOutput = await git(["ls-files", "--cached"], forkPath);
|
|
5604
|
-
const allFilesCount = allFilesOutput.split("\n").filter(Boolean).length;
|
|
5605
5118
|
const packages = await getWorkspacePackages(forkPath);
|
|
5606
5119
|
const entries = [];
|
|
5607
5120
|
for (const line of output.split("\n")) {
|
|
@@ -5610,7 +5123,6 @@ async function collectStats(forkPath) {
|
|
|
5610
5123
|
if (sep3 === -1) continue;
|
|
5611
5124
|
entries.push({ path: line.slice(0, sep3), loc: Number.parseInt(line.slice(sep3 + 1), 10) });
|
|
5612
5125
|
}
|
|
5613
|
-
const skipped = allFilesCount - entries.length;
|
|
5614
5126
|
const emptyCat = () => ({
|
|
5615
5127
|
test: { files: 0, loc: 0 },
|
|
5616
5128
|
stories: { files: 0, loc: 0 },
|
|
@@ -5640,7 +5152,7 @@ async function collectStats(forkPath) {
|
|
|
5640
5152
|
const sortedPackages = Object.fromEntries(
|
|
5641
5153
|
[...pkgStats.entries()].sort((a, b) => b[1].categories.other.loc - a[1].categories.other.loc)
|
|
5642
5154
|
);
|
|
5643
|
-
return { total: entries.length, totalLoc,
|
|
5155
|
+
return { total: entries.length, totalLoc, categories, packages: sortedPackages };
|
|
5644
5156
|
}
|
|
5645
5157
|
function pad(n, width = 6) {
|
|
5646
5158
|
return String(n).padStart(width);
|
|
@@ -5649,9 +5161,6 @@ function formatLoc(n) {
|
|
|
5649
5161
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
5650
5162
|
return String(n);
|
|
5651
5163
|
}
|
|
5652
|
-
function formatLocColumn(n, width = 8) {
|
|
5653
|
-
return formatLoc(n).padStart(width);
|
|
5654
|
-
}
|
|
5655
5164
|
function fmtPct(value, total) {
|
|
5656
5165
|
const pct = (value / total * 100).toFixed(1);
|
|
5657
5166
|
return `${pct.padStart(5)}%`;
|
|
@@ -5672,34 +5181,20 @@ function printStats(stats, verbose) {
|
|
|
5672
5181
|
];
|
|
5673
5182
|
for (const [label, data] of catEntries) {
|
|
5674
5183
|
console.info(
|
|
5675
|
-
` ${pad(data.files)} ${colors_default.dim(fmtPct(data.files, stats.total))} ${label.padEnd(18)} ${colors_default.cyan(
|
|
5184
|
+
` ${pad(data.files)} ${colors_default.dim(fmtPct(data.files, stats.total))} ${label.padEnd(18)} ${colors_default.cyan(formatLoc(data.loc).padStart(8))} ${colors_default.dim(fmtPct(data.loc, stats.totalLoc))}`
|
|
5676
5185
|
);
|
|
5677
5186
|
}
|
|
5678
5187
|
console.info();
|
|
5679
5188
|
console.info(DIVIDER);
|
|
5680
5189
|
console.info();
|
|
5681
|
-
|
|
5682
|
-
|
|
5683
|
-
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
|
|
5687
|
-
|
|
5688
|
-
|
|
5689
|
-
console.info(
|
|
5690
|
-
` ${colors_default.dim(pad(srcFiles))} ${name.padEnd(18)} ${colors_default.cyan(formatLocColumn(srcLoc))} ${colors_default.dim(pad(c.test.files))} ${colors_default.dim(pad(c.stories.files))} ${colors_default.dim(pad(c.generated.files))} ${colors_default.dim(pad(c.json.files))} ${colors_default.dim(pad(srcFiles))}`
|
|
5691
|
-
);
|
|
5692
|
-
}
|
|
5693
|
-
} else {
|
|
5694
|
-
console.info(colors_default.dim(" source & config by package"));
|
|
5695
|
-
for (const [name, data] of Object.entries(stats.packages)) {
|
|
5696
|
-
const srcFiles = data.categories.other.files;
|
|
5697
|
-
const srcLoc = data.categories.other.loc;
|
|
5698
|
-
if (srcFiles === 0) continue;
|
|
5699
|
-
console.info(
|
|
5700
|
-
` ${pad(srcFiles)} ${colors_default.dim(fmtPct(srcFiles, stats.categories.other.files))} ${name.padEnd(18)} ${colors_default.cyan(formatLocColumn(srcLoc))} ${colors_default.dim(fmtPct(srcLoc, stats.categories.other.loc))}`
|
|
5701
|
-
);
|
|
5702
|
-
}
|
|
5190
|
+
const header = verbose ? ` ${"".padEnd(6)} ${"package".padEnd(18)} ${"loc".padStart(8)} ${"test".padStart(6)} ${"story".padStart(6)} ${"gen".padStart(6)} ${"json".padStart(6)} ${"src".padStart(6)}` : " source & config by package";
|
|
5191
|
+
console.info(colors_default.dim(header));
|
|
5192
|
+
const { other } = stats.categories;
|
|
5193
|
+
for (const [name, { categories: c }] of Object.entries(stats.packages)) {
|
|
5194
|
+
if (c.other.files === 0) continue;
|
|
5195
|
+
const head = verbose ? colors_default.dim(pad(c.other.files)) : `${pad(c.other.files)} ${colors_default.dim(fmtPct(c.other.files, other.files))}`;
|
|
5196
|
+
const tail = verbose ? ` ${[c.test, c.stories, c.generated, c.json, c.other].map((cat) => colors_default.dim(pad(cat.files))).join(" ")}` : ` ${colors_default.dim(fmtPct(c.other.loc, other.loc))}`;
|
|
5197
|
+
console.info(` ${head} ${name.padEnd(18)} ${colors_default.cyan(formatLoc(c.other.loc).padStart(8))}${tail}`);
|
|
5703
5198
|
}
|
|
5704
5199
|
console.info();
|
|
5705
5200
|
}
|
|
@@ -5710,19 +5205,11 @@ async function runStats(config) {
|
|
|
5710
5205
|
spinnerSuccess("counted raw lines of code");
|
|
5711
5206
|
printStats(stats, config.verbose);
|
|
5712
5207
|
console.info();
|
|
5713
|
-
printCoverageSummary(forkPath
|
|
5208
|
+
printCoverageSummary(forkPath);
|
|
5714
5209
|
}
|
|
5715
5210
|
|
|
5716
5211
|
// src/services/stats-since.ts
|
|
5717
|
-
var
|
|
5718
|
-
source: "source",
|
|
5719
|
-
test: "tests",
|
|
5720
|
-
stories: "stories",
|
|
5721
|
-
generated: "generated",
|
|
5722
|
-
json: "json",
|
|
5723
|
-
docs: "docs",
|
|
5724
|
-
other: "other"
|
|
5725
|
-
};
|
|
5212
|
+
var kindLabel = (kind) => kind === "test" ? "tests" : kind;
|
|
5726
5213
|
var docExtensions = /* @__PURE__ */ new Set(["md", "mdx"]);
|
|
5727
5214
|
var lockFiles = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"]);
|
|
5728
5215
|
var codeExtensions = [...sourceExtensions].filter((ext) => !ext.startsWith("json"));
|
|
@@ -5745,9 +5232,9 @@ function classifyLine(text) {
|
|
|
5745
5232
|
}
|
|
5746
5233
|
function parseNumstat(output) {
|
|
5747
5234
|
const files = [];
|
|
5748
|
-
for (const
|
|
5749
|
-
if (!
|
|
5750
|
-
const [added, removed, ...rest] =
|
|
5235
|
+
for (const record2 of output.split("\0")) {
|
|
5236
|
+
if (!record2) continue;
|
|
5237
|
+
const [added, removed, ...rest] = record2.split(" ");
|
|
5751
5238
|
const path = rest.join(" ");
|
|
5752
5239
|
if (!path) continue;
|
|
5753
5240
|
files.push({
|
|
@@ -5790,9 +5277,7 @@ function countLineContent(patch, isCounted) {
|
|
|
5790
5277
|
return content;
|
|
5791
5278
|
}
|
|
5792
5279
|
async function collectBranchStats(forkPath, since) {
|
|
5793
|
-
const sinceSha = await
|
|
5794
|
-
ignoreErrors: true
|
|
5795
|
-
});
|
|
5280
|
+
const sinceSha = await revParse(forkPath, `${since}^{commit}`);
|
|
5796
5281
|
if (!sinceSha) throw new Error(`unknown ref '${since}'. pass a branch, tag or commit, such as origin/main`);
|
|
5797
5282
|
const mergeBase = await git(["merge-base", sinceSha, "HEAD"], forkPath, { ignoreErrors: true });
|
|
5798
5283
|
if (!mergeBase) throw new Error(`the current branch shares no history with '${since}'`);
|
|
@@ -5867,9 +5352,7 @@ function formatMarkdown(stats) {
|
|
|
5867
5352
|
const lines = [
|
|
5868
5353
|
"| Kind | Files | Added | Removed | Net |",
|
|
5869
5354
|
"| --- | ---: | ---: | ---: | ---: |",
|
|
5870
|
-
...changedKinds(stats).map(
|
|
5871
|
-
([kind, change]) => row(kind === "json" ? "JSON" : capitalize(kindLabels[kind]), change)
|
|
5872
|
-
),
|
|
5355
|
+
...changedKinds(stats).map(([kind, change]) => row(kind === "json" ? "JSON" : capitalize(kindLabel(kind)), change)),
|
|
5873
5356
|
`| **Total** | **${stats.total.files}** | **${signed(stats.total.added)}** | **${signed(-stats.total.removed)}** | **${signed(net(stats.total))}** |`
|
|
5874
5357
|
];
|
|
5875
5358
|
const notes = [];
|
|
@@ -5886,7 +5369,7 @@ function printBranchStats(stats, verbose) {
|
|
|
5886
5369
|
console.info();
|
|
5887
5370
|
console.info(colors_default.dim(` ${"files".padStart(5)} ${"added".padStart(8)} ${"removed".padStart(8)} ${"net".padStart(8)}`));
|
|
5888
5371
|
for (const [kind, change] of changedKinds(stats)) {
|
|
5889
|
-
console.info(columns(String(change.files), change,
|
|
5372
|
+
console.info(columns(String(change.files), change, kindLabel(kind)));
|
|
5890
5373
|
}
|
|
5891
5374
|
console.info(columns(String(stats.total.files), stats.total, colors_default.bold("total")));
|
|
5892
5375
|
if (stats.sourceContent && stats.kinds.source.files > 0) {
|
|
@@ -5909,7 +5392,7 @@ function printBranchStats(stats, verbose) {
|
|
|
5909
5392
|
console.info(DIVIDER);
|
|
5910
5393
|
console.info();
|
|
5911
5394
|
for (const file of stats.files) {
|
|
5912
|
-
console.info(columns("", file, `${file.path} ${colors_default.dim(
|
|
5395
|
+
console.info(columns("", file, `${file.path} ${colors_default.dim(kindLabel(file.kind))}`));
|
|
5913
5396
|
}
|
|
5914
5397
|
}
|
|
5915
5398
|
if (stats.uncommitted > 0) {
|
|
@@ -5920,48 +5403,60 @@ function printBranchStats(stats, verbose) {
|
|
|
5920
5403
|
}
|
|
5921
5404
|
console.info();
|
|
5922
5405
|
}
|
|
5923
|
-
async function runBranchStats(
|
|
5406
|
+
async function runBranchStats(config, since) {
|
|
5924
5407
|
createSpinner("counting changed lines...");
|
|
5925
|
-
const stats = await collectBranchStats(forkPath,
|
|
5408
|
+
const stats = await collectBranchStats(config.forkPath, since);
|
|
5926
5409
|
spinnerSuccess(`Counted what ${stats.head} changed since ${stats.since} (${stats.mergeBase})`);
|
|
5927
5410
|
if (stats.total.files === 0) {
|
|
5928
5411
|
console.info(colors_default.dim(` nothing committed since ${stats.since}`));
|
|
5929
5412
|
return;
|
|
5930
5413
|
}
|
|
5931
|
-
if (
|
|
5414
|
+
if (config.md) {
|
|
5932
5415
|
writeStdout(formatMarkdown(stats));
|
|
5933
5416
|
if (stats.uncommitted > 0) console.info(colors_default.dim(`${stats.uncommitted} uncommitted file(s) are not counted`));
|
|
5934
5417
|
return;
|
|
5935
5418
|
}
|
|
5936
|
-
printBranchStats(stats,
|
|
5419
|
+
printBranchStats(stats, config.verbose);
|
|
5937
5420
|
}
|
|
5938
5421
|
|
|
5939
5422
|
// src/cella-cli.ts
|
|
5940
5423
|
function getForkPath() {
|
|
5941
5424
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5942
5425
|
if (envPath) {
|
|
5943
|
-
const resolved =
|
|
5944
|
-
if (!
|
|
5426
|
+
const resolved = resolve7(envPath);
|
|
5427
|
+
if (!existsSync12(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5945
5428
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5946
5429
|
}
|
|
5947
5430
|
return resolved;
|
|
5948
5431
|
}
|
|
5949
|
-
let cwd =
|
|
5432
|
+
let cwd = process8.cwd();
|
|
5950
5433
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
5951
|
-
cwd =
|
|
5434
|
+
cwd = resolve7(cwd, "../..");
|
|
5952
5435
|
}
|
|
5953
5436
|
return cwd;
|
|
5954
5437
|
}
|
|
5955
5438
|
function preflight(forkPath) {
|
|
5956
|
-
if (!
|
|
5439
|
+
if (!existsSync12(join17(forkPath, ".git"))) {
|
|
5957
5440
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5958
5441
|
}
|
|
5959
5442
|
}
|
|
5443
|
+
var services = {
|
|
5444
|
+
analyze: runAnalyze,
|
|
5445
|
+
sync: runSyncCommand,
|
|
5446
|
+
migrate: runMigrate,
|
|
5447
|
+
audit: runAudit,
|
|
5448
|
+
forks: runForks,
|
|
5449
|
+
contributions: runContributions,
|
|
5450
|
+
stats: async (config) => {
|
|
5451
|
+
if (config.md && !config.since) throw new Error("--md goes with --since <ref>");
|
|
5452
|
+
if (!config.since) return runStats(config);
|
|
5453
|
+
return runBranchStats(config, config.since);
|
|
5454
|
+
}
|
|
5455
|
+
};
|
|
5960
5456
|
function isHelpOrVersionRequest() {
|
|
5961
|
-
return
|
|
5457
|
+
return process8.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process8.argv[2] === "help";
|
|
5962
5458
|
}
|
|
5963
5459
|
async function main() {
|
|
5964
|
-
registerSignalHandlers();
|
|
5965
5460
|
try {
|
|
5966
5461
|
const forkPath = getForkPath();
|
|
5967
5462
|
if (isHelpOrVersionRequest()) {
|
|
@@ -5973,46 +5468,12 @@ async function main() {
|
|
|
5973
5468
|
if (!["audit", "forks", "contributions", "stats"].includes(config.service)) {
|
|
5974
5469
|
preflight(forkPath);
|
|
5975
5470
|
}
|
|
5976
|
-
|
|
5977
|
-
case "analyze": {
|
|
5978
|
-
await runAnalyze(config);
|
|
5979
|
-
break;
|
|
5980
|
-
}
|
|
5981
|
-
case "sync": {
|
|
5982
|
-
await runSyncCommand(config);
|
|
5983
|
-
break;
|
|
5984
|
-
}
|
|
5985
|
-
case "migrate": {
|
|
5986
|
-
await runMigrate(config);
|
|
5987
|
-
break;
|
|
5988
|
-
}
|
|
5989
|
-
case "audit": {
|
|
5990
|
-
await runAudit(config);
|
|
5991
|
-
break;
|
|
5992
|
-
}
|
|
5993
|
-
case "forks": {
|
|
5994
|
-
await runForks(config);
|
|
5995
|
-
break;
|
|
5996
|
-
}
|
|
5997
|
-
case "contributions": {
|
|
5998
|
-
await runContributions(config);
|
|
5999
|
-
break;
|
|
6000
|
-
}
|
|
6001
|
-
case "stats": {
|
|
6002
|
-
if (config.md && !config.since) throw new Error("--md goes with --since <ref>");
|
|
6003
|
-
if (config.since) {
|
|
6004
|
-
await runBranchStats(config.forkPath, { since: config.since, markdown: config.md, verbose: config.verbose });
|
|
6005
|
-
} else {
|
|
6006
|
-
await runStats(config);
|
|
6007
|
-
}
|
|
6008
|
-
break;
|
|
6009
|
-
}
|
|
6010
|
-
}
|
|
5471
|
+
await services[config.service](config);
|
|
6011
5472
|
console.info();
|
|
6012
5473
|
} catch (error) {
|
|
6013
5474
|
console.error();
|
|
6014
5475
|
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
6015
|
-
|
|
5476
|
+
process8.exit(1);
|
|
6016
5477
|
}
|
|
6017
5478
|
}
|
|
6018
5479
|
main();
|