@cellajs/cli 0.4.1 → 0.5.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 +114 -41
- package/dist/config.js.map +1 -1
- package/dist/index.js +1620 -861
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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 existsSync14, statSync as statSync3 } from "fs";
|
|
5
|
+
import { join as join19, resolve as resolve7 } from "path";
|
|
6
|
+
import process11 from "process";
|
|
7
7
|
|
|
8
8
|
// src/cli.ts
|
|
9
|
-
import
|
|
9
|
+
import process6 from "process";
|
|
10
10
|
import { select } from "@inquirer/prompts";
|
|
11
11
|
import { Command } from "commander";
|
|
12
12
|
|
|
@@ -213,13 +213,13 @@ ${z2.prettifyError(result.error)}`);
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
// src/utils/display.ts
|
|
216
|
-
import { resolve as
|
|
217
|
-
import
|
|
216
|
+
import { resolve as resolve3 } from "path";
|
|
217
|
+
import process5 from "process";
|
|
218
218
|
|
|
219
219
|
// package.json
|
|
220
220
|
var package_default = {
|
|
221
221
|
name: "@cellajs/cli",
|
|
222
|
-
version: "0.
|
|
222
|
+
version: "0.5.0",
|
|
223
223
|
private: false,
|
|
224
224
|
license: "MIT",
|
|
225
225
|
description: "CLI for managing your monorepo and syncing with upstream Cella.",
|
|
@@ -342,489 +342,131 @@ var package_default = {
|
|
|
342
342
|
}
|
|
343
343
|
};
|
|
344
344
|
|
|
345
|
-
// src/utils/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
function writeStdout(text) {
|
|
365
|
-
process4.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
366
|
-
`);
|
|
367
|
-
}
|
|
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);
|
|
372
|
-
console.info();
|
|
373
|
-
console.info(`${colors_default.cyan(left)}${colors_default.dim(` \xB7 v${VERSION}`)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`);
|
|
374
|
-
console.info(DIVIDER);
|
|
375
|
-
console.info();
|
|
376
|
-
}
|
|
377
|
-
function createSpinner(text) {
|
|
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);
|
|
345
|
+
// src/utils/git.ts
|
|
346
|
+
import { execFile } from "child_process";
|
|
347
|
+
import { existsSync as existsSync3 } from "fs";
|
|
348
|
+
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
349
|
+
import { dirname as dirname2, join as join4, resolve as resolve2 } from "path";
|
|
350
|
+
import { promisify } from "util";
|
|
351
|
+
|
|
352
|
+
// src/utils/manifest.ts
|
|
353
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
354
|
+
import { dirname, join as join3 } from "path";
|
|
355
|
+
var MANIFEST_FILE = "cella/cella.manifest.json";
|
|
356
|
+
function parseSyncManifest(raw) {
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(raw);
|
|
359
|
+
const commit = parsed?.upstream?.commit;
|
|
360
|
+
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
361
|
+
return parsed;
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
393
364
|
}
|
|
394
|
-
activeSpinner = spinner;
|
|
395
|
-
return spinner;
|
|
396
365
|
}
|
|
397
|
-
function
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
console.info(` ${colors_default.dim(detail)}`);
|
|
403
|
-
console.info();
|
|
366
|
+
async function readSyncManifest(cwd) {
|
|
367
|
+
try {
|
|
368
|
+
return parseSyncManifest(await readFile(join3(cwd, MANIFEST_FILE), "utf8"));
|
|
369
|
+
} catch {
|
|
370
|
+
return null;
|
|
404
371
|
}
|
|
405
372
|
}
|
|
406
|
-
function
|
|
407
|
-
|
|
408
|
-
|
|
373
|
+
async function readManifestBase(cwd) {
|
|
374
|
+
const manifest = await readSyncManifest(cwd);
|
|
375
|
+
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
409
376
|
}
|
|
410
|
-
function
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
}
|
|
377
|
+
async function writeSyncManifest(cwd, manifest) {
|
|
378
|
+
const manifestPath = join3(cwd, MANIFEST_FILE);
|
|
379
|
+
await mkdir(dirname(manifestPath), { recursive: true });
|
|
380
|
+
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
381
|
+
`, "utf8");
|
|
414
382
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
383
|
+
|
|
384
|
+
// src/utils/git.ts
|
|
385
|
+
var execFileAsync = promisify(execFile);
|
|
386
|
+
var MAX_BUFFER = 50 * 1024 * 1024;
|
|
387
|
+
function redactUrlSecrets(text) {
|
|
388
|
+
return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
|
|
389
|
+
const protocol = match.startsWith("https") ? "https" : "http";
|
|
390
|
+
return `${protocol}://***@`;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
async function git(args, cwd, options = {}) {
|
|
394
|
+
const env = {
|
|
395
|
+
...getEnvSnapshot(),
|
|
396
|
+
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
397
|
+
...options.env
|
|
398
|
+
};
|
|
399
|
+
try {
|
|
400
|
+
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer: MAX_BUFFER });
|
|
401
|
+
return stdout.trim();
|
|
402
|
+
} catch (error) {
|
|
403
|
+
if (options.ignoreErrors) return "";
|
|
404
|
+
if (error instanceof Error) {
|
|
405
|
+
error.message = redactUrlSecrets(error.message);
|
|
406
|
+
}
|
|
407
|
+
throw error;
|
|
419
408
|
}
|
|
420
409
|
}
|
|
421
|
-
function
|
|
422
|
-
|
|
423
|
-
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
424
|
-
return `\x1B]8;;${safeUrl}\x07${label}\x1B]8;;\x07`;
|
|
410
|
+
async function revParse(cwd, ref) {
|
|
411
|
+
return await git(["rev-parse", "-q", "--verify", ref], cwd, { ignoreErrors: true }) || null;
|
|
425
412
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
const shown = commits.slice(0, maxShownCommits);
|
|
429
|
-
for (const commit of shown) {
|
|
430
|
-
const shortHash = commit.hash.slice(0, 7);
|
|
431
|
-
const commitUrl = upstreamGitHubUrl ? `${upstreamGitHubUrl}/commit/${commit.hash}` : void 0;
|
|
432
|
-
const hashLabel = hyperlink(shortHash, commitUrl);
|
|
433
|
-
lines.push(` ${hashLabel} "${commit.message}" (${commit.date})`);
|
|
434
|
-
}
|
|
435
|
-
if (commitCount > shown.length) {
|
|
436
|
-
lines.push(` ... and ${commitCount - shown.length} more`);
|
|
437
|
-
}
|
|
438
|
-
return lines.join("\n");
|
|
413
|
+
async function getCurrentBranch(cwd) {
|
|
414
|
+
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
439
415
|
}
|
|
440
|
-
function
|
|
441
|
-
|
|
442
|
-
console.info(title);
|
|
443
|
-
console.info(DIVIDER);
|
|
444
|
-
console.info();
|
|
416
|
+
async function switchBranch(cwd, branch) {
|
|
417
|
+
await git(["switch", branch], cwd);
|
|
445
418
|
}
|
|
446
|
-
function
|
|
447
|
-
|
|
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}`);
|
|
419
|
+
async function switchDetached(cwd, ref) {
|
|
420
|
+
await git(["switch", "--detach", ref], cwd);
|
|
456
421
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
if (autoMergedFiles.length === 0) {
|
|
460
|
-
return lines.join("\n");
|
|
461
|
-
}
|
|
462
|
-
lines.push("");
|
|
463
|
-
lines.push(` auto-merged files (${autoMergedFiles.length}):`);
|
|
464
|
-
const { forkPath } = linkOptions;
|
|
465
|
-
const shown = autoMergedFiles.slice(0, maxFiles);
|
|
466
|
-
for (const filePath of shown) {
|
|
467
|
-
lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve2(forkPath, filePath)}`) : filePath}`);
|
|
468
|
-
}
|
|
469
|
-
if (autoMergedFiles.length > shown.length) {
|
|
470
|
-
lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);
|
|
471
|
-
}
|
|
472
|
-
return lines.join("\n");
|
|
422
|
+
async function createBranchFrom(cwd, branch, startPoint) {
|
|
423
|
+
await git(["switch", "-c", branch, "--no-track", startPoint], cwd);
|
|
473
424
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
diverged: { icon: colors_default.magenta("\u21C5"), label: "diverged", color: colors_default.magenta, description: "both changed, will merge" },
|
|
477
|
-
drifted: { icon: colors_default.yellow("!"), label: "drifted", color: colors_default.yellow, description: "fork changed (at risk)" },
|
|
478
|
-
ahead: { icon: colors_default.blue("\u2191"), label: "ahead", color: colors_default.blue, description: "fork changed (protected)" },
|
|
479
|
-
local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file in cella folders" },
|
|
480
|
-
pinned: { icon: colors_default.green("\u2A00"), label: "pinned", color: colors_default.green, description: "both changed, fork wins" },
|
|
481
|
-
ignored: { icon: colors_default.gray("\u2A02"), label: "ignored", color: colors_default.gray },
|
|
482
|
-
identical: { icon: colors_default.gray("\u2713"), label: "identical", color: colors_default.gray, description: "no changes" },
|
|
483
|
-
deleted: { icon: colors_default.red("\u2717"), label: "deleted", color: colors_default.red },
|
|
484
|
-
renamed: { icon: colors_default.blue("\u2192"), label: "renamed", color: colors_default.blue }
|
|
485
|
-
};
|
|
486
|
-
var summaryStatusConfig = {
|
|
487
|
-
...statusConfig,
|
|
488
|
-
managed: {
|
|
489
|
-
icon: colors_default.cyan("\u25C7"),
|
|
490
|
-
label: "managed",
|
|
491
|
-
color: colors_default.cyan,
|
|
492
|
-
description: "package.jsons & cella/cella.config.ts handled separately"
|
|
493
|
-
},
|
|
494
|
-
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
495
|
-
};
|
|
496
|
-
function printSummary(summary, title = "summary") {
|
|
497
|
-
printSectionHeader(colors_default.cyan(title));
|
|
498
|
-
const maxCount = Math.max(...Object.values(summary));
|
|
499
|
-
const countWidth = String(maxCount).length + 1;
|
|
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
|
-
}
|
|
425
|
+
async function branchExists(cwd, branch) {
|
|
426
|
+
return await revParse(cwd, `refs/heads/${branch}`) !== null;
|
|
515
427
|
}
|
|
516
|
-
function
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
const
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
for (const file of shown) {
|
|
523
|
-
const icon = typeof options.icon === "string" ? options.icon : options.icon(file);
|
|
524
|
-
const commit = useUpstream ? file.upstreamCommit : file.changedCommit;
|
|
525
|
-
const date = useUpstream ? file.upstreamChangedAt : file.changedAt;
|
|
526
|
-
const dateInfo = formatFileDateInfo(file.path, commit, date, linkOptions);
|
|
527
|
-
console.info(` ${icon} ${file.path}${dateInfo}${options.suffix?.(file) ?? ""}`);
|
|
528
|
-
}
|
|
529
|
-
if (files.length > maxLines) {
|
|
530
|
-
console.info(colors_default.dim(` ... + ${files.length - maxLines} more`));
|
|
531
|
-
}
|
|
532
|
-
if (options.hint) {
|
|
533
|
-
console.info();
|
|
534
|
-
for (const line of Array.isArray(options.hint) ? options.hint : [options.hint]) {
|
|
535
|
-
console.info(colors_default.dim(` ${line}`));
|
|
536
|
-
}
|
|
428
|
+
async function getBranchWorktree(cwd, branch) {
|
|
429
|
+
const output = await git(["worktree", "list", "--porcelain"], cwd, { ignoreErrors: true });
|
|
430
|
+
let worktree = null;
|
|
431
|
+
for (const line of output.split("\n")) {
|
|
432
|
+
if (line.startsWith("worktree ")) worktree = line.slice("worktree ".length);
|
|
433
|
+
else if (line === `branch refs/heads/${branch}`) return worktree;
|
|
537
434
|
}
|
|
435
|
+
return null;
|
|
538
436
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
437
|
+
async function fastForwardBranch(cwd, branch, ref) {
|
|
438
|
+
await git(["fetch", "--quiet", ".", `${ref}:refs/heads/${branch}`], cwd);
|
|
541
439
|
}
|
|
542
|
-
function
|
|
543
|
-
|
|
544
|
-
const title = options?.title ?? `${config.icon} ${config.label}`;
|
|
545
|
-
printFileSection(filesWithStatus(files, status), title, linkOptions, { ...options, icon: config.icon });
|
|
440
|
+
async function deleteBranch(cwd, branch) {
|
|
441
|
+
await git(["branch", "-D", branch], cwd, { ignoreErrors: true });
|
|
546
442
|
}
|
|
547
|
-
function
|
|
548
|
-
|
|
443
|
+
async function pullFastForward(cwd) {
|
|
444
|
+
await git(["pull", "--ff-only"], cwd, { ignoreErrors: true, skipEditor: true });
|
|
549
445
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
446
|
+
async function getBranchUpstream(cwd, branch = "") {
|
|
447
|
+
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branch}@{upstream}`], cwd, {
|
|
448
|
+
ignoreErrors: true
|
|
449
|
+
});
|
|
450
|
+
return upstream || null;
|
|
553
451
|
}
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
452
|
+
async function getUpstreamStatus(cwd, branch) {
|
|
453
|
+
const local = branch ? `refs/heads/${branch}` : "HEAD";
|
|
454
|
+
const upstream = await getBranchUpstream(cwd, branch);
|
|
455
|
+
if (!upstream) return { upstream: null, ahead: 0, behind: 0 };
|
|
456
|
+
const remote = upstream.split("/")[0];
|
|
457
|
+
await git(["fetch", remote], cwd, { ignoreErrors: true, skipEditor: true });
|
|
458
|
+
const counts = await git(["rev-list", "--left-right", "--count", `${local}...${upstream}`], cwd, {
|
|
459
|
+
ignoreErrors: true
|
|
460
|
+
});
|
|
461
|
+
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
462
|
+
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
557
463
|
}
|
|
558
|
-
function
|
|
559
|
-
|
|
560
|
-
return n > 0 ? colors_default.yellow(` \xB7 ${plural(n, "upstream line")} absent`) : "";
|
|
464
|
+
async function pushBranch(cwd, remote, branch, options = {}) {
|
|
465
|
+
await git(["push", "-u", ...options.forceWithLease ? ["--force-with-lease"] : [], remote, branch], cwd);
|
|
561
466
|
}
|
|
562
|
-
function
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
title: colors_default.cyan("\u2193 behind on upstream")
|
|
566
|
-
});
|
|
567
|
-
printFileGroup(files, "ahead", linkOptions, {
|
|
568
|
-
title: `${colors_default.blue("\u2191 protected in fork")}`,
|
|
569
|
-
suffix: formatUpstreamLinesAbsent,
|
|
570
|
-
hint: [
|
|
571
|
-
"these files have fork changes but are protected (pinned); upstream did not change them since the last sync.",
|
|
572
|
-
"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."
|
|
573
|
-
]
|
|
574
|
-
});
|
|
575
|
-
printFileSection(protectedBehind, `${warningMark} ${colors_default.yellow("protected but behind upstream")}`, linkOptions, {
|
|
576
|
-
icon: protectionIcon,
|
|
577
|
-
suffix: formatUpstreamChangedLines,
|
|
578
|
-
dateSource: "upstream",
|
|
579
|
-
hint: PROTECTED_BEHIND_HINT
|
|
580
|
-
});
|
|
581
|
-
printFileGroup(files, "drifted", linkOptions, {
|
|
582
|
-
title: `${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
583
|
-
hint: "these files have fork changes but are not pinned or ignored."
|
|
584
|
-
});
|
|
585
|
-
printFileGroup(files, "diverged", linkOptions, {
|
|
586
|
-
title: `${colors_default.magenta("\u21C5 diverged")}`,
|
|
587
|
-
hint: "both fork and upstream changed.",
|
|
588
|
-
dateSource: "upstream"
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
function printSyncComplete(result, options = {}) {
|
|
592
|
-
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
593
|
-
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
594
|
-
const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
|
|
595
|
-
const conflicts = result.conflicts.length;
|
|
596
|
-
console.info();
|
|
597
|
-
if (options.stagedBranch) {
|
|
598
|
-
console.info(`${checkMark} sync merge staged on '${options.stagedBranch}'`);
|
|
599
|
-
console.info(
|
|
600
|
-
colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts. committing next...`)
|
|
601
|
-
);
|
|
602
|
-
} else {
|
|
603
|
-
console.info(`${checkMark} sync complete`);
|
|
604
|
-
console.info(colors_default.dim(` ${updated} files updated, ${merged} auto-merged, ${conflicts} conflicts`));
|
|
605
|
-
}
|
|
606
|
-
printProtectedConflicts(result);
|
|
607
|
-
console.info();
|
|
608
|
-
}
|
|
609
|
-
function printProtectedConflicts(result) {
|
|
610
|
-
const paths = result.protectedConflicts ?? [];
|
|
611
|
-
if (paths.length === 0) return;
|
|
612
|
-
const byPath = new Map(result.files.map((file) => [file.path, file]));
|
|
613
|
-
console.info();
|
|
614
|
-
console.info(
|
|
615
|
-
`${warningMark} ${colors_default.yellow(
|
|
616
|
-
`${plural(paths.length, "protected file kept", "protected files kept")} the fork version, dropping upstream changes:`
|
|
617
|
-
)}`
|
|
618
|
-
);
|
|
619
|
-
for (const path of paths) {
|
|
620
|
-
const file = byPath.get(path);
|
|
621
|
-
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
622
|
-
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
623
|
-
}
|
|
624
|
-
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
625
|
-
}
|
|
626
|
-
function plural(count3, singular, pluralForm = `${singular}s`) {
|
|
627
|
-
return `${count3} ${count3 === 1 ? singular : pluralForm}`;
|
|
628
|
-
}
|
|
629
|
-
function printIgnoredUpstreamChanges(result) {
|
|
630
|
-
const groups = result.ignoredUpstreamChanges ?? [];
|
|
631
|
-
if (groups.length === 0) return;
|
|
632
|
-
const total = groups.reduce((count3, group) => count3 + group.paths.length, 0);
|
|
633
|
-
console.info();
|
|
634
|
-
console.info(
|
|
635
|
-
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
636
|
-
);
|
|
637
|
-
for (const group of groups) {
|
|
638
|
-
const counts = [plural(group.paths.length, "file")];
|
|
639
|
-
if (group.added > 0) counts.push(`${group.added} new`);
|
|
640
|
-
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
641
|
-
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
642
|
-
}
|
|
643
|
-
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync: review and adopt what you need."));
|
|
644
|
-
}
|
|
645
|
-
function printUpstreamOverrideChanges(result) {
|
|
646
|
-
const report = result.upstreamOverrides;
|
|
647
|
-
if (!report) return;
|
|
648
|
-
console.info();
|
|
649
|
-
if (report.kind === "unreadable") {
|
|
650
|
-
const side = report.side === "base" ? "the last sync point" : "the incoming ref";
|
|
651
|
-
console.info(
|
|
652
|
-
colors_default.dim(`upstream sync config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
653
|
-
);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
const lines = [];
|
|
657
|
-
for (const list of ["pinned", "ignored", "packageJsonSync"]) {
|
|
658
|
-
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
659
|
-
for (const entry of report[list].removed) {
|
|
660
|
-
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
console.info(
|
|
664
|
-
`${warningMark} ${colors_default.yellow(`upstream changed its sync config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
665
|
-
);
|
|
666
|
-
for (const line of lines) console.info(line);
|
|
667
|
-
console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
|
|
668
|
-
}
|
|
669
|
-
function findMaskingPins(files) {
|
|
670
|
-
return files.filter(
|
|
671
|
-
(file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
672
|
-
);
|
|
673
|
-
}
|
|
674
|
-
function printMaskingPinWarning(files) {
|
|
675
|
-
const masking = findMaskingPins(files);
|
|
676
|
-
if (masking.length === 0) return;
|
|
677
|
-
console.info();
|
|
678
|
-
console.info(
|
|
679
|
-
`${warningMark} ${colors_default.yellow(
|
|
680
|
-
`${plural(masking.length, "pinned file matches", "pinned files match")} the previous upstream but changed upstream:`
|
|
681
|
-
)}`
|
|
682
|
-
);
|
|
683
|
-
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
684
|
-
for (const file of masking) {
|
|
685
|
-
console.info(colors_default.dim(` \u2A00 ${file.path}`));
|
|
686
|
-
}
|
|
687
|
-
console.info(
|
|
688
|
-
colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
|
|
689
|
-
);
|
|
690
|
-
console.info();
|
|
691
|
-
}
|
|
692
|
-
function printEngineReports(result, title) {
|
|
693
|
-
printSummary(result.summary, title);
|
|
694
|
-
printIgnoredUpstreamChanges(result);
|
|
695
|
-
printUpstreamOverrideChanges(result);
|
|
696
|
-
printMaskingPinWarning(result.files);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// src/utils/overrides.ts
|
|
700
|
-
import { existsSync as existsSync5 } from "fs";
|
|
701
|
-
import { join as join6 } from "path";
|
|
702
|
-
|
|
703
|
-
// src/utils/git.ts
|
|
704
|
-
import { execFile } from "child_process";
|
|
705
|
-
import { existsSync as existsSync3 } from "fs";
|
|
706
|
-
import { mkdir as mkdir2, readdir, rmdir, unlink } from "fs/promises";
|
|
707
|
-
import { dirname as dirname2, join as join4, resolve as resolve3 } from "path";
|
|
708
|
-
import { promisify } from "util";
|
|
709
|
-
|
|
710
|
-
// src/utils/manifest.ts
|
|
711
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
712
|
-
import { dirname, join as join3 } from "path";
|
|
713
|
-
var MANIFEST_FILE = "cella/cella.manifest.json";
|
|
714
|
-
function parseSyncManifest(raw) {
|
|
715
|
-
try {
|
|
716
|
-
const parsed = JSON.parse(raw);
|
|
717
|
-
const commit = parsed?.upstream?.commit;
|
|
718
|
-
if (typeof commit !== "string" || !/^[0-9a-f]{40}$/i.test(commit)) return null;
|
|
719
|
-
return parsed;
|
|
720
|
-
} catch {
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
async function readSyncManifest(cwd) {
|
|
725
|
-
try {
|
|
726
|
-
return parseSyncManifest(await readFile(join3(cwd, MANIFEST_FILE), "utf8"));
|
|
727
|
-
} catch {
|
|
728
|
-
return null;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
async function readManifestBase(cwd) {
|
|
732
|
-
const manifest = await readSyncManifest(cwd);
|
|
733
|
-
return manifest ? manifest.upstream.commit.toLowerCase() : null;
|
|
734
|
-
}
|
|
735
|
-
async function writeSyncManifest(cwd, manifest) {
|
|
736
|
-
const manifestPath = join3(cwd, MANIFEST_FILE);
|
|
737
|
-
await mkdir(dirname(manifestPath), { recursive: true });
|
|
738
|
-
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
739
|
-
`, "utf8");
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
// src/utils/git.ts
|
|
743
|
-
var execFileAsync = promisify(execFile);
|
|
744
|
-
var MAX_BUFFER = 50 * 1024 * 1024;
|
|
745
|
-
function redactUrlSecrets(text) {
|
|
746
|
-
return text.replace(/https?:\/\/[^@\s]+@/g, (match) => {
|
|
747
|
-
const protocol = match.startsWith("https") ? "https" : "http";
|
|
748
|
-
return `${protocol}://***@`;
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
async function git(args, cwd, options = {}) {
|
|
752
|
-
const env = {
|
|
753
|
-
...getEnvSnapshot(),
|
|
754
|
-
...options.skipEditor ? { GIT_EDITOR: "true" } : {},
|
|
755
|
-
...options.env
|
|
756
|
-
};
|
|
757
|
-
try {
|
|
758
|
-
const { stdout } = await execFileAsync("git", args, { cwd, env, maxBuffer: MAX_BUFFER });
|
|
759
|
-
return stdout.trim();
|
|
760
|
-
} catch (error) {
|
|
761
|
-
if (options.ignoreErrors) return "";
|
|
762
|
-
if (error instanceof Error) {
|
|
763
|
-
error.message = redactUrlSecrets(error.message);
|
|
764
|
-
}
|
|
765
|
-
throw error;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
async function revParse(cwd, ref) {
|
|
769
|
-
return await git(["rev-parse", "-q", "--verify", ref], cwd, { ignoreErrors: true }) || null;
|
|
770
|
-
}
|
|
771
|
-
async function getCurrentBranch(cwd) {
|
|
772
|
-
return git(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
|
|
773
|
-
}
|
|
774
|
-
async function switchBranch(cwd, branch) {
|
|
775
|
-
await git(["switch", branch], cwd);
|
|
776
|
-
}
|
|
777
|
-
async function switchDetached(cwd, ref) {
|
|
778
|
-
await git(["switch", "--detach", ref], cwd);
|
|
779
|
-
}
|
|
780
|
-
async function createBranchFrom(cwd, branch, startPoint) {
|
|
781
|
-
await git(["switch", "-c", branch, "--no-track", startPoint], cwd);
|
|
782
|
-
}
|
|
783
|
-
async function branchExists(cwd, branch) {
|
|
784
|
-
return await revParse(cwd, `refs/heads/${branch}`) !== null;
|
|
785
|
-
}
|
|
786
|
-
async function getBranchWorktree(cwd, branch) {
|
|
787
|
-
const output = await git(["worktree", "list", "--porcelain"], cwd, { ignoreErrors: true });
|
|
788
|
-
let worktree = null;
|
|
789
|
-
for (const line of output.split("\n")) {
|
|
790
|
-
if (line.startsWith("worktree ")) worktree = line.slice("worktree ".length);
|
|
791
|
-
else if (line === `branch refs/heads/${branch}`) return worktree;
|
|
792
|
-
}
|
|
793
|
-
return null;
|
|
794
|
-
}
|
|
795
|
-
async function fastForwardBranch(cwd, branch, ref) {
|
|
796
|
-
await git(["fetch", "--quiet", ".", `${ref}:refs/heads/${branch}`], cwd);
|
|
797
|
-
}
|
|
798
|
-
async function deleteBranch(cwd, branch) {
|
|
799
|
-
await git(["branch", "-D", branch], cwd, { ignoreErrors: true });
|
|
800
|
-
}
|
|
801
|
-
async function pullFastForward(cwd) {
|
|
802
|
-
await git(["pull", "--ff-only"], cwd, { ignoreErrors: true, skipEditor: true });
|
|
803
|
-
}
|
|
804
|
-
async function getBranchUpstream(cwd, branch = "") {
|
|
805
|
-
const upstream = await git(["rev-parse", "--abbrev-ref", "--symbolic-full-name", `${branch}@{upstream}`], cwd, {
|
|
806
|
-
ignoreErrors: true
|
|
807
|
-
});
|
|
808
|
-
return upstream || null;
|
|
809
|
-
}
|
|
810
|
-
async function getUpstreamStatus(cwd, branch) {
|
|
811
|
-
const local = branch ? `refs/heads/${branch}` : "HEAD";
|
|
812
|
-
const upstream = await getBranchUpstream(cwd, branch);
|
|
813
|
-
if (!upstream) return { upstream: null, ahead: 0, behind: 0 };
|
|
814
|
-
const remote = upstream.split("/")[0];
|
|
815
|
-
await git(["fetch", remote], cwd, { ignoreErrors: true, skipEditor: true });
|
|
816
|
-
const counts = await git(["rev-list", "--left-right", "--count", `${local}...${upstream}`], cwd, {
|
|
817
|
-
ignoreErrors: true
|
|
818
|
-
});
|
|
819
|
-
const [ahead, behind] = counts.split(/\s+/).map((n) => Number.parseInt(n, 10) || 0);
|
|
820
|
-
return { upstream, ahead: ahead ?? 0, behind: behind ?? 0 };
|
|
821
|
-
}
|
|
822
|
-
async function pushBranch(cwd, remote, branch, options = {}) {
|
|
823
|
-
await git(["push", "-u", ...options.forceWithLease ? ["--force-with-lease"] : [], remote, branch], cwd);
|
|
824
|
-
}
|
|
825
|
-
async function listBranchMergeCommits(cwd, baseRef) {
|
|
826
|
-
const out = await git(["rev-list", "--merges", "--first-parent", `${baseRef}..HEAD`], cwd, { ignoreErrors: true });
|
|
827
|
-
return out ? out.split("\n").filter(Boolean) : [];
|
|
467
|
+
async function listBranchMergeCommits(cwd, baseRef) {
|
|
468
|
+
const out = await git(["rev-list", "--merges", "--first-parent", `${baseRef}..HEAD`], cwd, { ignoreErrors: true });
|
|
469
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
828
470
|
}
|
|
829
471
|
async function flattenBranch(cwd, baseRef, message) {
|
|
830
472
|
const parent = await getMergeBase(cwd, baseRef, "HEAD");
|
|
@@ -840,7 +482,7 @@ async function commitSquash(cwd, message) {
|
|
|
840
482
|
await git(["commit", "-m", message], cwd, { skipEditor: true });
|
|
841
483
|
}
|
|
842
484
|
async function gitPath(cwd, file) {
|
|
843
|
-
return
|
|
485
|
+
return resolve2(cwd, await git(["rev-parse", "--git-path", file], cwd));
|
|
844
486
|
}
|
|
845
487
|
async function stageAll(cwd) {
|
|
846
488
|
await git(["add", "-A"], cwd);
|
|
@@ -848,6 +490,10 @@ async function stageAll(cwd) {
|
|
|
848
490
|
async function mergeInProgress(cwd) {
|
|
849
491
|
return await revParse(cwd, "MERGE_HEAD") !== null;
|
|
850
492
|
}
|
|
493
|
+
function splitPrNumber(subject) {
|
|
494
|
+
const match = /\s*\(#(\d+)\)\s*$/.exec(subject);
|
|
495
|
+
return match ? { subject: subject.slice(0, match.index), pr: Number(match[1]) } : { subject };
|
|
496
|
+
}
|
|
851
497
|
async function getShortSha(cwd, ref) {
|
|
852
498
|
return git(["rev-parse", "--short", ref], cwd);
|
|
853
499
|
}
|
|
@@ -920,16 +566,16 @@ async function countCommitsBetween(cwd, fromRef, toRef) {
|
|
|
920
566
|
return Number.parseInt(output.trim(), 10) || 0;
|
|
921
567
|
}
|
|
922
568
|
async function listCommitsBetween(cwd, fromRef, toRef, limit) {
|
|
923
|
-
const args = ["log", "--format=%H%x1f%s%x1f%ar", "--reverse", `${fromRef}..${toRef}`];
|
|
569
|
+
const args = ["log", "--format=%H%x1f%s%x1f%ar%x1f%ct", "--reverse", `${fromRef}..${toRef}`];
|
|
924
570
|
if (limit) args.push(`-n${limit}`);
|
|
925
571
|
const output = await git(args, cwd, { ignoreErrors: true });
|
|
926
572
|
if (!output) return [];
|
|
927
573
|
const commits = [];
|
|
928
574
|
for (const line of output.split("\n")) {
|
|
929
575
|
if (!line) continue;
|
|
930
|
-
const [hash, message, date] = line.split("");
|
|
576
|
+
const [hash, message, date, timestamp] = line.split("");
|
|
931
577
|
if (!hash || !message || !date) continue;
|
|
932
|
-
commits.push({ hash, message, date });
|
|
578
|
+
commits.push({ hash, message, date, timestamp: Number(timestamp) || void 0 });
|
|
933
579
|
}
|
|
934
580
|
return commits;
|
|
935
581
|
}
|
|
@@ -1231,36 +877,430 @@ async function getEffectiveMergeBase(cwd, headRef, upstreamRef) {
|
|
|
1231
877
|
if (recordedIsNewer && recorded !== gitBase) {
|
|
1232
878
|
return { base: recorded, isStale: true, storedRef: recorded };
|
|
1233
879
|
}
|
|
1234
|
-
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
880
|
+
return { base: gitBase, isStale: false, storedRef: recorded };
|
|
881
|
+
}
|
|
882
|
+
async function withTemporarySyncBaseGraft(cwd, headRef, baseSha, fn) {
|
|
883
|
+
const headSha = await revParse(cwd, headRef);
|
|
884
|
+
if (!headSha || !await commitObjectExists(cwd, baseSha)) return fn();
|
|
885
|
+
if (await isAncestor(cwd, baseSha, headSha)) return fn();
|
|
886
|
+
const existingReplacement = await git(["replace", "-l", headSha], cwd, { ignoreErrors: true });
|
|
887
|
+
if (existingReplacement) return fn();
|
|
888
|
+
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
889
|
+
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
890
|
+
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
891
|
+
try {
|
|
892
|
+
return await fn();
|
|
893
|
+
} finally {
|
|
894
|
+
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// src/utils/layout.ts
|
|
899
|
+
import process4 from "process";
|
|
900
|
+
import { stripVTControlCharacters } from "util";
|
|
901
|
+
function terminalColumns() {
|
|
902
|
+
const columns = process4.stdout.isTTY ? process4.stdout.columns : void 0;
|
|
903
|
+
return Math.max(60, columns || 80);
|
|
904
|
+
}
|
|
905
|
+
function visibleWidth(text) {
|
|
906
|
+
return stripVTControlCharacters(text).length;
|
|
907
|
+
}
|
|
908
|
+
function padVisible(text, width, align = "start") {
|
|
909
|
+
const padding = " ".repeat(Math.max(0, width - visibleWidth(text)));
|
|
910
|
+
return align === "start" ? `${text}${padding}` : `${padding}${text}`;
|
|
911
|
+
}
|
|
912
|
+
function fitText(text, width) {
|
|
913
|
+
if (text.length <= width) return text;
|
|
914
|
+
return `${text.slice(0, Math.max(0, width - 1)).trimEnd()}\u2026`;
|
|
915
|
+
}
|
|
916
|
+
var AGE_STEPS = [
|
|
917
|
+
[90 * 60, 60, "m"],
|
|
918
|
+
[36 * 3600, 3600, "h"],
|
|
919
|
+
[14 * 86400, 86400, "d"],
|
|
920
|
+
[70 * 86400, 7 * 86400, "w"],
|
|
921
|
+
[365 * 86400, 30 * 86400, "mo"]
|
|
922
|
+
];
|
|
923
|
+
function shortAge(epochSeconds, nowMs = Date.now()) {
|
|
924
|
+
const seconds = Math.max(0, Math.round(nowMs / 1e3 - epochSeconds));
|
|
925
|
+
if (seconds < 60) return "now";
|
|
926
|
+
for (const [below, unit, suffix] of AGE_STEPS) {
|
|
927
|
+
if (seconds < below) return `${Math.max(1, Math.round(seconds / unit))}${suffix}`;
|
|
928
|
+
}
|
|
929
|
+
return `${Math.floor(seconds / (365 * 86400))}y`;
|
|
930
|
+
}
|
|
931
|
+
function compactCount(count3) {
|
|
932
|
+
if (count3 < 1e4) return String(count3);
|
|
933
|
+
if (count3 < 1e6) return `${(count3 / 1e3).toFixed(1)}k`;
|
|
934
|
+
return `${(count3 / 1e6).toFixed(1)}M`;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// src/utils/display.ts
|
|
938
|
+
var VERSION = package_default.version;
|
|
939
|
+
var DIVIDER = "\u2500".repeat(60);
|
|
940
|
+
var MENU_DIVIDER = "\u2500".repeat(40);
|
|
941
|
+
var COMMIT_LIST_MAX = 50;
|
|
942
|
+
var COMMIT_STEP_MAX = 10;
|
|
943
|
+
var STEP_LABEL_WIDTH = 9;
|
|
944
|
+
var STEP_INDENT = 2 + STEP_LABEL_WIDTH + 2;
|
|
945
|
+
var warningMark = colors_default.yellow("\u26A0");
|
|
946
|
+
var checkMark = colors_default.green("\u2713");
|
|
947
|
+
var activeSpinner = null;
|
|
948
|
+
var jsonMode = false;
|
|
949
|
+
function setJsonMode(enabled) {
|
|
950
|
+
jsonMode = enabled;
|
|
951
|
+
if (!enabled) return;
|
|
952
|
+
const toStderr = (...args) => {
|
|
953
|
+
process5.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
954
|
+
`);
|
|
955
|
+
};
|
|
956
|
+
console.info = toStderr;
|
|
957
|
+
console.warn = toStderr;
|
|
958
|
+
}
|
|
959
|
+
function writeStdout(text) {
|
|
960
|
+
process5.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
961
|
+
`);
|
|
962
|
+
}
|
|
963
|
+
function printHeader() {
|
|
964
|
+
const left = "\u29C8 cella cli";
|
|
965
|
+
const version = ` \xB7 v${VERSION}`;
|
|
966
|
+
const right = "cellajs.com";
|
|
967
|
+
const padding = Math.max(1, DIVIDER.length - `${left}${version}${right}`.length);
|
|
968
|
+
console.info();
|
|
969
|
+
console.info(`${colors_default.cyan(left)}${colors_default.dim(version)}${colors_default.cyan(`${" ".repeat(padding)}${right}`)}`);
|
|
970
|
+
console.info(DIVIDER);
|
|
971
|
+
console.info();
|
|
972
|
+
}
|
|
973
|
+
function createSpinner(text) {
|
|
974
|
+
const enabled = !jsonMode && !getEnv("VITEST") && getEnv("NODE_ENV") !== "test" && !!process5.stdout.isTTY;
|
|
975
|
+
const frames = ["-", "\\", "|", "/"];
|
|
976
|
+
let frame = 0;
|
|
977
|
+
let timer;
|
|
978
|
+
const spinner = {
|
|
979
|
+
text,
|
|
980
|
+
stop() {
|
|
981
|
+
clearInterval(timer);
|
|
982
|
+
if (enabled) process5.stdout.write("\r\x1B[2K");
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
const render = () => process5.stdout.write(`\r${colors_default.cyan(frames[frame++ % frames.length])} ${spinner.text}`);
|
|
986
|
+
if (enabled) {
|
|
987
|
+
render();
|
|
988
|
+
timer = setInterval(render, 80);
|
|
989
|
+
}
|
|
990
|
+
activeSpinner = spinner;
|
|
991
|
+
return spinner;
|
|
992
|
+
}
|
|
993
|
+
function printStep(label, detail, mark = checkMark) {
|
|
994
|
+
stopSpinner();
|
|
995
|
+
const [first = "", ...rest] = (detail ?? "").split("\n");
|
|
996
|
+
console.info(`${mark} ${colors_default.bold(label.padEnd(STEP_LABEL_WIDTH))} ${first}`.trimEnd());
|
|
997
|
+
for (const line of rest) console.info(line ? `${" ".repeat(STEP_INDENT)}${line}` : "");
|
|
998
|
+
}
|
|
999
|
+
function spinnerSuccess(message, detail) {
|
|
1000
|
+
stopSpinner();
|
|
1001
|
+
if (!message) return;
|
|
1002
|
+
console.info(`${checkMark} ${message}`);
|
|
1003
|
+
if (detail) {
|
|
1004
|
+
console.info(` ${colors_default.dim(detail)}`);
|
|
1005
|
+
console.info();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
function spinnerFail(message) {
|
|
1009
|
+
stopSpinner();
|
|
1010
|
+
console.info(`${colors_default.red("\u2717")} ${message}`);
|
|
1011
|
+
}
|
|
1012
|
+
function spinnerText(text) {
|
|
1013
|
+
if (activeSpinner) {
|
|
1014
|
+
activeSpinner.text = text;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
function stopSpinner() {
|
|
1018
|
+
if (activeSpinner) {
|
|
1019
|
+
activeSpinner.stop();
|
|
1020
|
+
activeSpinner = null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function hyperlink(label, url) {
|
|
1024
|
+
if (!url) return label;
|
|
1025
|
+
const safeUrl = url.replace(/[\x00-\x1f\x7f]/g, "");
|
|
1026
|
+
return `\x1B]8;;${safeUrl}\x07${label}\x1B]8;;\x07`;
|
|
1027
|
+
}
|
|
1028
|
+
function styleSubject(subject) {
|
|
1029
|
+
if (/^chore(\([^)]*\))?: release\b/.test(subject)) return colors_default.dim(subject);
|
|
1030
|
+
const breaking = /^([a-z]+(\([^)]*\))?!:)/.exec(subject);
|
|
1031
|
+
return breaking ? `${colors_default.yellow(breaking[1])}${subject.slice(breaking[1].length)}` : subject;
|
|
1032
|
+
}
|
|
1033
|
+
function formatCheckoutStep(checkout) {
|
|
1034
|
+
const { forkPath, branch, headSha, changeCount, upstream } = checkout;
|
|
1035
|
+
const parts = [
|
|
1036
|
+
changeCount === 0 ? "clean" : `${plural(changeCount, "uncommitted change")}, not included in the analysis`
|
|
1037
|
+
];
|
|
1038
|
+
if (upstream.upstream && (upstream.ahead > 0 || upstream.behind > 0)) {
|
|
1039
|
+
parts.push(`${upstream.ahead} ahead, ${upstream.behind} behind ${upstream.upstream}`);
|
|
1040
|
+
}
|
|
1041
|
+
const line = `${branch} @ ${headSha}${colors_default.dim(` \xB7 ${parts.join(" \xB7 ")}`)}`;
|
|
1042
|
+
return resolve3(forkPath) === resolve3(process5.cwd()) ? line : `${line}
|
|
1043
|
+
${colors_default.dim(forkPath)}`;
|
|
1044
|
+
}
|
|
1045
|
+
function formatUpstreamStep(upstream) {
|
|
1046
|
+
const { label, sha, baseSha, pinned, commitCount, commits, githubUrl } = upstream;
|
|
1047
|
+
const since = commitCount === 0 ? "no new commits since the last sync" : `${plural(commitCount, "new commit")} since the last sync at ${baseSha}`;
|
|
1048
|
+
return [
|
|
1049
|
+
`${label} @ ${sha}${colors_default.dim(`${pinned ? " \xB7 --ref" : ""} \xB7 ${since}`)}`,
|
|
1050
|
+
...formatUpstreamCommits(commitCount, commits, githubUrl)
|
|
1051
|
+
].join("\n");
|
|
1052
|
+
}
|
|
1053
|
+
function formatUpstreamCommits(commitCount, commits, upstreamGitHubUrl, options = {}) {
|
|
1054
|
+
const { columns = terminalColumns(), max = COMMIT_STEP_MAX, nowMs } = options;
|
|
1055
|
+
const shown = commits.slice(-max).reverse();
|
|
1056
|
+
const rows = shown.map((commit) => ({ commit, ...splitPrNumber(commit.message) }));
|
|
1057
|
+
const prWidth = Math.max(0, ...rows.map((row) => row.pr ? `#${row.pr}`.length : 0));
|
|
1058
|
+
const ages = rows.map((row) => row.commit.timestamp ? shortAge(row.commit.timestamp, nowMs) : "");
|
|
1059
|
+
const ageWidth = Math.max(0, ...ages.map((age) => age.length));
|
|
1060
|
+
const subjectWidth = Math.max(
|
|
1061
|
+
20,
|
|
1062
|
+
columns - STEP_INDENT - 7 - 2 - (prWidth ? prWidth + 2 : 0) - (ageWidth ? ageWidth + 2 : 0)
|
|
1063
|
+
);
|
|
1064
|
+
const lines = rows.map((row, index) => {
|
|
1065
|
+
const hash = hyperlink(
|
|
1066
|
+
row.commit.hash.slice(0, 7),
|
|
1067
|
+
upstreamGitHubUrl && `${upstreamGitHubUrl}/commit/${row.commit.hash}`
|
|
1068
|
+
);
|
|
1069
|
+
const subject = fitText(row.subject, subjectWidth);
|
|
1070
|
+
let line = `${colors_default.dim(hash)} ${padVisible(styleSubject(subject), subjectWidth)}`;
|
|
1071
|
+
if (prWidth) {
|
|
1072
|
+
const pr = row.pr ? hyperlink(`#${row.pr}`, upstreamGitHubUrl && `${upstreamGitHubUrl}/pull/${row.pr}`) : "";
|
|
1073
|
+
line += ` ${colors_default.dim(padVisible(pr, prWidth))}`;
|
|
1074
|
+
}
|
|
1075
|
+
if (ageWidth) line += ` ${colors_default.dim(ages[index].padStart(ageWidth))}`;
|
|
1076
|
+
return line.trimEnd();
|
|
1077
|
+
});
|
|
1078
|
+
if (commitCount > shown.length) lines.push(colors_default.dim(`+ ${commitCount - shown.length} earlier`));
|
|
1079
|
+
return lines;
|
|
1080
|
+
}
|
|
1081
|
+
function printNextSteps(steps) {
|
|
1082
|
+
if (steps.length === 0) return;
|
|
1083
|
+
printSectionHeader(colors_default.cyan("next steps"));
|
|
1084
|
+
for (const [index, step] of steps.entries()) {
|
|
1085
|
+
const label = step.label.padEnd(STEP_INDENT - 5);
|
|
1086
|
+
console.info(` ${colors_default.dim(String(index + 1))} ${colors_default.bold(label)} ${step.line}`.trimEnd());
|
|
1087
|
+
for (const detail of step.details ?? []) console.info(`${" ".repeat(STEP_INDENT)}${detail}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function printSectionHeader(title) {
|
|
1091
|
+
console.info();
|
|
1092
|
+
console.info(title);
|
|
1093
|
+
console.info(DIVIDER);
|
|
1094
|
+
console.info();
|
|
1095
|
+
}
|
|
1096
|
+
function formatMergeInProgressDetail(conflictCount, autoMergedFiles, linkOptions, maxFiles = 100) {
|
|
1097
|
+
const lines = [`${plural(conflictCount, "conflict")} to resolve in your editor`];
|
|
1098
|
+
if (autoMergedFiles.length === 0) {
|
|
1099
|
+
return lines.join("\n");
|
|
1100
|
+
}
|
|
1101
|
+
lines.push("");
|
|
1102
|
+
lines.push(` auto-merged files (${autoMergedFiles.length}):`);
|
|
1103
|
+
const { forkPath } = linkOptions;
|
|
1104
|
+
const shown = autoMergedFiles.slice(0, maxFiles);
|
|
1105
|
+
for (const filePath of shown) {
|
|
1106
|
+
lines.push(` - ${forkPath ? hyperlink(filePath, `vscode://file${resolve3(forkPath, filePath)}`) : filePath}`);
|
|
1107
|
+
}
|
|
1108
|
+
if (autoMergedFiles.length > shown.length) {
|
|
1109
|
+
lines.push(` ... + ${autoMergedFiles.length - shown.length} more`);
|
|
1110
|
+
}
|
|
1111
|
+
return lines.join("\n");
|
|
1112
|
+
}
|
|
1113
|
+
var statusConfig = {
|
|
1114
|
+
behind: { icon: colors_default.cyan("\u2193"), label: "behind", color: colors_default.cyan, description: "upstream changed, will sync" },
|
|
1115
|
+
diverged: { icon: colors_default.magenta("\u21C5"), label: "diverged", color: colors_default.magenta, description: "both changed, will merge" },
|
|
1116
|
+
drifted: { icon: colors_default.yellow("!"), label: "drifted", color: colors_default.yellow, description: "fork changed (at risk)" },
|
|
1117
|
+
ahead: { icon: colors_default.blue("\u2191"), label: "ahead", color: colors_default.blue, description: "fork changed (protected)" },
|
|
1118
|
+
local: { icon: colors_default.green("+"), label: "local", color: colors_default.green, description: "local file in cella folders" },
|
|
1119
|
+
pinned: { icon: colors_default.green("\u2A00"), label: "pinned", color: colors_default.green, description: "both changed, fork wins" },
|
|
1120
|
+
ignored: { icon: colors_default.gray("\u2A02"), label: "ignored", color: colors_default.gray },
|
|
1121
|
+
identical: { icon: colors_default.gray("\u2713"), label: "identical", color: colors_default.gray, description: "no changes" },
|
|
1122
|
+
deleted: { icon: colors_default.red("\u2717"), label: "deleted", color: colors_default.red },
|
|
1123
|
+
renamed: { icon: colors_default.blue("\u2192"), label: "renamed", color: colors_default.blue }
|
|
1124
|
+
};
|
|
1125
|
+
var summaryStatusConfig = {
|
|
1126
|
+
...statusConfig,
|
|
1127
|
+
managed: {
|
|
1128
|
+
icon: colors_default.cyan("\u25C7"),
|
|
1129
|
+
label: "managed",
|
|
1130
|
+
color: colors_default.cyan,
|
|
1131
|
+
description: "package.jsons & cella/cella.config.ts handled separately"
|
|
1132
|
+
},
|
|
1133
|
+
ignored: { ...statusConfig.ignored, description: "protected by ignored config" }
|
|
1134
|
+
};
|
|
1135
|
+
function printSummary(summary, title = "summary") {
|
|
1136
|
+
printSectionHeader(colors_default.cyan(title));
|
|
1137
|
+
const maxCount = Math.max(...Object.values(summary));
|
|
1138
|
+
const countWidth = String(maxCount).length + 1;
|
|
1139
|
+
const groups = [
|
|
1140
|
+
["managed", "ignored"],
|
|
1141
|
+
["identical"],
|
|
1142
|
+
["ahead", "local", "drifted"],
|
|
1143
|
+
["diverged", "pinned"],
|
|
1144
|
+
["behind"]
|
|
1145
|
+
];
|
|
1146
|
+
for (const group of groups) {
|
|
1147
|
+
if (group !== groups[0]) console.info();
|
|
1148
|
+
for (const status of group) {
|
|
1149
|
+
const { icon, label, color, description } = summaryStatusConfig[status];
|
|
1150
|
+
const countStr = color(String(summary[status]).padStart(countWidth));
|
|
1151
|
+
console.info(` ${icon} ${countStr} ${label.padEnd(15)}${description ? colors_default.dim(description) : ""}`);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
function filesWithStatus(files, status) {
|
|
1156
|
+
return files.filter((f) => f.status === status && !isManagedFile(f.path));
|
|
1157
|
+
}
|
|
1158
|
+
function findProtectedBehind(files) {
|
|
1159
|
+
return files.filter((file) => file.upstreamChanged && !isManagedFile(file.path) && !isGeneratedFile(file.path));
|
|
1160
|
+
}
|
|
1161
|
+
function findProtectedBehindUpstream(result) {
|
|
1162
|
+
const ignoredOnly = new Set((result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths));
|
|
1163
|
+
return [...findProtectedBehind(result.files), ...result.files.filter((file) => ignoredOnly.has(file.path))];
|
|
1164
|
+
}
|
|
1165
|
+
var PROTECTED_BEHIND_HINT = "pinned/ignored files where upstream also changed since the last sync; the fork side wins on conflict, so review what upstream changed and adopt what you need.";
|
|
1166
|
+
function protectionIcon(file) {
|
|
1167
|
+
return file.isIgnored ? statusConfig.ignored.icon : statusConfig.pinned.icon;
|
|
1168
|
+
}
|
|
1169
|
+
function formatUpstreamChangedLines(file) {
|
|
1170
|
+
if (file.upstreamChangedLines === void 0) return "";
|
|
1171
|
+
return colors_default.yellow(` \xB7 ${plural(file.upstreamChangedLines, "line")} changed upstream`);
|
|
1172
|
+
}
|
|
1173
|
+
function printSyncComplete(result, options = {}) {
|
|
1174
|
+
const updated = result.files.filter((file) => file.status === "behind" || file.status === "diverged").length;
|
|
1175
|
+
const diverged = result.files.filter((file) => file.status === "diverged").length;
|
|
1176
|
+
const merged = result.autoMergedFiles?.length ?? Math.max(0, diverged - result.conflicts.length);
|
|
1177
|
+
const conflicts = result.conflicts.length;
|
|
1178
|
+
const counts = `${plural(updated, "file")} updated, ${merged} auto-merged, ${plural(conflicts, "conflict")}`;
|
|
1179
|
+
console.info();
|
|
1180
|
+
if (options.stagedBranch) {
|
|
1181
|
+
console.info(`${checkMark} sync merge staged on '${options.stagedBranch}'`);
|
|
1182
|
+
console.info(colors_default.dim(` ${counts}. committing next...`));
|
|
1183
|
+
} else if (conflicts > 0) {
|
|
1184
|
+
console.info(`${warningMark} sync merge staged with conflicts`);
|
|
1185
|
+
console.info(colors_default.dim(` ${counts}`));
|
|
1186
|
+
} else {
|
|
1187
|
+
console.info(`${checkMark} sync complete`);
|
|
1188
|
+
console.info(colors_default.dim(` ${counts}`));
|
|
1189
|
+
}
|
|
1190
|
+
printProtectedConflicts(result);
|
|
1191
|
+
console.info();
|
|
1192
|
+
}
|
|
1193
|
+
function printProtectedConflicts(result) {
|
|
1194
|
+
const paths = result.protectedConflicts ?? [];
|
|
1195
|
+
if (paths.length === 0) return;
|
|
1196
|
+
const byPath2 = new Map(result.files.map((file) => [file.path, file]));
|
|
1197
|
+
console.info();
|
|
1198
|
+
console.info(
|
|
1199
|
+
`${warningMark} ${colors_default.yellow(
|
|
1200
|
+
`${plural(paths.length, "protected file kept", "protected files kept")} the fork version, dropping upstream changes:`
|
|
1201
|
+
)}`
|
|
1202
|
+
);
|
|
1203
|
+
for (const path of paths) {
|
|
1204
|
+
const file = byPath2.get(path);
|
|
1205
|
+
const icon = file ? protectionIcon(file) : statusConfig.pinned.icon;
|
|
1206
|
+
console.info(` ${icon} ${path}${file ? formatUpstreamChangedLines(file) : ""}`);
|
|
1207
|
+
}
|
|
1208
|
+
console.info(colors_default.dim(` ${PROTECTED_BEHIND_HINT}`));
|
|
1209
|
+
}
|
|
1210
|
+
function plural(count3, singular, pluralForm = `${singular}s`) {
|
|
1211
|
+
return `${count3} ${count3 === 1 ? singular : pluralForm}`;
|
|
1212
|
+
}
|
|
1213
|
+
function printIgnoredUpstreamChanges(result) {
|
|
1214
|
+
const groups = result.ignoredUpstreamChanges ?? [];
|
|
1215
|
+
if (groups.length === 0) return;
|
|
1216
|
+
const total = groups.reduce((count3, group) => count3 + group.paths.length, 0);
|
|
1217
|
+
console.info();
|
|
1218
|
+
console.info(
|
|
1219
|
+
`${warningMark} ${colors_default.yellow(`ignored paths changed upstream \xB7 ${plural(total, "file")} under ${plural(groups.length, "entry", "entries")}`)}`
|
|
1220
|
+
);
|
|
1221
|
+
for (const group of groups) {
|
|
1222
|
+
const counts = [plural(group.paths.length, "file")];
|
|
1223
|
+
if (group.added > 0) counts.push(`${group.added} new`);
|
|
1224
|
+
if (group.deleted > 0) counts.push(`${group.deleted} deleted`);
|
|
1225
|
+
console.info(` ${statusConfig.ignored.icon} ${group.entry} ${colors_default.dim(`\xB7 ${counts.join(", ")}`)}`);
|
|
1226
|
+
}
|
|
1227
|
+
console.info(colors_default.dim(" the fork left these untouched and ignored paths never sync: review and adopt what you need."));
|
|
1228
|
+
}
|
|
1229
|
+
function printUpstreamOverrideChanges(result) {
|
|
1230
|
+
const report = result.upstreamOverrides;
|
|
1231
|
+
if (!report) return;
|
|
1232
|
+
console.info();
|
|
1233
|
+
if (report.kind === "unreadable") {
|
|
1234
|
+
const side = report.side === "base" ? "the last sync point" : "the incoming ref";
|
|
1235
|
+
console.info(
|
|
1236
|
+
colors_default.dim(`upstream sync config not compared: upstream ${CONFIG_FILE} is missing or unreadable at ${side}`)
|
|
1237
|
+
);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
const lines = [];
|
|
1241
|
+
for (const list of ["pinned", "ignored", "packageJsonSync"]) {
|
|
1242
|
+
for (const entry of report[list].added) lines.push(` ${colors_default.green("+")} ${list}: ${entry}`);
|
|
1243
|
+
for (const entry of report[list].removed) {
|
|
1244
|
+
lines.push(` ${colors_default.red("\u2212")} ${list}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
console.info(
|
|
1248
|
+
`${warningMark} ${colors_default.yellow(`upstream changed its sync config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
|
|
1249
|
+
);
|
|
1250
|
+
for (const line of lines) console.info(line);
|
|
1251
|
+
console.info(colors_default.dim(` ${CONFIG_FILE} never syncs: add or drop these by hand where they fit your app.`));
|
|
1252
|
+
}
|
|
1253
|
+
function findMaskingPins(files) {
|
|
1254
|
+
return files.filter(
|
|
1255
|
+
(file) => file.isPinned && !isManagedFile(file.path) && file.status === "behind" && file.existsInFork && file.existsInUpstream
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
function printMaskingPinWarning(files) {
|
|
1259
|
+
const masking = findMaskingPins(files);
|
|
1260
|
+
if (masking.length === 0) return;
|
|
1261
|
+
console.info();
|
|
1262
|
+
console.info(
|
|
1263
|
+
`${warningMark} ${colors_default.yellow(
|
|
1264
|
+
`${plural(masking.length, "pinned file matches", "pinned files match")} the previous upstream but changed upstream:`
|
|
1265
|
+
)}`
|
|
1266
|
+
);
|
|
1267
|
+
console.info(colors_default.yellow(" the pin keeps the old fork copy and silently drops those upstream changes:"));
|
|
1268
|
+
for (const file of masking) {
|
|
1269
|
+
console.info(colors_default.dim(` \u2A00 ${file.path}`));
|
|
1270
|
+
}
|
|
1271
|
+
console.info(
|
|
1272
|
+
colors_default.yellow(" if the fork never customized these, unpin them in cella/cella.config.ts to take upstream.")
|
|
1273
|
+
);
|
|
1274
|
+
console.info();
|
|
1235
1275
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
if (existingReplacement) return fn();
|
|
1242
|
-
const parentsRaw = await git(["log", "-1", "--format=%P", headSha], cwd, { ignoreErrors: true });
|
|
1243
|
-
const parents = parentsRaw ? parentsRaw.split(/\s+/) : [];
|
|
1244
|
-
await git(["replace", "-f", "--graft", headSha, ...parents, baseSha], cwd);
|
|
1245
|
-
try {
|
|
1246
|
-
return await fn();
|
|
1247
|
-
} finally {
|
|
1248
|
-
await git(["replace", "-d", headSha], cwd, { ignoreErrors: true });
|
|
1249
|
-
}
|
|
1276
|
+
function printEngineReports(result, title) {
|
|
1277
|
+
printSummary(result.summary, title);
|
|
1278
|
+
printIgnoredUpstreamChanges(result);
|
|
1279
|
+
printUpstreamOverrideChanges(result);
|
|
1280
|
+
printMaskingPinWarning(result.files);
|
|
1250
1281
|
}
|
|
1251
1282
|
|
|
1252
|
-
// src/utils/
|
|
1283
|
+
// src/utils/overrides.ts
|
|
1284
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1285
|
+
import { join as join6 } from "path";
|
|
1286
|
+
|
|
1287
|
+
// src/utils/alignments.ts
|
|
1253
1288
|
import { existsSync as existsSync4 } from "fs";
|
|
1254
1289
|
import { mkdir as mkdir3, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1255
1290
|
import { dirname as dirname3, join as join5 } from "path";
|
|
1256
|
-
var
|
|
1257
|
-
var
|
|
1258
|
-
var
|
|
1291
|
+
var ALIGNMENTS_DIR = "cella/alignments";
|
|
1292
|
+
var UPSTREAM_ONLY_DIRS = [ALIGNMENTS_DIR, "cella/migrations"];
|
|
1293
|
+
var PENDING_FILE = "cella/cella.alignments.json";
|
|
1294
|
+
var ALIGNMENT_ID = /^\d{8}T\d{4}-[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1295
|
+
var CAUGHT_BY = ["compiler", "tests", "nothing"];
|
|
1259
1296
|
function isCodemodFile(name) {
|
|
1260
1297
|
return name.endsWith(".ts") && !name.endsWith(".test.ts");
|
|
1261
1298
|
}
|
|
1299
|
+
function splitList(value) {
|
|
1300
|
+
return (value ?? "").split(",").map((item) => item.trim()).filter(Boolean);
|
|
1301
|
+
}
|
|
1262
1302
|
function isUpstreamOnly(filePath) {
|
|
1263
|
-
return filePath ===
|
|
1303
|
+
return UPSTREAM_ONLY_DIRS.some((dir) => filePath === dir || filePath.startsWith(`${dir}/`));
|
|
1264
1304
|
}
|
|
1265
1305
|
function bodyLines(lines) {
|
|
1266
1306
|
let inComment = false;
|
|
@@ -1270,7 +1310,7 @@ function bodyLines(lines) {
|
|
|
1270
1310
|
return false;
|
|
1271
1311
|
});
|
|
1272
1312
|
}
|
|
1273
|
-
function
|
|
1313
|
+
function parseAlignment(id, source, fileNames) {
|
|
1274
1314
|
const lines = source.replace(/\r\n/g, "\n").split("\n");
|
|
1275
1315
|
const close = lines[0] === "---" ? lines.indexOf("---", 1) : -1;
|
|
1276
1316
|
const meta = /* @__PURE__ */ new Map();
|
|
@@ -1282,31 +1322,74 @@ function parseNote(id, source, fileNames) {
|
|
|
1282
1322
|
const blocks = body.split(/\n\s*\n/).map((block) => block.trim()).filter(Boolean);
|
|
1283
1323
|
const title = /^# (.+)$/.exec(blocks[0] ?? "")?.[1].trim();
|
|
1284
1324
|
const summary = title && blocks[1] && !/^(#|>|[-*|]|```)/.test(blocks[1]) ? blocks[1].replace(/\s*\n\s*/g, " ") : "";
|
|
1325
|
+
const caughtBy = CAUGHT_BY.find((value) => value === meta.get("caughtBy")) ?? null;
|
|
1285
1326
|
return {
|
|
1286
1327
|
id,
|
|
1287
1328
|
title: title || id,
|
|
1288
1329
|
summary,
|
|
1289
1330
|
syncBreaking: meta.get("syncBreaking") === "true",
|
|
1290
1331
|
clientCacheBump: meta.get("clientCacheBump") === "true",
|
|
1291
|
-
|
|
1332
|
+
caughtBy,
|
|
1333
|
+
generate: meta.get("generate") === "true",
|
|
1334
|
+
grep: splitList(meta.get("grep")),
|
|
1335
|
+
roots: splitList(meta.get("roots")),
|
|
1292
1336
|
codemod: fileNames.find(isCodemodFile) ?? null
|
|
1293
1337
|
};
|
|
1294
1338
|
}
|
|
1295
|
-
async function
|
|
1296
|
-
const out = await git(["ls-tree", "--name-only", `${ref}:${
|
|
1297
|
-
return out.split("\n").filter((name) =>
|
|
1339
|
+
async function listAlignmentIds(cwd, ref) {
|
|
1340
|
+
const out = await git(["ls-tree", "--name-only", `${ref}:${ALIGNMENTS_DIR}`], cwd, { ignoreErrors: true });
|
|
1341
|
+
return out.split("\n").filter((name) => ALIGNMENT_ID.test(name)).sort();
|
|
1298
1342
|
}
|
|
1299
|
-
async function
|
|
1300
|
-
const source = await git(["show", `${ref}:${
|
|
1343
|
+
async function readAlignment(cwd, ref, id) {
|
|
1344
|
+
const source = await git(["show", `${ref}:${ALIGNMENTS_DIR}/${id}/README.md`], cwd, { ignoreErrors: true });
|
|
1301
1345
|
if (!source) return null;
|
|
1302
|
-
const files = await git(["ls-tree", "--name-only", `${ref}:${
|
|
1303
|
-
return
|
|
1346
|
+
const files = await git(["ls-tree", "--name-only", `${ref}:${ALIGNMENTS_DIR}/${id}`], cwd, { ignoreErrors: true });
|
|
1347
|
+
return parseAlignment(id, source, files.split("\n").filter(Boolean));
|
|
1304
1348
|
}
|
|
1305
|
-
async function
|
|
1306
|
-
const [before, after] = await Promise.all([
|
|
1349
|
+
async function arrivedAlignmentIds(cwd, from, to) {
|
|
1350
|
+
const [before, after] = await Promise.all([listAlignmentIds(cwd, from), listAlignmentIds(cwd, to)]);
|
|
1307
1351
|
const known = new Set(before);
|
|
1308
1352
|
return after.filter((id) => !known.has(id));
|
|
1309
1353
|
}
|
|
1354
|
+
async function readAlignmentSource(cwd, ref, id) {
|
|
1355
|
+
const out = await git(
|
|
1356
|
+
["log", "-1", "--diff-filter=A", "--format=%H%x09%s", ref, "--", `${ALIGNMENTS_DIR}/${id}/README.md`],
|
|
1357
|
+
cwd,
|
|
1358
|
+
{ ignoreErrors: true }
|
|
1359
|
+
);
|
|
1360
|
+
if (!out) return null;
|
|
1361
|
+
const [commit, subject = ""] = out.split(" ");
|
|
1362
|
+
const { pr = null } = splitPrNumber(subject);
|
|
1363
|
+
const nulSeparated = (text) => text.split("\0").filter(Boolean);
|
|
1364
|
+
const [changed, added] = await Promise.all([
|
|
1365
|
+
git(["diff-tree", "--root", "--no-commit-id", "--name-only", "--no-renames", "-r", "-z", commit], cwd),
|
|
1366
|
+
git(
|
|
1367
|
+
[
|
|
1368
|
+
"diff-tree",
|
|
1369
|
+
"--root",
|
|
1370
|
+
"--no-commit-id",
|
|
1371
|
+
"--name-only",
|
|
1372
|
+
"--diff-filter=A",
|
|
1373
|
+
"-r",
|
|
1374
|
+
"-z",
|
|
1375
|
+
commit,
|
|
1376
|
+
"--",
|
|
1377
|
+
ALIGNMENTS_DIR
|
|
1378
|
+
],
|
|
1379
|
+
cwd
|
|
1380
|
+
)
|
|
1381
|
+
]);
|
|
1382
|
+
const readmeId = new RegExp(`^${ALIGNMENTS_DIR}/([^/]+)/README\\.md$`);
|
|
1383
|
+
const sharedCommit = nulSeparated(added).map((path) => readmeId.exec(path)?.[1]).filter((other) => !!other && other !== id && ALIGNMENT_ID.test(other)).sort();
|
|
1384
|
+
return { commit, pr, sharedCommit, changedFiles: nulSeparated(changed) };
|
|
1385
|
+
}
|
|
1386
|
+
async function grepAlignmentTerms(cwd, terms, inFiles) {
|
|
1387
|
+
if (terms.length === 0) return [];
|
|
1388
|
+
const out = await git(["grep", "-n", "-I", "-w", "-F", "-z", ...terms.flatMap((term) => ["-e", term]), "--"], cwd, {
|
|
1389
|
+
ignoreErrors: true
|
|
1390
|
+
});
|
|
1391
|
+
return out.split("\n").map((match) => match.split("\0")).filter(([file, line]) => file && line && inFiles(file)).map(([file, line]) => ({ file, line: Number(line) }));
|
|
1392
|
+
}
|
|
1310
1393
|
async function readPending(cwd) {
|
|
1311
1394
|
const path = join5(cwd, PENDING_FILE);
|
|
1312
1395
|
if (!existsSync4(path)) return [];
|
|
@@ -1324,14 +1407,13 @@ async function writePending(cwd, ids) {
|
|
|
1324
1407
|
await writeFile2(path, `${JSON.stringify({ pending }, null, 2)}
|
|
1325
1408
|
`, "utf8");
|
|
1326
1409
|
}
|
|
1327
|
-
function
|
|
1328
|
-
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${
|
|
1410
|
+
function alignmentUrl(upstreamGitHubUrl, commit, id) {
|
|
1411
|
+
return upstreamGitHubUrl ? `${upstreamGitHubUrl}/blob/${commit}/${ALIGNMENTS_DIR}/${id}/README.md` : void 0;
|
|
1329
1412
|
}
|
|
1330
|
-
function
|
|
1413
|
+
function formatAlignmentsLine(total, open) {
|
|
1331
1414
|
const parts = [open === 0 ? `all ${total} handled` : `${Math.max(0, total - open)} of ${total} handled`];
|
|
1332
|
-
if (
|
|
1333
|
-
|
|
1334
|
-
return `migration notes: ${parts.join(" \xB7 ")}`;
|
|
1415
|
+
if (open > 0) parts.push(`${open} open: pnpm cella align`);
|
|
1416
|
+
return `alignments: ${parts.join(" \xB7 ")}`;
|
|
1335
1417
|
}
|
|
1336
1418
|
|
|
1337
1419
|
// src/utils/overrides.ts
|
|
@@ -1455,6 +1537,7 @@ var serviceDefinitions = [
|
|
|
1455
1537
|
["--list", "non-interactive output for tooling (one file per line)"],
|
|
1456
1538
|
jsonOption,
|
|
1457
1539
|
["--scope <scope>", "analyze scope for --list/--json: all|risk|protected"],
|
|
1540
|
+
["--all", "list every file instead of collapsing large groups"],
|
|
1458
1541
|
trackOption,
|
|
1459
1542
|
refOption,
|
|
1460
1543
|
unpinnedOption
|
|
@@ -1473,19 +1556,19 @@ var serviceDefinitions = [
|
|
|
1473
1556
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1474
1557
|
},
|
|
1475
1558
|
{
|
|
1476
|
-
name: "
|
|
1477
|
-
description: "list the upstream
|
|
1559
|
+
name: "align",
|
|
1560
|
+
description: "list the upstream alignments this app has not handled yet",
|
|
1478
1561
|
options: [
|
|
1479
|
-
["--all", "list every upstream
|
|
1562
|
+
["--all", "list every upstream alignment, handled or not"],
|
|
1480
1563
|
jsonOption,
|
|
1481
|
-
["--show <id>", "print one
|
|
1482
|
-
["--extract <id>", "write one
|
|
1483
|
-
["--run <id>", "run one
|
|
1484
|
-
["--mark <ids...>", "record
|
|
1564
|
+
["--show <id>", "print one alignment's README and what git tells about it"],
|
|
1565
|
+
["--extract <id>", "write one alignment's folder under node_modules/.cache to run its codemod"],
|
|
1566
|
+
["--run <id>", "run one alignment's codemod; files identical to upstream stay as they are"],
|
|
1567
|
+
["--mark <ids...>", "record alignments as handled"]
|
|
1485
1568
|
],
|
|
1486
1569
|
operands: [
|
|
1487
1570
|
"[codemodArgs...]",
|
|
1488
|
-
"with --run, after `--`: arguments for the codemod (default: inventory and the
|
|
1571
|
+
"with --run, after `--`: arguments for the codemod (default: inventory and the alignment's roots)"
|
|
1489
1572
|
],
|
|
1490
1573
|
includeInMenu: (context) => !context.isUpstreamRepo
|
|
1491
1574
|
},
|
|
@@ -1560,8 +1643,8 @@ function buildProgram(setSelection) {
|
|
|
1560
1643
|
" $ cella analyze --json --scope risk",
|
|
1561
1644
|
" $ cella sync --track branch",
|
|
1562
1645
|
" $ cella sync --ref 4f7d87c",
|
|
1563
|
-
" $ cella
|
|
1564
|
-
" $ cella
|
|
1646
|
+
" $ cella align --run 20261001T2116-tailwind-class-conventions -- rewrite frontend/src",
|
|
1647
|
+
" $ cella align --mark 20261002T0614-config-switch",
|
|
1565
1648
|
" $ cella audit --list",
|
|
1566
1649
|
" $ cella stats --since origin/main --md",
|
|
1567
1650
|
" $ cella contributions --fork raak --json"
|
|
@@ -1592,7 +1675,7 @@ async function promptForService(userConfig, forkPath) {
|
|
|
1592
1675
|
if (selected === "exit") {
|
|
1593
1676
|
console.info(colors_default.dim("exiting..."));
|
|
1594
1677
|
console.info();
|
|
1595
|
-
|
|
1678
|
+
process6.exit(0);
|
|
1596
1679
|
}
|
|
1597
1680
|
console.info();
|
|
1598
1681
|
return selected;
|
|
@@ -1601,7 +1684,7 @@ function buildRuntimeConfig(userConfig, forkPath, selection, upstreamRef) {
|
|
|
1601
1684
|
return { ...userConfig, forkPath, upstreamRef, service: selection.service ?? "analyze", ...selection.options };
|
|
1602
1685
|
}
|
|
1603
1686
|
async function parseCli(userConfig, forkPath) {
|
|
1604
|
-
const selection = parseCommandLine(
|
|
1687
|
+
const selection = parseCommandLine(process6.argv);
|
|
1605
1688
|
const machineList = selection.options.list && selection.service !== "audit";
|
|
1606
1689
|
if (selection.options.json || selection.options.md || machineList) setJsonMode(true);
|
|
1607
1690
|
printHeader();
|
|
@@ -1615,6 +1698,12 @@ async function parseCli(userConfig, forkPath) {
|
|
|
1615
1698
|
return buildRuntimeConfig(userConfig, forkPath, selection, branchRef);
|
|
1616
1699
|
}
|
|
1617
1700
|
|
|
1701
|
+
// src/services/align.ts
|
|
1702
|
+
import { execFileSync, spawnSync } from "child_process";
|
|
1703
|
+
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
1704
|
+
import { dirname as dirname4, join as join8 } from "path";
|
|
1705
|
+
import process7 from "process";
|
|
1706
|
+
|
|
1618
1707
|
// src/services/merge-engine.ts
|
|
1619
1708
|
import { existsSync as existsSync6 } from "fs";
|
|
1620
1709
|
import { rm as rm2 } from "fs/promises";
|
|
@@ -1936,7 +2025,9 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
|
|
|
1936
2025
|
const upstreamOnly = fileIsIgnored && isReportableIgnored(filePath) && upstreamHash !== baseHash && forkHash === baseHash;
|
|
1937
2026
|
push(status, { upstreamChanged: upstreamChanged || void 0, upstreamOnly: upstreamOnly || void 0 });
|
|
1938
2027
|
}
|
|
1939
|
-
const flagged = analyzedFiles.filter(
|
|
2028
|
+
const flagged = analyzedFiles.filter(
|
|
2029
|
+
(file) => file.upstreamChanged && !renamedProtected.has(file.path) || file.upstreamOnly
|
|
2030
|
+
);
|
|
1940
2031
|
if (flagged.length > 0) {
|
|
1941
2032
|
const stat = await getDiffStat(
|
|
1942
2033
|
repoPath,
|
|
@@ -1983,7 +2074,7 @@ async function enrichChangeInfo(repoPath, files, mergeBaseRef, localRef, incomin
|
|
|
1983
2074
|
setChanged(file, forkInfo.get(file.path));
|
|
1984
2075
|
} else if (file.status === "behind") {
|
|
1985
2076
|
setChanged(file, upstreamInfo.get(file.path));
|
|
1986
|
-
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged) {
|
|
2077
|
+
} else if (file.status === "diverged" || file.status === "pinned" || file.upstreamChanged || file.upstreamOnly) {
|
|
1987
2078
|
setChanged(file, forkInfo.get(file.path));
|
|
1988
2079
|
const info = upstreamInfo.get(file.path);
|
|
1989
2080
|
if (info) {
|
|
@@ -2015,6 +2106,10 @@ function getGitHubBaseUrl(remoteUrl) {
|
|
|
2015
2106
|
const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
2016
2107
|
return match ? `https://github.com/${match[1]}/${match[2]}` : null;
|
|
2017
2108
|
}
|
|
2109
|
+
async function readOriginGitHubUrl(forkPath) {
|
|
2110
|
+
const origin = await git(["remote", "get-url", "origin"], forkPath, { ignoreErrors: true });
|
|
2111
|
+
return getGitHubBaseUrl(origin.trim()) ?? void 0;
|
|
2112
|
+
}
|
|
2018
2113
|
function isProtectedFile(file) {
|
|
2019
2114
|
return file.isIgnored || file.isPinned;
|
|
2020
2115
|
}
|
|
@@ -2150,11 +2245,6 @@ function calculateSummary(files) {
|
|
|
2150
2245
|
}
|
|
2151
2246
|
return summary;
|
|
2152
2247
|
}
|
|
2153
|
-
function formatLocalCheckoutDetail(forkPath, branch, headSha, changeCount, upstream) {
|
|
2154
|
-
const tracking = upstream.upstream ? `, tracking ${upstream.upstream} (${upstream.ahead} ahead, ${upstream.behind} behind)` : ", no tracking branch";
|
|
2155
|
-
const workingTree = changeCount === 0 ? " working tree clean" : ` ${changeCount} uncommitted change${changeCount === 1 ? "" : "s"} in working tree, not included in analysis`;
|
|
2156
|
-
return [`${forkPath}`, ` ${branch} @ ${headSha}${tracking}`, workingTree].join("\n");
|
|
2157
|
-
}
|
|
2158
2248
|
async function resolvePinnedRef(forkPath, config, remoteName, branchRef, ref) {
|
|
2159
2249
|
const resolved = await resolveUpstreamCommit(forkPath, remoteName, ref);
|
|
2160
2250
|
if (!resolved) {
|
|
@@ -2178,7 +2268,7 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2178
2268
|
getWorkingTreeChangeCount(forkPath),
|
|
2179
2269
|
getUpstreamStatus(forkPath)
|
|
2180
2270
|
]);
|
|
2181
|
-
onStep?.("
|
|
2271
|
+
onStep?.("checkout", formatCheckoutStep({ forkPath, branch, headSha, changeCount, upstream: upstreamStatus }));
|
|
2182
2272
|
onProgress?.("setting up upstream remote...");
|
|
2183
2273
|
const { track: configTrack, branchRef } = resolveUpstream(config.settings);
|
|
2184
2274
|
const remoteName = DEFAULT_UPSTREAM_REMOTE;
|
|
@@ -2206,7 +2296,6 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2206
2296
|
}
|
|
2207
2297
|
const upstreamLabel = releaseTag ?? (config.ref ? `--ref ${config.ref}` : upstreamRef);
|
|
2208
2298
|
config.upstreamRef = upstreamRef;
|
|
2209
|
-
onStep?.("remote configured", `${upstreamLabel} \u2192 ${config.settings.upstreamUrl}`);
|
|
2210
2299
|
const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
|
|
2211
2300
|
const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
|
|
2212
2301
|
if (cliMismatch) throw new Error(cliMismatch);
|
|
@@ -2217,20 +2306,35 @@ async function prepareUpstream(config, onProgress, onStep) {
|
|
|
2217
2306
|
const mergeRef = config.unpinned ? await getMergeBase(forkPath, "HEAD", upstreamRef) : mergeBase;
|
|
2218
2307
|
const upstreamCommit = await getCommitInfo(forkPath, upstreamRef);
|
|
2219
2308
|
if (upstreamCommit.hash !== mergeBase && await isAncestor(forkPath, upstreamCommit.hash, mergeBase)) {
|
|
2220
|
-
const [
|
|
2309
|
+
const [upstreamShort2, syncShort] = await Promise.all([
|
|
2221
2310
|
getShortSha(forkPath, upstreamCommit.hash),
|
|
2222
2311
|
getShortSha(forkPath, mergeBase)
|
|
2223
2312
|
]);
|
|
2224
|
-
const
|
|
2313
|
+
const hint2 = 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}.`;
|
|
2225
2314
|
throw new BehindSyncPointError(
|
|
2226
|
-
`upstream ${upstreamLabel} (${
|
|
2227
|
-
${
|
|
2315
|
+
`upstream ${upstreamLabel} (${upstreamShort2}) is behind the last sync point ${syncShort}: the fork already has it, and syncing it would revert the upstream changes after it.
|
|
2316
|
+
${hint2}`
|
|
2228
2317
|
);
|
|
2229
2318
|
}
|
|
2230
2319
|
const commitCount = await countCommitsBetween(forkPath, mergeBase, upstreamRef);
|
|
2231
|
-
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef,
|
|
2232
|
-
|
|
2233
|
-
|
|
2320
|
+
const upstreamCommits = commitCount > 0 ? await listCommitsBetween(forkPath, mergeBase, upstreamRef, COMMIT_STEP_MAX) : [];
|
|
2321
|
+
const [upstreamShort, baseShort] = await Promise.all([
|
|
2322
|
+
getShortSha(forkPath, upstreamCommit.hash),
|
|
2323
|
+
getShortSha(forkPath, mergeBase)
|
|
2324
|
+
]);
|
|
2325
|
+
onStep?.(
|
|
2326
|
+
"upstream",
|
|
2327
|
+
formatUpstreamStep({
|
|
2328
|
+
label: releaseTag ?? branchRef.replace(`${remoteName}/`, ""),
|
|
2329
|
+
sha: upstreamShort,
|
|
2330
|
+
baseSha: baseShort,
|
|
2331
|
+
pinned: !!config.ref,
|
|
2332
|
+
commitCount,
|
|
2333
|
+
commits: upstreamCommits,
|
|
2334
|
+
githubUrl: upstreamGitHubUrl
|
|
2335
|
+
})
|
|
2336
|
+
);
|
|
2337
|
+
return { upstreamRef, releaseTag, mergeBase, mergeRef, upstreamGitHubUrl, upstreamCommit, commitCount };
|
|
2234
2338
|
}
|
|
2235
2339
|
async function assertSyncConfigFollowed(config, ctx) {
|
|
2236
2340
|
if (config.keepConfig) return;
|
|
@@ -2245,7 +2349,8 @@ function resultMeta(config, ctx) {
|
|
|
2245
2349
|
upstreamRef: ctx.upstreamRef,
|
|
2246
2350
|
upstreamTag: ctx.releaseTag,
|
|
2247
2351
|
upstreamGitHubUrl: ctx.upstreamGitHubUrl,
|
|
2248
|
-
upstreamCommit: ctx.upstreamCommit
|
|
2352
|
+
upstreamCommit: ctx.upstreamCommit,
|
|
2353
|
+
upstreamCommitCount: ctx.commitCount
|
|
2249
2354
|
};
|
|
2250
2355
|
}
|
|
2251
2356
|
async function upstreamOnlyReports(config, ctx, files) {
|
|
@@ -2266,33 +2371,40 @@ async function upstreamOnlyReports(config, ctx, files) {
|
|
|
2266
2371
|
return {};
|
|
2267
2372
|
}
|
|
2268
2373
|
}
|
|
2269
|
-
|
|
2374
|
+
function pendingAfterSync(alignments) {
|
|
2375
|
+
return [.../* @__PURE__ */ new Set([...alignments.pending, ...alignments.arrived])];
|
|
2376
|
+
}
|
|
2377
|
+
async function readAlignmentsInRange(config, ctx) {
|
|
2270
2378
|
const { forkPath } = config;
|
|
2271
2379
|
try {
|
|
2272
2380
|
const [total, arrived, pending] = await Promise.all([
|
|
2273
|
-
|
|
2274
|
-
|
|
2381
|
+
listAlignmentIds(forkPath, ctx.upstreamRef),
|
|
2382
|
+
arrivedAlignmentIds(forkPath, ctx.mergeBase, ctx.upstreamRef),
|
|
2275
2383
|
readPending(forkPath)
|
|
2276
2384
|
]);
|
|
2277
|
-
return { total: total.length, arrived, pending
|
|
2385
|
+
return { total: total.length, arrived, pending };
|
|
2278
2386
|
} catch {
|
|
2279
2387
|
return void 0;
|
|
2280
2388
|
}
|
|
2281
2389
|
}
|
|
2282
|
-
function
|
|
2283
|
-
|
|
2390
|
+
function alignmentsResult(alignments, options) {
|
|
2391
|
+
if (!alignments) return {};
|
|
2392
|
+
const open = options.dryRun ? alignments.pending : pendingAfterSync(alignments);
|
|
2393
|
+
return { alignments: { total: alignments.total, arrived: alignments.arrived, open } };
|
|
2284
2394
|
}
|
|
2285
2395
|
async function removeUpstreamOnlyCopy(forkPath, onStep) {
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2396
|
+
for (const dir of UPSTREAM_ONLY_DIRS) {
|
|
2397
|
+
if (!existsSync6(join7(forkPath, dir))) continue;
|
|
2398
|
+
await git(["rm", "-r", "-q", "--ignore-unmatch", "--", dir], forkPath);
|
|
2399
|
+
await rm2(join7(forkPath, dir), { recursive: true, force: true });
|
|
2400
|
+
onStep?.("alignments", `removed ${dir}/: alignments stay upstream, list them with pnpm cella align`);
|
|
2401
|
+
}
|
|
2290
2402
|
}
|
|
2291
2403
|
async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
2292
2404
|
const { forkPath } = config;
|
|
2293
2405
|
const { upstreamRef, releaseTag, mergeRef, upstreamGitHubUrl, upstreamCommit } = ctx;
|
|
2294
2406
|
await assertSyncConfigFollowed(config, ctx);
|
|
2295
|
-
const
|
|
2407
|
+
const alignments = await readAlignmentsInRange(config, ctx);
|
|
2296
2408
|
const { remainingConflicts, analyzedFiles, autoMergedFiles, protectedConflicts } = await applyDirectMerge(
|
|
2297
2409
|
forkPath,
|
|
2298
2410
|
upstreamRef,
|
|
@@ -2317,18 +2429,15 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2317
2429
|
await storeLastSyncRef(forkPath, upstreamCommit.hash);
|
|
2318
2430
|
await writeSyncManifest(forkPath, syncManifest);
|
|
2319
2431
|
await stagePath(forkPath, MANIFEST_FILE);
|
|
2320
|
-
if (
|
|
2321
|
-
await writePending(forkPath,
|
|
2432
|
+
if (alignments) {
|
|
2433
|
+
await writePending(forkPath, pendingAfterSync(alignments));
|
|
2322
2434
|
await stagePath(forkPath, PENDING_FILE);
|
|
2323
2435
|
}
|
|
2324
2436
|
await removeUpstreamOnlyCopy(forkPath, onStep);
|
|
2325
2437
|
};
|
|
2326
2438
|
if (remainingConflicts.length > 0) {
|
|
2327
2439
|
await recordSyncPoint();
|
|
2328
|
-
onStep?.(
|
|
2329
|
-
"merge in progress",
|
|
2330
|
-
formatMergeInProgressDetail(remainingConflicts.length, autoMergedFiles, { forkPath }, 100)
|
|
2331
|
-
);
|
|
2440
|
+
onStep?.("merged", formatMergeInProgressDetail(remainingConflicts.length, autoMergedFiles, { forkPath }, 100));
|
|
2332
2441
|
} else if (totalResolved > 0) {
|
|
2333
2442
|
const label = synced > 0 ? `${synced} files from upstream` : "upstream changes";
|
|
2334
2443
|
await recordSyncPoint();
|
|
@@ -2346,12 +2455,12 @@ async function runSyncMerge(config, ctx, onProgress, onStep) {
|
|
|
2346
2455
|
protectedConflicts,
|
|
2347
2456
|
...resultMeta(config, ctx),
|
|
2348
2457
|
...await upstreamOnlyReports(config, ctx, analyzedFiles),
|
|
2349
|
-
...
|
|
2458
|
+
...alignmentsResult(alignments, { dryRun: false })
|
|
2350
2459
|
};
|
|
2351
2460
|
}
|
|
2352
2461
|
async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
2353
2462
|
const { forkPath } = config;
|
|
2354
|
-
const
|
|
2463
|
+
const alignments = await readAlignmentsInRange(config, ctx);
|
|
2355
2464
|
onProgress?.("analyzing files...");
|
|
2356
2465
|
const analyzedFiles = await analyzeRefs(
|
|
2357
2466
|
forkPath,
|
|
@@ -2364,7 +2473,7 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2364
2473
|
await enrichChangeInfo(forkPath, analyzedFiles, ctx.mergeBase, "HEAD", ctx.upstreamRef);
|
|
2365
2474
|
const protectedConflicts = analyzedFiles.filter((f) => f.upstreamChanged).map((f) => f.path);
|
|
2366
2475
|
const reports = await upstreamOnlyReports(config, ctx, analyzedFiles);
|
|
2367
|
-
onStep?.("
|
|
2476
|
+
onStep?.("analyzed", `${plural(analyzedFiles.length, "file")}${colors_default.dim(" \xB7 dry run, nothing changed")}`);
|
|
2368
2477
|
return {
|
|
2369
2478
|
success: true,
|
|
2370
2479
|
files: analyzedFiles,
|
|
@@ -2374,25 +2483,23 @@ async function runAnalyzePreview(config, ctx, onProgress, onStep) {
|
|
|
2374
2483
|
protectedConflicts,
|
|
2375
2484
|
...resultMeta(config, ctx),
|
|
2376
2485
|
...reports,
|
|
2377
|
-
...
|
|
2486
|
+
...alignmentsResult(alignments, { dryRun: true })
|
|
2378
2487
|
};
|
|
2379
2488
|
}
|
|
2380
2489
|
async function runEngineWithSpinner(config, apply) {
|
|
2381
2490
|
const onProgress = (message) => spinnerText(message);
|
|
2382
2491
|
const onStep = (label, detail) => {
|
|
2383
|
-
|
|
2492
|
+
printStep(label, detail);
|
|
2384
2493
|
createSpinner("...");
|
|
2385
2494
|
};
|
|
2386
2495
|
const ctx = await prepareUpstream(config, onProgress, onStep);
|
|
2387
2496
|
return apply ? runSyncMerge(config, ctx, onProgress, onStep) : runAnalyzePreview(config, ctx, onProgress, onStep);
|
|
2388
2497
|
}
|
|
2389
2498
|
|
|
2390
|
-
// src/services/
|
|
2391
|
-
|
|
2392
|
-
import { mkdir as mkdir4, writeFile as writeFile3 } from "fs/promises";
|
|
2393
|
-
import { dirname as dirname4, join as join8 } from "path";
|
|
2394
|
-
var EXTRACT_DIR = "node_modules/.cache/cella/migrations";
|
|
2499
|
+
// src/services/align.ts
|
|
2500
|
+
var EXTRACT_DIR = "node_modules/.cache/cella/alignments";
|
|
2395
2501
|
var RESTORED_LIST_MAX = 20;
|
|
2502
|
+
var SHOW_LIST_MAX = 30;
|
|
2396
2503
|
async function ensureUpstreamCommit(config, sha) {
|
|
2397
2504
|
const { forkPath } = config;
|
|
2398
2505
|
const present = async () => !!await revParse(forkPath, `${sha}^{commit}`);
|
|
@@ -2402,60 +2509,158 @@ async function ensureUpstreamCommit(config, sha) {
|
|
|
2402
2509
|
if (!await present())
|
|
2403
2510
|
throw new Error(`upstream commit ${sha.slice(0, 7)} is not reachable from '${DEFAULT_UPSTREAM_REMOTE}'`);
|
|
2404
2511
|
}
|
|
2405
|
-
async function
|
|
2512
|
+
async function resolveAlignmentsRef(config) {
|
|
2406
2513
|
const { forkPath } = config;
|
|
2407
2514
|
const ref = await getStoredSyncRef(forkPath) ?? await readManifestBaseAtRef(forkPath, "HEAD");
|
|
2408
2515
|
if (!ref) return null;
|
|
2409
2516
|
await ensureUpstreamCommit(config, ref);
|
|
2410
2517
|
return ref;
|
|
2411
2518
|
}
|
|
2412
|
-
async function
|
|
2519
|
+
async function readOpenAlignments(config) {
|
|
2413
2520
|
try {
|
|
2414
|
-
const ref = await
|
|
2521
|
+
const ref = await resolveAlignmentsRef(config);
|
|
2415
2522
|
if (!ref) return null;
|
|
2416
|
-
|
|
2417
|
-
return ids.length > 0 || pending.length > 0 ? { total: ids.length, open: pending.length } : null;
|
|
2523
|
+
return { ref, open: await readPending(config.forkPath) };
|
|
2418
2524
|
} catch {
|
|
2419
2525
|
return null;
|
|
2420
2526
|
}
|
|
2421
2527
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
const
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2528
|
+
var STEP_ROWS_MAX = 5;
|
|
2529
|
+
function alignmentTags(alignment) {
|
|
2530
|
+
return [
|
|
2531
|
+
alignment.codemod ? "codemod" : "manual",
|
|
2532
|
+
alignment.syncBreaking ? "sync-breaking" : "not sync-breaking",
|
|
2533
|
+
alignment.caughtBy ? `caught by ${alignment.caughtBy}` : null
|
|
2534
|
+
].filter(Boolean).join(" \xB7 ");
|
|
2535
|
+
}
|
|
2536
|
+
async function alignmentsStep(config, view) {
|
|
2537
|
+
const arriving = view.arriving ?? [];
|
|
2538
|
+
const open = view.open.filter((id) => !arriving.includes(id));
|
|
2539
|
+
const arrived = (view.arrived ?? []).filter((id) => open.includes(id));
|
|
2540
|
+
if (arriving.length === 0 && open.length === 0) return void 0;
|
|
2541
|
+
const line = arriving.length > 0 ? `${plural(arriving.length, "alignment")} ${arriving.length === 1 ? "arrives" : "arrive"} with this sync${open.length > 0 ? colors_default.dim(` \xB7 ${open.length} open`) : ""}` : `${plural(open.length, "alignment")} open${arrived.length > 0 ? colors_default.dim(` \xB7 ${arrived.length} arrived with this sync`) : ""}`;
|
|
2542
|
+
const githubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
|
|
2543
|
+
const ids = [...arriving, ...open];
|
|
2544
|
+
const details = [];
|
|
2545
|
+
for (const id of ids.slice(0, STEP_ROWS_MAX)) {
|
|
2546
|
+
const alignment = await readAlignment(config.forkPath, view.ref, id).catch(() => null);
|
|
2547
|
+
const title = hyperlink(alignment?.title ?? id, alignmentUrl(githubUrl, view.ref, id));
|
|
2548
|
+
details.push(`${colors_default.cyan("\u25CB")} ${title}${alignment ? colors_default.dim(` \xB7 ${alignmentTags(alignment)}`) : ""}`);
|
|
2549
|
+
if (!process7.stdout.isTTY) details.push(colors_default.dim(` id ${id}`));
|
|
2550
|
+
}
|
|
2551
|
+
if (ids.length > STEP_ROWS_MAX) details.push(colors_default.dim(`+ ${ids.length - STEP_ROWS_MAX} more`));
|
|
2552
|
+
details.push(`${arriving.length > 0 ? colors_default.dim("after the sync: ") : ""}pnpm cella align`);
|
|
2553
|
+
return { label: "align", line, details };
|
|
2554
|
+
}
|
|
2555
|
+
async function readForkView(config, ref) {
|
|
2556
|
+
const { pinned = [], ignored = [] } = config.overrides ?? {};
|
|
2557
|
+
const counts = (path) => !isManagedFile(path) && !isGeneratedFile(path) && !isUpstreamOnly(path);
|
|
2558
|
+
const out = await git(["diff", "--name-only", "--no-renames", "-z", ref, "--"], config.forkPath);
|
|
2559
|
+
return {
|
|
2560
|
+
githubUrl: getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0,
|
|
2561
|
+
owns: (path) => counts(path) && isUnderAnyFolder(path, [...pinned, ...ignored]),
|
|
2562
|
+
changed: new Set(out.split("\0").filter((path) => path && counts(path)))
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
async function describeAlignment(config, view, ref, alignment, open) {
|
|
2566
|
+
const { forkPath } = config;
|
|
2567
|
+
const [source, grepHits] = await Promise.all([
|
|
2568
|
+
readAlignmentSource(forkPath, ref, alignment.id),
|
|
2569
|
+
grepAlignmentTerms(forkPath, alignment.grep, (path) => view.changed.has(path))
|
|
2570
|
+
]);
|
|
2571
|
+
const pr = source?.pr ?? null;
|
|
2572
|
+
return {
|
|
2573
|
+
...alignment,
|
|
2574
|
+
kind: alignment.codemod ? "codemod" : "manual",
|
|
2575
|
+
open,
|
|
2576
|
+
url: alignmentUrl(view.githubUrl, ref, alignment.id) ?? null,
|
|
2577
|
+
commit: source?.commit ?? null,
|
|
2578
|
+
pr,
|
|
2579
|
+
prUrl: pr !== null && view.githubUrl ? `${view.githubUrl}/pull/${pr}` : null,
|
|
2580
|
+
sharedCommit: source?.sharedCommit ?? [],
|
|
2581
|
+
yourFiles: (source?.changedFiles ?? []).filter((path) => view.owns(path) || view.changed.has(path)),
|
|
2582
|
+
grepHits
|
|
2583
|
+
};
|
|
2584
|
+
}
|
|
2585
|
+
function grepSummary(alignment) {
|
|
2586
|
+
if (alignment.grep.length === 0) return null;
|
|
2587
|
+
const hits = alignment.grepHits.length;
|
|
2588
|
+
if (hits === 0) return "grep: no app hits";
|
|
2589
|
+
const files = new Set(alignment.grepHits.map((hit) => hit.file)).size;
|
|
2590
|
+
return `grep: ${hits} hit${hits === 1 ? "" : "s"} in ${files} app file${files === 1 ? "" : "s"}`;
|
|
2436
2591
|
}
|
|
2437
|
-
function
|
|
2592
|
+
function printAlignment(index, alignment) {
|
|
2438
2593
|
const tags = [
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2594
|
+
alignment.codemod ? "codemod" : "manual",
|
|
2595
|
+
alignment.syncBreaking ? "sync-breaking" : null,
|
|
2596
|
+
alignment.clientCacheBump ? "cache-bump" : null,
|
|
2597
|
+
alignment.caughtBy ? `caught by: ${alignment.caughtBy}` : null,
|
|
2598
|
+
alignment.generate ? "generate" : null
|
|
2442
2599
|
].filter(Boolean).join(", ");
|
|
2443
|
-
|
|
2444
|
-
console.info(colors_default.dim(`
|
|
2445
|
-
|
|
2446
|
-
console.info(
|
|
2447
|
-
if (
|
|
2448
|
-
|
|
2600
|
+
const grep = grepSummary(alignment);
|
|
2601
|
+
console.info(`${index}. ${alignment.title} ${colors_default.dim(`[${tags}]`)}`);
|
|
2602
|
+
console.info(colors_default.dim(` id: ${alignment.id}`));
|
|
2603
|
+
if (alignment.summary) console.info(` ${alignment.summary}`);
|
|
2604
|
+
if (alignment.pr !== null) {
|
|
2605
|
+
console.info(colors_default.dim(` added in #${alignment.pr}${alignment.prUrl ? ` ${alignment.prUrl}` : ""}`));
|
|
2606
|
+
}
|
|
2607
|
+
if (grep) console.info(colors_default.dim(` ${grep}`));
|
|
2608
|
+
console.info(colors_default.dim(` read: pnpm cella align --show ${alignment.id}`));
|
|
2609
|
+
if (alignment.url) console.info(colors_default.dim(` ${alignment.url}`));
|
|
2610
|
+
if (alignment.codemod) console.info(colors_default.dim(` codemod: pnpm cella align --run ${alignment.id}`));
|
|
2611
|
+
console.info();
|
|
2612
|
+
}
|
|
2613
|
+
function printShowBlock(title, items) {
|
|
2614
|
+
if (items.length === 0) return;
|
|
2615
|
+
console.info(colors_default.dim(`${title} (${items.length}):`));
|
|
2616
|
+
for (const item of items.slice(0, SHOW_LIST_MAX)) console.info(colors_default.dim(` ${item}`));
|
|
2617
|
+
if (items.length > SHOW_LIST_MAX) console.info(colors_default.dim(` ... + ${items.length - SHOW_LIST_MAX} more`));
|
|
2618
|
+
}
|
|
2619
|
+
async function showAlignment(config, ref, pending, id) {
|
|
2620
|
+
const { forkPath } = config;
|
|
2621
|
+
const upstreamBranch = `${DEFAULT_UPSTREAM_REMOTE}/${config.settings.upstreamBranch}`;
|
|
2622
|
+
let readRef = ref;
|
|
2623
|
+
let alignment = await readAlignment(forkPath, ref, id);
|
|
2624
|
+
if (!alignment && await revParse(forkPath, upstreamBranch)) {
|
|
2625
|
+
alignment = await readAlignment(forkPath, upstreamBranch, id);
|
|
2626
|
+
readRef = upstreamBranch;
|
|
2627
|
+
}
|
|
2628
|
+
if (!alignment) throw new Error(`no alignment '${id}' at upstream ${ref.slice(0, 7)} or ${upstreamBranch}`);
|
|
2629
|
+
const source = await git(["show", `${readRef}:${ALIGNMENTS_DIR}/${id}/README.md`], forkPath);
|
|
2630
|
+
const described = await describeAlignment(
|
|
2631
|
+
config,
|
|
2632
|
+
await readForkView(config, ref),
|
|
2633
|
+
readRef,
|
|
2634
|
+
alignment,
|
|
2635
|
+
pending.includes(id)
|
|
2636
|
+
);
|
|
2637
|
+
if (readRef !== ref) console.info(colors_default.dim(`not at the sync point ${ref.slice(0, 7)}: read at ${upstreamBranch}`));
|
|
2638
|
+
if (described.url) console.info(colors_default.dim(described.url));
|
|
2639
|
+
console.info(source);
|
|
2449
2640
|
console.info();
|
|
2641
|
+
if (described.commit) {
|
|
2642
|
+
const pr = described.pr !== null ? `#${described.pr} ` : "";
|
|
2643
|
+
console.info(
|
|
2644
|
+
colors_default.dim(`added in ${pr}(${described.commit.slice(0, 9)})${described.prUrl ? ` ${described.prUrl}` : ""}`)
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
printShowBlock("added in the same commit", described.sharedCommit);
|
|
2648
|
+
printShowBlock("files of that commit the app pins, ignores or changed", described.yourFiles);
|
|
2649
|
+
const grep = grepSummary(described);
|
|
2650
|
+
if (grep) console.info(colors_default.dim(`${grep} (${described.grep.join(", ")})`));
|
|
2651
|
+
printShowBlock(
|
|
2652
|
+
"grep hits",
|
|
2653
|
+
described.grepHits.map((hit) => `${hit.file}:${hit.line}`)
|
|
2654
|
+
);
|
|
2450
2655
|
}
|
|
2451
|
-
async function
|
|
2656
|
+
async function writeAlignmentFolder(config, ref, id) {
|
|
2452
2657
|
const { forkPath } = config;
|
|
2453
|
-
const
|
|
2454
|
-
if (!
|
|
2455
|
-
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${
|
|
2658
|
+
const alignment = await readAlignment(forkPath, ref, id);
|
|
2659
|
+
if (!alignment) throw new Error(`no alignment '${id}' at upstream ${ref.slice(0, 7)}`);
|
|
2660
|
+
const files = (await git(["ls-tree", "-r", "--name-only", `${ref}:${ALIGNMENTS_DIR}/${id}`], forkPath)).split("\n").filter(Boolean);
|
|
2456
2661
|
const target = join8(EXTRACT_DIR, id);
|
|
2457
2662
|
for (const file of files) {
|
|
2458
|
-
const content = execFileSync("git", ["show", `${ref}:${
|
|
2663
|
+
const content = execFileSync("git", ["show", `${ref}:${ALIGNMENTS_DIR}/${id}/${file}`], {
|
|
2459
2664
|
cwd: forkPath,
|
|
2460
2665
|
maxBuffer: MAX_BUFFER
|
|
2461
2666
|
});
|
|
@@ -2463,27 +2668,27 @@ async function writeNoteFolder(config, ref, id) {
|
|
|
2463
2668
|
await mkdir4(dirname4(path), { recursive: true });
|
|
2464
2669
|
await writeFile3(path, content);
|
|
2465
2670
|
}
|
|
2466
|
-
return {
|
|
2671
|
+
return { alignment, target, files };
|
|
2467
2672
|
}
|
|
2468
|
-
async function
|
|
2469
|
-
const {
|
|
2673
|
+
async function extractAlignment(config, ref, id) {
|
|
2674
|
+
const { alignment, target, files } = await writeAlignmentFolder(config, ref, id);
|
|
2470
2675
|
console.info(`${checkMark} extracted ${files.length} file(s) to ${target}/`);
|
|
2471
|
-
console.info(colors_default.dim(` the README's ${
|
|
2472
|
-
if (
|
|
2473
|
-
console.info(colors_default.dim(` pnpm exec tsx ${target}/${
|
|
2474
|
-
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella
|
|
2676
|
+
console.info(colors_default.dim(` the README's ${ALIGNMENTS_DIR}/${id}/ paths are this folder.`));
|
|
2677
|
+
if (alignment.codemod) {
|
|
2678
|
+
console.info(colors_default.dim(` pnpm exec tsx ${target}/${alignment.codemod} inventory ${alignment.roots.join(" ")}`));
|
|
2679
|
+
console.info(colors_default.dim(` or, keeping files identical to upstream as they are: pnpm cella align --run ${id}`));
|
|
2475
2680
|
}
|
|
2476
2681
|
}
|
|
2477
|
-
async function
|
|
2682
|
+
async function runAlignment(config, ref, id) {
|
|
2478
2683
|
const { forkPath } = config;
|
|
2479
|
-
const {
|
|
2480
|
-
if (!
|
|
2684
|
+
const { alignment, target } = await writeAlignmentFolder(config, ref, id);
|
|
2685
|
+
if (!alignment.codemod) {
|
|
2481
2686
|
throw new Error(
|
|
2482
|
-
`
|
|
2687
|
+
`alignment '${id}' ships no codemod script: its steps are manual, read them with \`pnpm cella align --show ${id}\`.`
|
|
2483
2688
|
);
|
|
2484
2689
|
}
|
|
2485
|
-
const script = join8(target,
|
|
2486
|
-
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...
|
|
2690
|
+
const script = join8(target, alignment.codemod);
|
|
2691
|
+
const args = config.runArgs?.length ? config.runArgs : ["inventory", ...alignment.roots];
|
|
2487
2692
|
const upstreamCommit = await readManifestBase(forkPath) ?? ref;
|
|
2488
2693
|
await ensureUpstreamCommit(config, upstreamCommit);
|
|
2489
2694
|
const identical = await listIdenticalToRef(forkPath, upstreamCommit);
|
|
@@ -2508,92 +2713,335 @@ async function runNote(config, ref, id) {
|
|
|
2508
2713
|
}
|
|
2509
2714
|
if (child.status !== 0) throw new Error(`the codemod exited with code ${child.status ?? "unknown"}`);
|
|
2510
2715
|
if (!config.runArgs?.length) {
|
|
2511
|
-
const apply = ["rewrite", ...
|
|
2512
|
-
console.info(colors_default.dim(`report only. to apply: pnpm cella
|
|
2716
|
+
const apply = ["rewrite", ...alignment.roots].join(" ");
|
|
2717
|
+
console.info(colors_default.dim(`report only. to apply: pnpm cella align --run ${id} -- ${apply}`));
|
|
2513
2718
|
}
|
|
2514
2719
|
}
|
|
2515
|
-
async function
|
|
2720
|
+
async function markAlignments(config, ref, pending, ids) {
|
|
2516
2721
|
const open = new Set(pending);
|
|
2517
2722
|
const unknown = ids.filter((id) => !open.has(id));
|
|
2518
2723
|
for (const id of ids) open.delete(id);
|
|
2519
2724
|
await writePending(config.forkPath, [...open]);
|
|
2520
2725
|
const marked = ids.length - unknown.length;
|
|
2521
|
-
if (marked > 0) console.info(`${checkMark} recorded ${marked}
|
|
2726
|
+
if (marked > 0) console.info(`${checkMark} recorded ${marked} alignment(s) as handled`);
|
|
2522
2727
|
if (unknown.length > 0) console.info(colors_default.dim(`not pending, left as is: ${unknown.join(", ")}`));
|
|
2523
|
-
console.info(colors_default.dim(
|
|
2728
|
+
console.info(colors_default.dim(formatAlignmentsLine((await listAlignmentIds(config.forkPath, ref)).length, open.size)));
|
|
2524
2729
|
}
|
|
2525
|
-
async function
|
|
2730
|
+
async function runAlign(config) {
|
|
2526
2731
|
const { forkPath } = config;
|
|
2527
|
-
const ref = await
|
|
2732
|
+
const ref = await resolveAlignmentsRef(config);
|
|
2528
2733
|
if (!ref) {
|
|
2529
|
-
console.info(colors_default.dim("no sync point recorded yet:
|
|
2734
|
+
console.info(colors_default.dim("no sync point recorded yet: alignments start with the first `pnpm cella sync`."));
|
|
2530
2735
|
return;
|
|
2531
2736
|
}
|
|
2532
2737
|
const pending = await readPending(forkPath);
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
if (config.
|
|
2536
|
-
if (config.
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
});
|
|
2542
|
-
if (!source) throw new Error(`no migration note '${config.show}' at upstream ${ref.slice(0, 7)}`);
|
|
2543
|
-
const url = urlOf(config.show);
|
|
2544
|
-
if (url) console.info(colors_default.dim(url));
|
|
2545
|
-
console.info(source);
|
|
2546
|
-
return;
|
|
2547
|
-
}
|
|
2548
|
-
const ids = config.all ? await listNoteIds(forkPath, ref) : pending;
|
|
2549
|
-
const notes = (await Promise.all(ids.map((id) => readNote(forkPath, ref, id)))).map(
|
|
2550
|
-
(note, i) => note ?? {
|
|
2738
|
+
if (config.mark?.length) return markAlignments(config, ref, pending, config.mark);
|
|
2739
|
+
if (config.run) return runAlignment(config, ref, config.run);
|
|
2740
|
+
if (config.extract) return extractAlignment(config, ref, config.extract);
|
|
2741
|
+
if (config.show) return showAlignment(config, ref, pending, config.show);
|
|
2742
|
+
const ids = config.all ? await listAlignmentIds(forkPath, ref) : pending;
|
|
2743
|
+
const read = await Promise.all(ids.map((id) => readAlignment(forkPath, ref, id)));
|
|
2744
|
+
const alignments = read.map(
|
|
2745
|
+
(alignment, i) => alignment ?? {
|
|
2551
2746
|
id: ids[i],
|
|
2552
2747
|
title: `${ids[i]} (no longer upstream)`,
|
|
2553
2748
|
summary: "",
|
|
2554
2749
|
syncBreaking: false,
|
|
2555
2750
|
clientCacheBump: false,
|
|
2751
|
+
caughtBy: null,
|
|
2752
|
+
generate: false,
|
|
2753
|
+
grep: [],
|
|
2556
2754
|
roots: [],
|
|
2557
2755
|
codemod: null
|
|
2558
2756
|
}
|
|
2559
2757
|
);
|
|
2560
|
-
const total = (await
|
|
2758
|
+
const total = (await listAlignmentIds(forkPath, ref)).length;
|
|
2759
|
+
const open = new Set(pending);
|
|
2760
|
+
const view = await readForkView(config, ref);
|
|
2761
|
+
const described = await Promise.all(
|
|
2762
|
+
alignments.map((alignment) => describeAlignment(config, view, ref, alignment, open.has(alignment.id)))
|
|
2763
|
+
);
|
|
2561
2764
|
if (config.json) {
|
|
2562
|
-
|
|
2563
|
-
const out = {
|
|
2564
|
-
upstreamCommit: ref,
|
|
2565
|
-
total,
|
|
2566
|
-
open: pending.length,
|
|
2567
|
-
notes: notes.map((note) => ({
|
|
2568
|
-
...note,
|
|
2569
|
-
kind: note.codemod ? "codemod" : "manual",
|
|
2570
|
-
open: open.has(note.id),
|
|
2571
|
-
url: urlOf(note.id) ?? null
|
|
2572
|
-
}))
|
|
2573
|
-
};
|
|
2574
|
-
writeStdout(JSON.stringify(out, null, 2));
|
|
2765
|
+
writeStdout(JSON.stringify({ upstreamCommit: ref, total, open: pending.length, alignments: described }, null, 2));
|
|
2575
2766
|
return;
|
|
2576
2767
|
}
|
|
2577
|
-
console.info(colors_default.dim(
|
|
2578
|
-
if (
|
|
2768
|
+
console.info(colors_default.dim(formatAlignmentsLine(total, pending.length)));
|
|
2769
|
+
if (described.length === 0) return;
|
|
2579
2770
|
console.info();
|
|
2580
|
-
for (const [i,
|
|
2771
|
+
for (const [i, alignment] of described.entries()) printAlignment(i + 1, alignment);
|
|
2772
|
+
const schema = described.filter((alignment) => alignment.open && alignment.generate);
|
|
2773
|
+
if (schema.length > 1) {
|
|
2774
|
+
console.info(
|
|
2775
|
+
colors_default.dim(
|
|
2776
|
+
`${schema.length} open alignments change the schema: work their code steps first, then run pnpm generate once.`
|
|
2777
|
+
)
|
|
2778
|
+
);
|
|
2779
|
+
}
|
|
2581
2780
|
if (pending.length > 0) {
|
|
2582
|
-
console.info(colors_default.dim("work each
|
|
2583
|
-
console.info(colors_default.dim(` pnpm cella
|
|
2781
|
+
console.info(colors_default.dim("work each alignment's README, check with `pnpm check`, then record it:"));
|
|
2782
|
+
console.info(colors_default.dim(` pnpm cella align --mark ${pending.join(" ")}`));
|
|
2584
2783
|
}
|
|
2585
2784
|
}
|
|
2586
2785
|
|
|
2587
|
-
// src/services/
|
|
2786
|
+
// src/services/analyze-report.ts
|
|
2588
2787
|
import { basename as basename2 } from "path";
|
|
2589
|
-
|
|
2788
|
+
var BEHIND_FLAT_MAX = 15;
|
|
2789
|
+
var AHEAD_FLAT_MAX = 8;
|
|
2790
|
+
var ABSENT_SHOWN_MIN = 10;
|
|
2791
|
+
var ENTRY_COLLAPSE_MIN = 5;
|
|
2792
|
+
var SECTION_ROWS_MAX = 100;
|
|
2793
|
+
var UPSTREAM_CHANGE = { fields: "changed", repo: "upstream" };
|
|
2794
|
+
var UPSTREAM_SIDE = { fields: "upstream", repo: "upstream" };
|
|
2795
|
+
var FORK_SIDE = { fields: "changed", repo: "fork" };
|
|
2796
|
+
function cell(value, width, color, align) {
|
|
2797
|
+
if (!value) return " ".repeat(width);
|
|
2798
|
+
return padVisible(color ? color(value) : value, width, align === "end" ? "end" : "start");
|
|
2799
|
+
}
|
|
2800
|
+
function formatRows(rows, caption) {
|
|
2801
|
+
const iconWidth = rows.some((row) => row.icon) ? 2 : 0;
|
|
2802
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length + (row.note ? row.note.length + 1 : 0)));
|
|
2803
|
+
const countWidth = Math.max(0, ...rows.map((row) => row.count?.length ?? 0));
|
|
2804
|
+
const tagWidth = Math.max(0, ...rows.map((row) => row.tag?.length ?? 0));
|
|
2805
|
+
const ageWidth = Math.max(0, ...rows.map((row) => row.age?.length ?? 0));
|
|
2806
|
+
const lines = [];
|
|
2807
|
+
if (caption && countWidth > 0) lines.push(colors_default.dim(caption.padStart(2 + iconWidth + labelWidth + 2 + countWidth)));
|
|
2808
|
+
for (const row of rows) {
|
|
2809
|
+
const icon = iconWidth ? cell(row.icon, iconWidth) : "";
|
|
2810
|
+
const label = `${hyperlink(row.label, row.link)}${row.note ? ` ${colors_default.dim(row.note)}` : ""}`;
|
|
2811
|
+
const cells = [` ${icon}${padVisible(label, labelWidth)}`];
|
|
2812
|
+
if (countWidth) cells.push(cell(row.count, countWidth, row.countColor, "end"));
|
|
2813
|
+
if (tagWidth) cells.push(cell(row.tag, tagWidth, row.tagColor));
|
|
2814
|
+
if (ageWidth) cells.push(cell(row.age, ageWidth, colors_default.dim, "end"));
|
|
2815
|
+
lines.push(cells.join(" ").trimEnd());
|
|
2816
|
+
}
|
|
2817
|
+
return lines;
|
|
2818
|
+
}
|
|
2819
|
+
function formatCappedRows(rows, caption) {
|
|
2820
|
+
if (rows.length <= SECTION_ROWS_MAX) return formatRows(rows, caption);
|
|
2821
|
+
return [
|
|
2822
|
+
...formatRows(rows.slice(0, SECTION_ROWS_MAX), caption),
|
|
2823
|
+
colors_default.dim(` + ${rows.length - SECTION_ROWS_MAX} more`)
|
|
2824
|
+
];
|
|
2825
|
+
}
|
|
2826
|
+
function hint(text, command = "") {
|
|
2827
|
+
return ` ${colors_default.dim(text)}${command}`;
|
|
2828
|
+
}
|
|
2829
|
+
function printSection(title, count3, body, hints = []) {
|
|
2830
|
+
printSectionHeader(`${title} ${colors_default.dim(`\xB7 ${plural(count3, "file")}`)}`);
|
|
2831
|
+
for (const line of body) console.info(line);
|
|
2832
|
+
if (hints.length === 0) return;
|
|
2833
|
+
console.info();
|
|
2834
|
+
for (const line of hints) console.info(line);
|
|
2835
|
+
}
|
|
2836
|
+
function byPath(a, b) {
|
|
2837
|
+
return a.path.localeCompare(b.path);
|
|
2838
|
+
}
|
|
2839
|
+
function fileLink(file, side, links) {
|
|
2840
|
+
const { fileLinkMode = "commit", upstreamGitHubUrl, upstreamSha, forkGitHubUrl } = links;
|
|
2841
|
+
if (fileLinkMode === "file") {
|
|
2842
|
+
if (!upstreamGitHubUrl || !upstreamSha || !file.existsInUpstream) return void 0;
|
|
2843
|
+
return `${upstreamGitHubUrl}/blob/${upstreamSha}/${file.path}`;
|
|
2844
|
+
}
|
|
2845
|
+
const commit = side.fields === "upstream" ? file.upstreamCommit : file.changedCommit;
|
|
2846
|
+
const repo = side.repo === "upstream" ? upstreamGitHubUrl : forkGitHubUrl;
|
|
2847
|
+
return commit && repo ? `${repo}/commit/${commit}` : void 0;
|
|
2848
|
+
}
|
|
2849
|
+
function changeTimestamp(file, side) {
|
|
2850
|
+
return side.fields === "upstream" ? file.upstreamChangedTs : file.changedTs;
|
|
2851
|
+
}
|
|
2852
|
+
function changeKind(file) {
|
|
2853
|
+
if (!file.existsInFork) return "new";
|
|
2854
|
+
if (!file.existsInUpstream) return "deleted";
|
|
2855
|
+
return "changed";
|
|
2856
|
+
}
|
|
2857
|
+
var KIND_TAG = { new: { tag: "new", color: colors_default.green }, deleted: { tag: "deleted", color: colors_default.red } };
|
|
2858
|
+
function fileRow(file, side, options, extra = {}) {
|
|
2859
|
+
const timestamp = changeTimestamp(file, side);
|
|
2860
|
+
const kind = changeKind(file);
|
|
2861
|
+
return {
|
|
2862
|
+
label: file.path,
|
|
2863
|
+
link: fileLink(file, side, options.links),
|
|
2864
|
+
age: timestamp ? shortAge(timestamp, options.nowMs) : void 0,
|
|
2865
|
+
...kind === "changed" ? {} : { tag: KIND_TAG[kind].tag, tagColor: KIND_TAG[kind].color },
|
|
2866
|
+
...extra
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function commonFolder(paths) {
|
|
2870
|
+
const folders = paths.map((path) => path.split("/").slice(0, -1));
|
|
2871
|
+
const [first] = folders;
|
|
2872
|
+
let depth = 0;
|
|
2873
|
+
while (depth < first.length && folders.every((parts) => parts[depth] === first[depth])) depth++;
|
|
2874
|
+
return first.slice(0, depth).join("/");
|
|
2875
|
+
}
|
|
2876
|
+
function formatFolderRows(files, columns) {
|
|
2877
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2878
|
+
for (const file of files) {
|
|
2879
|
+
const top = file.path.includes("/") ? file.path.split("/")[0] : file.path;
|
|
2880
|
+
groups.set(top, [...groups.get(top) ?? [], file]);
|
|
2881
|
+
}
|
|
2882
|
+
const rows = [...groups.values()].map((group) => ({
|
|
2883
|
+
label: group.length === 1 ? group[0].path : `${commonFolder(group.map((file) => file.path))}/`,
|
|
2884
|
+
files: group
|
|
2885
|
+
})).sort((a, b) => b.files.length - a.files.length || a.label.localeCompare(b.label));
|
|
2886
|
+
const labelWidth = Math.max(...rows.map((row) => row.label.length));
|
|
2887
|
+
const countWidth = Math.max(...rows.map((row) => String(row.files.length).length));
|
|
2888
|
+
const order = { new: 0, deleted: 1, changed: 2 };
|
|
2889
|
+
return rows.map((row) => {
|
|
2890
|
+
let line = ` ${row.label.padEnd(labelWidth)} ${colors_default.cyan(String(row.files.length).padStart(countWidth))}`;
|
|
2891
|
+
if (row.files.length === 1) return line.trimEnd();
|
|
2892
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2893
|
+
for (const file of row.files) seen.set(basename2(file.path), (seen.get(basename2(file.path)) ?? 0) + 1);
|
|
2894
|
+
const names = row.files.map((file) => ({
|
|
2895
|
+
kind: changeKind(file),
|
|
2896
|
+
name: (seen.get(basename2(file.path)) ?? 0) > 1 ? file.path.slice(row.label.length) : basename2(file.path)
|
|
2897
|
+
})).sort((a, b) => order[a.kind] - order[b.kind] || a.name.localeCompare(b.name));
|
|
2898
|
+
let width = visibleWidth(line);
|
|
2899
|
+
for (const { kind, name } of names) {
|
|
2900
|
+
const text = kind === "new" ? `+${name}` : kind === "deleted" ? `\u2212${name}` : name;
|
|
2901
|
+
if (width + 2 + text.length > columns - 2) {
|
|
2902
|
+
line += colors_default.dim(" \u2026");
|
|
2903
|
+
break;
|
|
2904
|
+
}
|
|
2905
|
+
line += ` ${kind === "new" ? colors_default.green(text) : kind === "deleted" ? colors_default.red(text) : colors_default.dim(text)}`;
|
|
2906
|
+
width += 2 + text.length;
|
|
2907
|
+
}
|
|
2908
|
+
return line;
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2911
|
+
function printBehind(files, options, columns) {
|
|
2912
|
+
const behind = filesWithStatus(files, "behind").sort(byPath);
|
|
2913
|
+
if (behind.length === 0) return;
|
|
2914
|
+
const title = colors_default.cyan("\u2193 behind on upstream");
|
|
2915
|
+
if (options.all || behind.length <= BEHIND_FLAT_MAX) {
|
|
2916
|
+
printSection(title, behind.length, formatCappedRows(behind.map((file) => fileRow(file, UPSTREAM_CHANGE, options))));
|
|
2917
|
+
return;
|
|
2918
|
+
}
|
|
2919
|
+
printSection(title, behind.length, formatFolderRows(behind, columns), [
|
|
2920
|
+
hint("every path: ", "pnpm cella analyze --all")
|
|
2921
|
+
]);
|
|
2922
|
+
}
|
|
2923
|
+
function printFlat(files, title, side, options) {
|
|
2924
|
+
if (files.length === 0) return;
|
|
2925
|
+
const rows = [...files].sort(byPath).map((file) => fileRow(file, side, options));
|
|
2926
|
+
printSection(title, files.length, formatCappedRows(rows));
|
|
2927
|
+
}
|
|
2928
|
+
function absentLines(file) {
|
|
2929
|
+
return file.upstreamLinesAbsent ?? 0;
|
|
2930
|
+
}
|
|
2931
|
+
function describeAbsent(files) {
|
|
2932
|
+
const counts = files.map(absentLines);
|
|
2933
|
+
const missing = counts.filter((count3) => count3 > 0);
|
|
2934
|
+
const parts = [];
|
|
2935
|
+
if (missing.length > 0) {
|
|
2936
|
+
const [low, high] = [Math.min(...missing), Math.max(...missing)];
|
|
2937
|
+
parts.push(`${missing.length} with ${low === high ? plural(low, "line") : `${low}\u2013${high} lines`} absent`);
|
|
2938
|
+
}
|
|
2939
|
+
if (missing.length < counts.length) parts.push(`${counts.length - missing.length} with none`);
|
|
2940
|
+
return parts.join(", ");
|
|
2941
|
+
}
|
|
2942
|
+
function printAhead(files, options) {
|
|
2943
|
+
const ahead = filesWithStatus(files, "ahead").sort((a, b) => absentLines(b) - absentLines(a) || byPath(a, b));
|
|
2944
|
+
if (ahead.length === 0) return;
|
|
2945
|
+
let shown = ahead;
|
|
2946
|
+
if (!options.all && ahead.length > AHEAD_FLAT_MAX) {
|
|
2947
|
+
const many = ahead.filter((file) => absentLines(file) >= ABSENT_SHOWN_MIN);
|
|
2948
|
+
shown = many.length > 0 ? many : ahead.slice(0, 5);
|
|
2949
|
+
}
|
|
2950
|
+
const rest = ahead.slice(shown.length);
|
|
2951
|
+
const rows = shown.map(
|
|
2952
|
+
(file) => fileRow(file, FORK_SIDE, options, {
|
|
2953
|
+
count: absentLines(file) > 0 ? String(absentLines(file)) : void 0,
|
|
2954
|
+
countColor: colors_default.yellow
|
|
2955
|
+
})
|
|
2956
|
+
);
|
|
2957
|
+
const body = formatCappedRows(rows, "upstream lines absent");
|
|
2958
|
+
if (rest.length > 0) body.push(colors_default.dim(` + ${rest.length} more: ${describeAbsent(rest)}`));
|
|
2959
|
+
const hints = ahead.some((file) => absentLines(file) > 0) ? [hint("what a merge would bring in: ", "pnpm cella analyze --unpinned")] : [];
|
|
2960
|
+
printSection(colors_default.blue("\u2191 protected in fork"), ahead.length, body, hints);
|
|
2961
|
+
}
|
|
2962
|
+
function entryFinder(overrides) {
|
|
2963
|
+
const entries = [...overrides?.ignored ?? [], ...overrides?.pinned ?? []].map(normalizeEntry).filter(Boolean).sort((a, b) => b.length - a.length);
|
|
2964
|
+
return (path) => entries.find((entry) => isUnderAnyFolder(path, [entry]));
|
|
2965
|
+
}
|
|
2966
|
+
var KIND_ORDER = { new: 0, changed: 1, deleted: 2 };
|
|
2967
|
+
function protectedRow(items, options) {
|
|
2968
|
+
const [{ file, kind, entry }] = items;
|
|
2969
|
+
const lines = items.map((item) => item.file.upstreamChangedLines);
|
|
2970
|
+
const total = lines.every((count3) => count3 !== void 0) ? lines.reduce((sum, count3) => sum + (count3 ?? 0), 0) : void 0;
|
|
2971
|
+
const sign = kind === "new" ? "+" : kind === "deleted" ? "\u2212" : "";
|
|
2972
|
+
const newest = Math.max(0, ...items.map((item) => changeTimestamp(item.file, UPSTREAM_SIDE) ?? 0));
|
|
2973
|
+
const row = {
|
|
2974
|
+
label: file.path,
|
|
2975
|
+
link: fileLink(file, UPSTREAM_SIDE, options.links),
|
|
2976
|
+
icon: protectionIcon(file),
|
|
2977
|
+
count: total === void 0 ? void 0 : `${sign}${compactCount(total)}`,
|
|
2978
|
+
countColor: kind === "new" ? colors_default.green : kind === "deleted" ? colors_default.red : colors_default.yellow,
|
|
2979
|
+
...kind === "changed" ? {} : { tag: KIND_TAG[kind].tag, tagColor: KIND_TAG[kind].color },
|
|
2980
|
+
age: newest ? shortAge(newest, options.nowMs) : void 0
|
|
2981
|
+
};
|
|
2982
|
+
if (items.length === 1 || !entry) return row;
|
|
2983
|
+
const names = new Set(items.map((item) => basename2(item.file.path)));
|
|
2984
|
+
const { upstreamGitHubUrl, upstreamSha } = options.links;
|
|
2985
|
+
return {
|
|
2986
|
+
...row,
|
|
2987
|
+
label: `${entry}/`,
|
|
2988
|
+
note: names.size === 1 ? `\xB7 ${items.length} \xD7 ${[...names][0]}` : `\xB7 ${items.length} files`,
|
|
2989
|
+
link: upstreamGitHubUrl && upstreamSha ? `${upstreamGitHubUrl}/tree/${upstreamSha}/${entry}` : void 0
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
function printProtectedBehind(result, options) {
|
|
2993
|
+
const findEntry = entryFinder(options.overrides);
|
|
2994
|
+
const items = findProtectedBehindUpstream(result).map((file) => ({
|
|
2995
|
+
file,
|
|
2996
|
+
kind: file.upstreamOnly ? changeKind(file) : "changed",
|
|
2997
|
+
entry: findEntry(file.path)
|
|
2998
|
+
}));
|
|
2999
|
+
if (items.length === 0) return;
|
|
3000
|
+
const groups = /* @__PURE__ */ new Map();
|
|
3001
|
+
for (const item of items) {
|
|
3002
|
+
const key = options.all || !item.entry ? item.file.path : `${item.kind}\0${item.entry}`;
|
|
3003
|
+
groups.set(key, [...groups.get(key) ?? [], item]);
|
|
3004
|
+
}
|
|
3005
|
+
const single = (group) => group.length < ENTRY_COLLAPSE_MIN;
|
|
3006
|
+
const lines = (group) => group.reduce((sum, item) => sum + (item.file.upstreamChangedLines ?? 0), 0);
|
|
3007
|
+
const ordered = [...groups.values()].flatMap((group) => single(group) ? group.map((item) => [item]) : [group]).sort(
|
|
3008
|
+
(a, b) => Number(a.length > 1) - Number(b.length > 1) || KIND_ORDER[a[0].kind] - KIND_ORDER[b[0].kind] || lines(b) - lines(a) || byPath(a[0].file, b[0].file)
|
|
3009
|
+
);
|
|
3010
|
+
printSection(
|
|
3011
|
+
`${warningMark} ${colors_default.yellow("protected but behind upstream")}`,
|
|
3012
|
+
items.length,
|
|
3013
|
+
formatCappedRows(
|
|
3014
|
+
ordered.map((group) => protectedRow(group, options)),
|
|
3015
|
+
"lines changed upstream"
|
|
3016
|
+
),
|
|
3017
|
+
[hint("the fork side wins: adopt what you need from the review page")]
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
function printAnalysisFileGroups(result, options) {
|
|
3021
|
+
const columns = options.columns ?? terminalColumns();
|
|
3022
|
+
printBehind(result.files, options, columns);
|
|
3023
|
+
printFlat(filesWithStatus(result.files, "diverged"), colors_default.magenta("\u21C5 diverged"), UPSTREAM_SIDE, options);
|
|
3024
|
+
printAhead(result.files, options);
|
|
3025
|
+
printFlat(
|
|
3026
|
+
filesWithStatus(result.files, "drifted"),
|
|
3027
|
+
`${warningMark} ${colors_default.yellow("drifted from upstream")}`,
|
|
3028
|
+
FORK_SIDE,
|
|
3029
|
+
options
|
|
3030
|
+
);
|
|
3031
|
+
printProtectedBehind(result, options);
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
// src/services/review-page.ts
|
|
3035
|
+
import { existsSync as existsSync7, rmSync } from "fs";
|
|
3036
|
+
import { basename as basename3 } from "path";
|
|
3037
|
+
import process9 from "process";
|
|
2590
3038
|
import { pathToFileURL } from "url";
|
|
2591
3039
|
|
|
2592
3040
|
// src/utils/diff.ts
|
|
2593
3041
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
2594
3042
|
import { mkdirSync, writeFileSync } from "fs";
|
|
2595
|
-
import { join as join9 } from "path";
|
|
2596
|
-
import
|
|
3043
|
+
import { dirname as dirname5, join as join9 } from "path";
|
|
3044
|
+
import process8 from "process";
|
|
2597
3045
|
function gitDiffFile(cwd, range, filePath) {
|
|
2598
3046
|
try {
|
|
2599
3047
|
return execFileSync2("git", ["diff", "--no-color", range, "--", filePath], { cwd, maxBuffer: MAX_BUFFER });
|
|
@@ -2628,9 +3076,9 @@ ${prerenderedHTML}
|
|
|
2628
3076
|
for (const file of section.files) files.push(await renderFile(file));
|
|
2629
3077
|
const count3 = section.files.length + (section.omitted?.length ?? 0);
|
|
2630
3078
|
const title = section.title ? `<h2>${escapeHtml(section.title)} <span class="count">\xB7 ${count3} ${count3 === 1 ? "file" : "files"}</span></h2>` : "";
|
|
2631
|
-
const
|
|
3079
|
+
const hint2 = section.hint ? `<p class="hint">${escapeHtml(section.hint)}</p>` : "";
|
|
2632
3080
|
const omitted = section.omitted?.length ? `<p class="hint">Not shown here (binary, too large, or past the page limit):</p><ul class="omitted">${section.omitted.map((path) => `<li><code>${escapeHtml(path)}</code></li>`).join("")}</ul>` : "";
|
|
2633
|
-
blocks.push(`<section id="section-${index2 + 1}">${title}${
|
|
3081
|
+
blocks.push(`<section id="section-${index2 + 1}">${title}${hint2}${files.join("\n")}${omitted}</section>`);
|
|
2634
3082
|
}
|
|
2635
3083
|
const titled = sections.filter((section) => section.title);
|
|
2636
3084
|
const index = titled.length > 1 ? `<nav><ol>${sections.map(
|
|
@@ -2694,16 +3142,18 @@ ${blocks.join("\n")}
|
|
|
2694
3142
|
}
|
|
2695
3143
|
async function writeDiffPage(sections, meta, repoPath, name) {
|
|
2696
3144
|
const html = await renderDiffPage(sections, meta);
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
mkdirSync(dir, { recursive: true });
|
|
2700
|
-
const outPath = join9(dir, `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3145
|
+
const outPath = diffPagePath(repoPath, name);
|
|
3146
|
+
mkdirSync(dirname5(outPath), { recursive: true });
|
|
2701
3147
|
writeFileSync(outPath, html);
|
|
2702
3148
|
return outPath;
|
|
2703
3149
|
}
|
|
3150
|
+
function diffPagePath(repoPath, name) {
|
|
3151
|
+
const gitDir = execFileSync2("git", ["rev-parse", "--absolute-git-dir"], { cwd: repoPath, encoding: "utf8" }).trim();
|
|
3152
|
+
return join9(gitDir, "cella-diff", `${name.replace(/[^\w.-]+/g, "-")}.html`);
|
|
3153
|
+
}
|
|
2704
3154
|
function openInBrowser(target) {
|
|
2705
3155
|
try {
|
|
2706
|
-
execFileSync2(
|
|
3156
|
+
execFileSync2(process8.platform === "darwin" ? "open" : "xdg-open", [target], { stdio: "ignore" });
|
|
2707
3157
|
return true;
|
|
2708
3158
|
} catch {
|
|
2709
3159
|
return false;
|
|
@@ -2711,38 +3161,39 @@ function openInBrowser(target) {
|
|
|
2711
3161
|
}
|
|
2712
3162
|
|
|
2713
3163
|
// src/services/review-page.ts
|
|
2714
|
-
var
|
|
3164
|
+
var PAGE_LIMITS = { files: 50, lines: 15e3, patchLines: 2e3 };
|
|
2715
3165
|
var MAX_PATCH_BYTES = 512 * 1024;
|
|
2716
3166
|
function reviewSections(result, command) {
|
|
2717
3167
|
const [base, upstream] = result.upstreamDiffRange?.split("..") ?? [];
|
|
2718
3168
|
if (!base || !upstream) return [];
|
|
2719
3169
|
const sinceSync = `${base}..${upstream}`;
|
|
2720
|
-
const
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
hint: "What upstream changed in these files since the last sync. They are pinned or ignored, so the sync keeps the fork version and none of it arrives: adopt what you need.",
|
|
2728
|
-
range: sinceSync,
|
|
2729
|
-
paths: findProtectedBehind(result.files).map((file) => file.path)
|
|
2730
|
-
};
|
|
2731
|
-
const sections = [
|
|
2732
|
-
dropped,
|
|
3170
|
+
const sections = command === "sync" ? [
|
|
3171
|
+
{
|
|
3172
|
+
title: "protected files kept the fork version",
|
|
3173
|
+
hint: "What upstream changed in these files since the last sync. The fork kept its own version, so none of it arrived: adopt what you need.",
|
|
3174
|
+
range: sinceSync,
|
|
3175
|
+
paths: result.protectedConflicts ?? []
|
|
3176
|
+
},
|
|
2733
3177
|
{
|
|
2734
3178
|
title: "ignored paths changed upstream",
|
|
2735
3179
|
hint: "What upstream changed under ignored paths since the last sync. The fork left these files untouched, and ignored paths never sync.",
|
|
2736
3180
|
range: sinceSync,
|
|
2737
3181
|
paths: (result.ignoredUpstreamChanges ?? []).flatMap((group) => group.paths)
|
|
2738
|
-
}
|
|
3182
|
+
}
|
|
3183
|
+
] : [
|
|
2739
3184
|
{
|
|
2740
|
-
title: "
|
|
2741
|
-
hint: "What upstream changed in these files since the last sync.
|
|
3185
|
+
title: "protected but behind upstream",
|
|
3186
|
+
hint: "What upstream changed in these files since the last sync. They are pinned or ignored, so the sync keeps the fork version and none of it arrives: adopt what you need.",
|
|
2742
3187
|
range: sinceSync,
|
|
2743
|
-
paths:
|
|
3188
|
+
paths: findProtectedBehindUpstream(result).map((file) => file.path)
|
|
2744
3189
|
}
|
|
2745
3190
|
];
|
|
3191
|
+
sections.push({
|
|
3192
|
+
title: "pinned files that match the previous upstream",
|
|
3193
|
+
hint: "What upstream changed in these files since the last sync. The fork copy still equals the previous upstream, so the pin only drops these changes: unpin to take them.",
|
|
3194
|
+
range: sinceSync,
|
|
3195
|
+
paths: findMaskingPins(result.files).map((file) => file.path)
|
|
3196
|
+
});
|
|
2746
3197
|
if (command === "analyze") {
|
|
2747
3198
|
sections.push(
|
|
2748
3199
|
{
|
|
@@ -2761,55 +3212,100 @@ function reviewSections(result, command) {
|
|
|
2761
3212
|
}
|
|
2762
3213
|
return sections.filter((section) => section.paths.length > 0);
|
|
2763
3214
|
}
|
|
2764
|
-
|
|
3215
|
+
function countLines(patch) {
|
|
3216
|
+
let count3 = 0;
|
|
3217
|
+
for (let at = patch.indexOf(10); at !== -1; at = patch.indexOf(10, at + 1)) count3++;
|
|
3218
|
+
return count3;
|
|
3219
|
+
}
|
|
3220
|
+
async function writeReviewPage(forkPath, sections, meta, limits = PAGE_LIMITS) {
|
|
2765
3221
|
const [base, upstream] = meta.range.split("..");
|
|
3222
|
+
const pageSections = sections.map((section) => ({
|
|
3223
|
+
title: section.title,
|
|
3224
|
+
hint: section.hint,
|
|
3225
|
+
files: [],
|
|
3226
|
+
omitted: []
|
|
3227
|
+
}));
|
|
3228
|
+
const queues = sections.map((section) => [...section.paths]);
|
|
2766
3229
|
let diffs = 0;
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
const
|
|
2770
|
-
|
|
2771
|
-
if (
|
|
2772
|
-
|
|
3230
|
+
let lines = 0;
|
|
3231
|
+
while (queues.some((queue) => queue.length > 0)) {
|
|
3232
|
+
for (const [index, queue] of queues.entries()) {
|
|
3233
|
+
const path = queue.shift();
|
|
3234
|
+
if (path === void 0) continue;
|
|
3235
|
+
const pageSection = pageSections[index];
|
|
3236
|
+
if (diffs >= limits.files || lines >= limits.lines) {
|
|
3237
|
+
pageSection.omitted.push(path);
|
|
2773
3238
|
continue;
|
|
2774
3239
|
}
|
|
2775
|
-
const patch = gitDiffFile(forkPath,
|
|
3240
|
+
const patch = gitDiffFile(forkPath, sections[index].range, path);
|
|
2776
3241
|
if (patch.length === 0) continue;
|
|
2777
|
-
|
|
2778
|
-
|
|
3242
|
+
const patchLines = patch.length > MAX_PATCH_BYTES ? Number.POSITIVE_INFINITY : countLines(patch);
|
|
3243
|
+
if (patchLines > limits.patchLines || lines + patchLines > limits.lines || !patch.includes("\n@@ ")) {
|
|
3244
|
+
pageSection.omitted.push(path);
|
|
2779
3245
|
continue;
|
|
2780
3246
|
}
|
|
2781
3247
|
pageSection.files.push({ path, patch: patch.toString() });
|
|
2782
3248
|
diffs++;
|
|
3249
|
+
lines += patchLines;
|
|
2783
3250
|
}
|
|
2784
|
-
if (pageSection.files.length > 0 || pageSection.omitted?.length) pageSections.push(pageSection);
|
|
2785
3251
|
}
|
|
2786
3252
|
if (diffs === 0) return void 0;
|
|
3253
|
+
const shown = pageSections.filter((section) => section.files.length > 0 || section.omitted.length > 0);
|
|
2787
3254
|
const page = await writeDiffPage(
|
|
2788
|
-
|
|
2789
|
-
{ title: `${meta.command} review`, note: `${
|
|
3255
|
+
shown,
|
|
3256
|
+
{ title: `${meta.command} review`, note: `${basename3(forkPath)} \xB7 last sync ${base} \u2192 upstream ${upstream}` },
|
|
2790
3257
|
forkPath,
|
|
2791
3258
|
`${meta.command}-review`
|
|
2792
3259
|
);
|
|
2793
|
-
|
|
3260
|
+
const listed = shown.reduce((count3, section) => count3 + section.omitted.length, 0);
|
|
3261
|
+
return { path: page, diffs, listed, sections: shown.length };
|
|
2794
3262
|
}
|
|
2795
3263
|
async function reportReviewPage(forkPath, result, command) {
|
|
2796
3264
|
const sections = reviewSections(result, command);
|
|
2797
|
-
if (sections.length === 0 || !result.upstreamDiffRange) return;
|
|
2798
|
-
createSpinner("writing review page...");
|
|
2799
3265
|
let page;
|
|
3266
|
+
if (sections.length > 0 && result.upstreamDiffRange) {
|
|
3267
|
+
createSpinner("writing review page...");
|
|
3268
|
+
try {
|
|
3269
|
+
page = await writeReviewPage(forkPath, sections, { command, range: result.upstreamDiffRange });
|
|
3270
|
+
} catch {
|
|
3271
|
+
page = void 0;
|
|
3272
|
+
}
|
|
3273
|
+
spinnerSuccess();
|
|
3274
|
+
}
|
|
3275
|
+
if (!page) {
|
|
3276
|
+
if (command === "sync") removeSyncReviewPage(forkPath);
|
|
3277
|
+
return void 0;
|
|
3278
|
+
}
|
|
3279
|
+
const opened = command === "sync" && process9.stdout.isTTY === true && openInBrowser(page.path);
|
|
3280
|
+
return { ...page, command, opened };
|
|
3281
|
+
}
|
|
3282
|
+
function removeSyncReviewPage(forkPath) {
|
|
2800
3283
|
try {
|
|
2801
|
-
|
|
3284
|
+
rmSync(diffPagePath(forkPath, "sync-review"), { force: true });
|
|
2802
3285
|
} catch {
|
|
2803
|
-
page = void 0;
|
|
2804
3286
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3287
|
+
}
|
|
3288
|
+
function existingSyncReviewPage(forkPath) {
|
|
3289
|
+
try {
|
|
3290
|
+
const path = diffPagePath(forkPath, "sync-review");
|
|
3291
|
+
return existsSync7(path) ? path : void 0;
|
|
3292
|
+
} catch {
|
|
3293
|
+
return void 0;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
function reviewStep(page) {
|
|
3297
|
+
const path = typeof page === "string" ? page : page.path;
|
|
3298
|
+
const url = pathToFileURL(path).href;
|
|
3299
|
+
let detail = colors_default.dim("upstream changes the sync did not bring in, from the merge run");
|
|
3300
|
+
if (typeof page !== "string") {
|
|
3301
|
+
const what = page.command === "sync" ? `${plural(page.diffs, "diff")} of upstream changes that did not arrive` : `${plural(page.diffs, "diff")} in ${plural(page.sections, "section")}`;
|
|
3302
|
+
const notes = [
|
|
3303
|
+
page.listed > 0 ? `${page.listed} listed without a diff` : "",
|
|
3304
|
+
page.opened ? "opened in the browser" : ""
|
|
3305
|
+
].filter(Boolean).map((note) => ` \xB7 ${note}`).join("");
|
|
3306
|
+
detail = `${what}${notes ? colors_default.dim(notes) : ""}`;
|
|
3307
|
+
}
|
|
3308
|
+
return { label: "review", line: hyperlink(colors_default.cyan(url), url), details: [detail] };
|
|
2813
3309
|
}
|
|
2814
3310
|
|
|
2815
3311
|
// src/services/analyze.ts
|
|
@@ -2856,29 +3352,67 @@ async function runAnalyze(config) {
|
|
|
2856
3352
|
}
|
|
2857
3353
|
return result;
|
|
2858
3354
|
}
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3355
|
+
printAnalysisFileGroups(result, {
|
|
3356
|
+
links: {
|
|
3357
|
+
upstreamGitHubUrl: result.upstreamGitHubUrl,
|
|
3358
|
+
upstreamSha: result.upstreamCommit?.hash,
|
|
3359
|
+
forkGitHubUrl: await readOriginGitHubUrl(config.forkPath),
|
|
3360
|
+
fileLinkMode: config.settings.fileLinkMode,
|
|
3361
|
+
forkPath: config.forkPath
|
|
3362
|
+
},
|
|
3363
|
+
all: config.all,
|
|
3364
|
+
overrides: config.overrides
|
|
3365
|
+
});
|
|
3366
|
+
printUpstreamOverrideChanges(result);
|
|
3367
|
+
printMaskingPinWarning(result.files);
|
|
3368
|
+
printSummary(result.summary, "analysis summary");
|
|
3369
|
+
const steps = [];
|
|
3370
|
+
const review = await reportReviewPage(config.forkPath, result, "analyze");
|
|
3371
|
+
if (review) steps.push(reviewStep(review));
|
|
3372
|
+
const sync = syncStep(result);
|
|
3373
|
+
if (sync) steps.push(sync);
|
|
3374
|
+
const align = result.alignments && result.upstreamCommit ? await alignmentsStep(config, {
|
|
3375
|
+
ref: result.upstreamCommit.hash,
|
|
3376
|
+
open: result.alignments.open,
|
|
3377
|
+
arriving: result.alignments.arrived
|
|
3378
|
+
}) : void 0;
|
|
3379
|
+
if (align) steps.push(align);
|
|
3380
|
+
printNextSteps(steps);
|
|
2870
3381
|
return result;
|
|
2871
3382
|
}
|
|
3383
|
+
function syncStep(result) {
|
|
3384
|
+
if (!result.upstreamCommitCount) return void 0;
|
|
3385
|
+
const { behind, diverged, renamed } = result.summary;
|
|
3386
|
+
const changes = [
|
|
3387
|
+
behind > 0 ? `${behind} update` : "",
|
|
3388
|
+
diverged > 0 ? `${diverged} merge` : "",
|
|
3389
|
+
renamed > 0 ? `${renamed} rename` : ""
|
|
3390
|
+
].filter(Boolean);
|
|
3391
|
+
return {
|
|
3392
|
+
label: "sync",
|
|
3393
|
+
line: "pnpm cella sync",
|
|
3394
|
+
details: [
|
|
3395
|
+
colors_default.dim(
|
|
3396
|
+
`commits on a fresh branch, a rerun opens the PR \xB7 ${changes.length > 0 ? changes.join(", ") : "no synced file changes"}`
|
|
3397
|
+
)
|
|
3398
|
+
]
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
2872
3401
|
|
|
2873
3402
|
// src/services/audit.ts
|
|
2874
3403
|
import { readFileSync as readFileSync4 } from "fs";
|
|
2875
|
-
import { basename as
|
|
3404
|
+
import { basename as basename5, join as join13, relative as relative3 } from "path";
|
|
2876
3405
|
import { checkbox, Separator } from "@inquirer/prompts";
|
|
2877
3406
|
|
|
2878
3407
|
// src/utils/audit-utils.ts
|
|
2879
3408
|
import { execFile as execFile2 } from "child_process";
|
|
2880
3409
|
import { promisify as promisify2 } from "util";
|
|
2881
3410
|
var execFileAsync2 = promisify2(execFile2);
|
|
3411
|
+
function outdatedPackageName(key) {
|
|
3412
|
+
const name = key.split(" ")[0];
|
|
3413
|
+
const at = name.indexOf("@", 1);
|
|
3414
|
+
return at === -1 ? name : name.slice(0, at);
|
|
3415
|
+
}
|
|
2882
3416
|
function getRepoUrl(repositoryUrl) {
|
|
2883
3417
|
if (!repositoryUrl) return null;
|
|
2884
3418
|
let url = repositoryUrl;
|
|
@@ -2973,9 +3507,16 @@ function formatDependents(dependents) {
|
|
|
2973
3507
|
return `${dependents[0]} +${dependents.length - 1}`;
|
|
2974
3508
|
}
|
|
2975
3509
|
|
|
3510
|
+
// src/utils/local-tools.ts
|
|
3511
|
+
import { execFile as execFile3 } from "child_process";
|
|
3512
|
+
import { existsSync as existsSync8, realpathSync as realpathSync2 } from "fs";
|
|
3513
|
+
import { tmpdir } from "os";
|
|
3514
|
+
import { delimiter, dirname as dirname7, join as join11, sep as sep2 } from "path";
|
|
3515
|
+
import { promisify as promisify3 } from "util";
|
|
3516
|
+
|
|
2976
3517
|
// src/utils/runtimes.ts
|
|
2977
3518
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2978
|
-
import { basename as
|
|
3519
|
+
import { basename as basename4, dirname as dirname6, join as join10 } from "path";
|
|
2979
3520
|
var DAY = 24 * 60 * 60 * 1e3;
|
|
2980
3521
|
var FROZEN_AFTER = 180 * DAY;
|
|
2981
3522
|
var RECENT_PUSH = 30 * DAY;
|
|
@@ -2983,7 +3524,7 @@ var EXACT_VERSION = /^\d+\.\d+\.\d+$/;
|
|
|
2983
3524
|
var FROM_LINE = /^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)(?:\s+AS\s+(\S+))?\s*$/i;
|
|
2984
3525
|
var NODE_ARG_TAG = /^(?:docker\.io\/)?(?:library\/)?node:\$\{?\w+\}?([\w.-]*)$/;
|
|
2985
3526
|
var MINOR_TAG = /^(\d+(?:\.\d+){1,2})(-[\w.-]+)?$/;
|
|
2986
|
-
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(
|
|
3527
|
+
var isDockerfile = (path) => /(^|\.)Dockerfile$|^Dockerfile\./i.test(basename4(path));
|
|
2987
3528
|
function parseDockerfile(file, content) {
|
|
2988
3529
|
const images = [];
|
|
2989
3530
|
const nodeImageSuffixes = [];
|
|
@@ -3094,24 +3635,23 @@ async function fetchImageTags(pin) {
|
|
|
3094
3635
|
if (!Array.isArray(results)) return null;
|
|
3095
3636
|
return { pinned: hubTag(pinned), tags: results.flatMap((entry) => hubTag(entry) ?? []) };
|
|
3096
3637
|
}
|
|
3638
|
+
async function fetchPnpmAdvisories(version) {
|
|
3639
|
+
const answer = await fetchJson("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {
|
|
3640
|
+
method: "POST",
|
|
3641
|
+
headers: { "content-type": "application/json" },
|
|
3642
|
+
body: JSON.stringify({ pnpm: [version] })
|
|
3643
|
+
});
|
|
3644
|
+
return answer ? answer.pnpm ?? [] : null;
|
|
3645
|
+
}
|
|
3097
3646
|
async function fetchRuntimeFeeds(pins, cwd, now) {
|
|
3098
3647
|
const nodeMajor = pins.node ? parseComparableVersion(pins.node)?.major : void 0;
|
|
3099
3648
|
const [node, published, advisories, images] = await Promise.all([
|
|
3100
3649
|
nodeMajor === void 0 ? null : fetchNodeReleases(nodeMajor, now),
|
|
3101
3650
|
pins.pnpm ? fetchPublishTimes("pnpm", cwd) : null,
|
|
3102
|
-
pins.pnpm ?
|
|
3103
|
-
method: "POST",
|
|
3104
|
-
headers: { "content-type": "application/json" },
|
|
3105
|
-
body: JSON.stringify({ pnpm: [pins.pnpm] })
|
|
3106
|
-
}) : null,
|
|
3651
|
+
pins.pnpm ? fetchPnpmAdvisories(pins.pnpm) : null,
|
|
3107
3652
|
Promise.all(pins.images.map(fetchImageTags))
|
|
3108
3653
|
]);
|
|
3109
|
-
|
|
3110
|
-
return {
|
|
3111
|
-
node,
|
|
3112
|
-
pnpm: published && advisories ? { published, advisories: pnpmAdvisories ?? [] } : null,
|
|
3113
|
-
images
|
|
3114
|
-
};
|
|
3654
|
+
return { node, pnpm: published && advisories ? { published, advisories } : null, images };
|
|
3115
3655
|
}
|
|
3116
3656
|
var count = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
3117
3657
|
var monthYear = (time) => new Date(time).toLocaleString("en", { month: "short", year: "numeric", timeZone: "UTC" }).toLowerCase();
|
|
@@ -3222,7 +3762,7 @@ async function missingNodeImages(version, suffixes) {
|
|
|
3222
3762
|
return missing.flat();
|
|
3223
3763
|
}
|
|
3224
3764
|
function pinLocation(file) {
|
|
3225
|
-
const dir =
|
|
3765
|
+
const dir = dirname6(file);
|
|
3226
3766
|
return dir === "." ? file : dir;
|
|
3227
3767
|
}
|
|
3228
3768
|
|
|
@@ -3341,6 +3881,130 @@ function liveExcludes(input) {
|
|
|
3341
3881
|
});
|
|
3342
3882
|
}
|
|
3343
3883
|
|
|
3884
|
+
// src/utils/local-tools.ts
|
|
3885
|
+
var execFileAsync3 = promisify3(execFile3);
|
|
3886
|
+
function findOnPath(name, skip = []) {
|
|
3887
|
+
for (const dir of (getEnv("PATH") ?? "").split(delimiter)) {
|
|
3888
|
+
if (!dir || skip.includes(dir) || dir.endsWith(`node_modules${sep2}.bin`)) continue;
|
|
3889
|
+
if (existsSync8(join11(dir, name))) return join11(dir, name);
|
|
3890
|
+
}
|
|
3891
|
+
return null;
|
|
3892
|
+
}
|
|
3893
|
+
function installerOf(file) {
|
|
3894
|
+
let real = file;
|
|
3895
|
+
try {
|
|
3896
|
+
real = realpathSync2(file);
|
|
3897
|
+
} catch {
|
|
3898
|
+
}
|
|
3899
|
+
if (real.includes(`${sep2}.volta${sep2}`)) return "volta";
|
|
3900
|
+
return /[\\/](Cellar|homebrew|linuxbrew)[\\/]/.test(real) ? "brew" : null;
|
|
3901
|
+
}
|
|
3902
|
+
async function versionOf(file, cwd) {
|
|
3903
|
+
try {
|
|
3904
|
+
const { stdout } = await execFileAsync3(file, ["--version"], { encoding: "utf8", timeout: 1e4, cwd });
|
|
3905
|
+
return stdout.match(/\d+\.\d+\.\d+/)?.[0] ?? null;
|
|
3906
|
+
} catch {
|
|
3907
|
+
return null;
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
async function readNode(now) {
|
|
3911
|
+
const version = process.version.slice(1);
|
|
3912
|
+
const major = parseComparableVersion(version)?.major ?? 0;
|
|
3913
|
+
return { version, installer: installerOf(process.execPath), releases: await fetchNodeReleases(major, now) };
|
|
3914
|
+
}
|
|
3915
|
+
async function readPnpm(cwd) {
|
|
3916
|
+
const running = getEnv("npm_execpath");
|
|
3917
|
+
const file = findOnPath("pnpm", running ? [dirname7(running)] : []);
|
|
3918
|
+
const version = file && await versionOf(file, tmpdir());
|
|
3919
|
+
if (!file || !version) return null;
|
|
3920
|
+
const [published, advisories] = await Promise.all([fetchPublishTimes("pnpm", cwd), fetchPnpmAdvisories(version)]);
|
|
3921
|
+
return { version, installer: installerOf(file), published, advisories };
|
|
3922
|
+
}
|
|
3923
|
+
async function fetchOsvSeverities(module, version) {
|
|
3924
|
+
const answer = await fetchJson("https://api.osv.dev/v1/query", {
|
|
3925
|
+
method: "POST",
|
|
3926
|
+
body: JSON.stringify({ package: { name: module, ecosystem: "Go" }, version })
|
|
3927
|
+
});
|
|
3928
|
+
if (!answer) return null;
|
|
3929
|
+
const byGhsa = /* @__PURE__ */ new Map();
|
|
3930
|
+
for (const vuln of answer.vulns ?? []) {
|
|
3931
|
+
const id = [vuln.id, ...vuln.aliases ?? []].find((alias) => alias.startsWith("GHSA-")) ?? vuln.id;
|
|
3932
|
+
const severity = SEVERITIES.find((level) => level === vuln.database_specific?.severity?.toLowerCase());
|
|
3933
|
+
if (severity || !byGhsa.has(id)) byGhsa.set(id, severity ?? "moderate");
|
|
3934
|
+
}
|
|
3935
|
+
return [...byGhsa.values()];
|
|
3936
|
+
}
|
|
3937
|
+
async function readGh() {
|
|
3938
|
+
const file = findOnPath("gh");
|
|
3939
|
+
const version = file && await versionOf(file);
|
|
3940
|
+
if (!file || !version) return null;
|
|
3941
|
+
const [list, advisories] = await Promise.all([
|
|
3942
|
+
fetchJson("https://api.github.com/repos/cli/cli/releases?per_page=30"),
|
|
3943
|
+
fetchOsvSeverities("github.com/cli/cli/v2", version)
|
|
3944
|
+
]);
|
|
3945
|
+
const releases = Array.isArray(list) ? list.flatMap((entry) => {
|
|
3946
|
+
const { tag_name, published_at } = entry;
|
|
3947
|
+
const publishedAt = Date.parse(published_at ?? "");
|
|
3948
|
+
return tag_name && !Number.isNaN(publishedAt) ? [{ version: tag_name.replace(/^v/, ""), publishedAt }] : [];
|
|
3949
|
+
}) : null;
|
|
3950
|
+
return { version, installer: installerOf(file), releases, advisories };
|
|
3951
|
+
}
|
|
3952
|
+
async function readLocalTools(cwd, now) {
|
|
3953
|
+
const [node, pnpm, gh] = await Promise.all([readNode(now), readPnpm(cwd), readGh()]);
|
|
3954
|
+
return { node, pnpm, gh };
|
|
3955
|
+
}
|
|
3956
|
+
function upgradeCommand(name, installer, target) {
|
|
3957
|
+
if (installer === "brew") return `brew upgrade ${name}`;
|
|
3958
|
+
if (!target) return "no fixed release older than the release age yet";
|
|
3959
|
+
if (installer === "volta") return `volta install ${name}@${target}`;
|
|
3960
|
+
return name === "pnpm" ? `pnpm self-update ${target}` : `update to ${target}`;
|
|
3961
|
+
}
|
|
3962
|
+
function toCheck(name, tool, missed, target, noun) {
|
|
3963
|
+
if (!missed)
|
|
3964
|
+
return { name, version: tool.version, severity: null, note: "not checked, no answer from the feed", fix: "" };
|
|
3965
|
+
if (missed.length === 0) return { name, version: tool.version, severity: null, note: "", fix: "" };
|
|
3966
|
+
const high = missed.filter((severity) => severity === "critical" || severity === "high").length;
|
|
3967
|
+
return {
|
|
3968
|
+
name,
|
|
3969
|
+
version: tool.version,
|
|
3970
|
+
severity: SEVERITIES.find((severity) => missed.includes(severity)) ?? null,
|
|
3971
|
+
note: `${plural(missed.length, ...noun)}${high > 0 && high < missed.length ? `, ${high} high` : ""}`,
|
|
3972
|
+
fix: upgradeCommand(name, tool.installer, target)
|
|
3973
|
+
};
|
|
3974
|
+
}
|
|
3975
|
+
function planLocalTools(tools, options) {
|
|
3976
|
+
const cutoff = options.now - options.releaseAgeMinutes * 60 * 1e3;
|
|
3977
|
+
const { node, pnpm, gh } = tools;
|
|
3978
|
+
const nodeTarget = node.releases ? newestAged(node.version, node.releases, cutoff) : void 0;
|
|
3979
|
+
const securityReleases = (node.releases ?? []).filter(
|
|
3980
|
+
(release) => release.security && nodeTarget && compareVersionStrings(release.version, node.version) > 0 && compareVersionStrings(release.version, nodeTarget.version) <= 0
|
|
3981
|
+
);
|
|
3982
|
+
const checks = [
|
|
3983
|
+
toCheck(
|
|
3984
|
+
"node",
|
|
3985
|
+
node,
|
|
3986
|
+
node.releases && securityReleases.map(() => "high"),
|
|
3987
|
+
nodeTarget?.version,
|
|
3988
|
+
["security release", "security releases"]
|
|
3989
|
+
)
|
|
3990
|
+
];
|
|
3991
|
+
if (pnpm) {
|
|
3992
|
+
const affecting = (version) => (pnpm.advisories ?? []).filter((advisory) => satisfiesComparators(version, advisory.vulnerable_versions));
|
|
3993
|
+
const fixed = Object.entries(pnpm.published ?? {}).map(([version, time]) => ({ version, publishedAt: Date.parse(time) })).filter(
|
|
3994
|
+
(release) => parseComparableVersion(release.version) && !isPrerelease(release.version) && release.publishedAt <= cutoff && compareVersionStrings(release.version, pnpm.version) > 0 && affecting(release.version).length === 0
|
|
3995
|
+
).sort((a, b) => compareVersionStrings(b.version, a.version));
|
|
3996
|
+
const major = parseComparableVersion(pnpm.version)?.major;
|
|
3997
|
+
const target = fixed.find((release) => parseComparableVersion(release.version)?.major === major) ?? fixed[0];
|
|
3998
|
+
const missed = pnpm.advisories && pnpm.published && affecting(pnpm.version).map((advisory) => advisory.severity);
|
|
3999
|
+
checks.push(toCheck("pnpm", pnpm, missed, target?.version, ["advisory", "advisories"]));
|
|
4000
|
+
}
|
|
4001
|
+
if (gh) {
|
|
4002
|
+
const target = gh.releases ? newestAged(gh.version, gh.releases, cutoff) : void 0;
|
|
4003
|
+
checks.push(toCheck("gh", gh, gh.advisories, target?.version, ["advisory", "advisories"]));
|
|
4004
|
+
}
|
|
4005
|
+
return checks;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
3344
4008
|
// src/utils/workspace-yaml.ts
|
|
3345
4009
|
import { isMap, isScalar, isSeq, parseDocument } from "yaml";
|
|
3346
4010
|
var record = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -3421,37 +4085,37 @@ function writeAuditSettings(content, next) {
|
|
|
3421
4085
|
|
|
3422
4086
|
// src/services/audit-security.ts
|
|
3423
4087
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
3424
|
-
import { cpSync, existsSync as
|
|
3425
|
-
import { tmpdir } from "os";
|
|
3426
|
-
import { dirname as
|
|
4088
|
+
import { cpSync, existsSync as existsSync9, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
4089
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
4090
|
+
import { dirname as dirname8, join as join12, relative as relative2 } from "path";
|
|
3427
4091
|
import { confirm, select as select2 } from "@inquirer/prompts";
|
|
3428
4092
|
var WORKSPACE_FILE = "pnpm-workspace.yaml";
|
|
3429
4093
|
var LOCK_FILE = "pnpm-lock.yaml";
|
|
3430
4094
|
var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
|
|
3431
|
-
var readOr = (path, fallback = "") =>
|
|
4095
|
+
var readOr = (path, fallback = "") => existsSync9(path) ? readFileSync3(path, "utf8") : fallback;
|
|
3432
4096
|
var count2 = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
|
|
3433
4097
|
var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
|
|
3434
4098
|
var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
|
|
3435
4099
|
var sameEntries = (a, b) => Object.keys(a).length === Object.keys(b).length && Object.entries(a).every(([key, value]) => b[key] === value);
|
|
3436
4100
|
async function createStaging(forkPath, workspaceYaml) {
|
|
3437
|
-
const staging = mkdtempSync(
|
|
4101
|
+
const staging = mkdtempSync(join12(tmpdir2(), "cella-audit-"));
|
|
3438
4102
|
const projects = await runPnpmJson(["ls", "-r", "--depth", "-1", "--json"], forkPath) ?? [];
|
|
3439
4103
|
const files = [
|
|
3440
4104
|
...RESOLUTION_FILES,
|
|
3441
4105
|
...readPatchFiles(workspaceYaml),
|
|
3442
|
-
...projects.map((project) =>
|
|
4106
|
+
...projects.map((project) => join12(relative2(forkPath, project.path), "package.json"))
|
|
3443
4107
|
];
|
|
3444
4108
|
for (const file of new Set(files)) {
|
|
3445
|
-
if (file.startsWith("..") || !
|
|
3446
|
-
mkdirSync2(
|
|
3447
|
-
cpSync(
|
|
4109
|
+
if (file.startsWith("..") || !existsSync9(join12(forkPath, file))) continue;
|
|
4110
|
+
mkdirSync2(dirname8(join12(staging, file)), { recursive: true });
|
|
4111
|
+
cpSync(join12(forkPath, file), join12(staging, file));
|
|
3448
4112
|
}
|
|
3449
4113
|
return staging;
|
|
3450
4114
|
}
|
|
3451
4115
|
async function resolveStaging(staging, workspaceYaml, settings) {
|
|
3452
|
-
writeFileSync2(
|
|
4116
|
+
writeFileSync2(join12(staging, WORKSPACE_FILE), writeAuditSettings(workspaceYaml, settings));
|
|
3453
4117
|
const result = await runPnpm(["install", "--lockfile-only", "--no-frozen-lockfile", "--ignore-scripts"], staging);
|
|
3454
|
-
return result.ok ? { lock: readOr(
|
|
4118
|
+
return result.ok ? { lock: readOr(join12(staging, LOCK_FILE)) } : { error: `${result.stdout}${result.stderr}`.trim() };
|
|
3455
4119
|
}
|
|
3456
4120
|
function lockedVersions(lock, name) {
|
|
3457
4121
|
const escaped = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
@@ -3555,8 +4219,8 @@ function changeLine(label, before, after) {
|
|
|
3555
4219
|
var NO_EDITS = { manifests: {}, files: {} };
|
|
3556
4220
|
async function runSecurityPass(forkPath, options) {
|
|
3557
4221
|
const { releaseAgeMinutes, edits = NO_EDITS } = options;
|
|
3558
|
-
const workspaceYaml = readOr(
|
|
3559
|
-
const workspaceLock = readOr(
|
|
4222
|
+
const workspaceYaml = readOr(join12(forkPath, WORKSPACE_FILE));
|
|
4223
|
+
const workspaceLock = readOr(join12(forkPath, LOCK_FILE));
|
|
3560
4224
|
let previous;
|
|
3561
4225
|
try {
|
|
3562
4226
|
previous = readAuditSettings(workspaceYaml);
|
|
@@ -3582,7 +4246,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3582
4246
|
};
|
|
3583
4247
|
const staging = await createStaging(forkPath, workspaceYaml);
|
|
3584
4248
|
try {
|
|
3585
|
-
for (const [file, content] of Object.entries(edits.manifests)) writeFileSync2(
|
|
4249
|
+
for (const [file, content] of Object.entries(edits.manifests)) writeFileSync2(join12(staging, file), content);
|
|
3586
4250
|
const bare = await resolveStaging(staging, workspaceYaml, { ...previous, overrides: lasting, ignoredGhsas: [] });
|
|
3587
4251
|
if ("error" in bare) {
|
|
3588
4252
|
const without = updatesManifests ? "with the selected updates" : "without its audit overrides";
|
|
@@ -3634,7 +4298,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3634
4298
|
const settingsChanged = nextYaml !== workspaceYaml;
|
|
3635
4299
|
const otherFiles = Object.keys(edits.files);
|
|
3636
4300
|
const writeFiles = () => {
|
|
3637
|
-
for (const [file, content] of Object.entries(edits.files)) writeFileSync2(
|
|
4301
|
+
for (const [file, content] of Object.entries(edits.files)) writeFileSync2(join12(forkPath, file), content);
|
|
3638
4302
|
if (otherFiles.length > 0) console.info(`${checkMark} ${otherFiles.join(", ")} updated`);
|
|
3639
4303
|
};
|
|
3640
4304
|
if (!settingsChanged && !updatesManifests) {
|
|
@@ -3654,7 +4318,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3654
4318
|
};
|
|
3655
4319
|
if (!updatesManifests && sameEntries(next.overrides, previous.overrides)) {
|
|
3656
4320
|
writeFiles();
|
|
3657
|
-
writeFileSync2(
|
|
4321
|
+
writeFileSync2(join12(forkPath, WORKSPACE_FILE), nextYaml);
|
|
3658
4322
|
console.info(`${checkMark} ${WORKSPACE_FILE} updated ${colors_default.dim("\xB7 nothing to install")}`);
|
|
3659
4323
|
printOpen(open);
|
|
3660
4324
|
summarize();
|
|
@@ -3665,14 +4329,14 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3665
4329
|
stagingSpinner.stop();
|
|
3666
4330
|
if ("error" in staged) return fail("could not resolve the workspace with the new settings", staged.error);
|
|
3667
4331
|
const written = { ...edits.files, ...edits.manifests, [WORKSPACE_FILE]: nextYaml, [LOCK_FILE]: staged.lock };
|
|
3668
|
-
const before = Object.fromEntries(Object.keys(written).map((file) => [file, readOr(
|
|
4332
|
+
const before = Object.fromEntries(Object.keys(written).map((file) => [file, readOr(join12(forkPath, file))]));
|
|
3669
4333
|
before[WORKSPACE_FILE] = workspaceYaml;
|
|
3670
4334
|
before[LOCK_FILE] = workspaceLock;
|
|
3671
|
-
for (const [file, content] of Object.entries(written)) writeFileSync2(
|
|
4335
|
+
for (const [file, content] of Object.entries(written)) writeFileSync2(join12(forkPath, file), content);
|
|
3672
4336
|
const install = spawnSync2("pnpm", ["install"], { cwd: forkPath, stdio: "inherit" });
|
|
3673
4337
|
console.info();
|
|
3674
4338
|
if (install.status !== 0) {
|
|
3675
|
-
for (const [file, content] of Object.entries(before)) writeFileSync2(
|
|
4339
|
+
for (const [file, content] of Object.entries(before)) writeFileSync2(join12(forkPath, file), content);
|
|
3676
4340
|
console.error(`${colors_default.red("\u2717")} pnpm install failed (exit code ${install.status})`);
|
|
3677
4341
|
console.info(colors_default.dim(` ${Object.keys(before).join(", ")} are back as they were; run pnpm install to match`));
|
|
3678
4342
|
console.info();
|
|
@@ -3686,7 +4350,7 @@ async function runSecurityPass(forkPath, options) {
|
|
|
3686
4350
|
return true;
|
|
3687
4351
|
} finally {
|
|
3688
4352
|
spinner.stop();
|
|
3689
|
-
|
|
4353
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
3690
4354
|
}
|
|
3691
4355
|
}
|
|
3692
4356
|
function printOpen(modules) {
|
|
@@ -3741,7 +4405,7 @@ function createDependentPkgReader() {
|
|
|
3741
4405
|
let cached = cache.get(location);
|
|
3742
4406
|
if (cached === void 0) {
|
|
3743
4407
|
try {
|
|
3744
|
-
cached = JSON.parse(readFileSync4(
|
|
4408
|
+
cached = JSON.parse(readFileSync4(join13(location, "package.json"), "utf8"));
|
|
3745
4409
|
} catch {
|
|
3746
4410
|
cached = null;
|
|
3747
4411
|
}
|
|
@@ -3756,11 +4420,12 @@ async function runAudit(config) {
|
|
|
3756
4420
|
try {
|
|
3757
4421
|
const pins = await readRuntimePins(forkPath);
|
|
3758
4422
|
const now = Date.now();
|
|
3759
|
-
const [outdated, auditResult, releaseAgeMinutes, feeds] = await Promise.all([
|
|
4423
|
+
const [outdated, auditResult, releaseAgeMinutes, feeds, machine] = await Promise.all([
|
|
3760
4424
|
runPnpmJson(["-r", "outdated", "--json", "--long"], forkPath),
|
|
3761
4425
|
runPnpmAudit(forkPath),
|
|
3762
4426
|
readReleaseAgeMinutes(forkPath),
|
|
3763
|
-
fetchRuntimeFeeds(pins, forkPath, now)
|
|
4427
|
+
fetchRuntimeFeeds(pins, forkPath, now),
|
|
4428
|
+
readLocalTools(forkPath, now)
|
|
3764
4429
|
]);
|
|
3765
4430
|
if (!outdated) {
|
|
3766
4431
|
spinnerFail("pnpm outdated gave no answer: run pnpm -r outdated to see why");
|
|
@@ -3768,6 +4433,7 @@ async function runAudit(config) {
|
|
|
3768
4433
|
return;
|
|
3769
4434
|
}
|
|
3770
4435
|
const runtimes = planRuntimeUpdates(pins, feeds, { releaseAgeMinutes, now });
|
|
4436
|
+
const localTools = planLocalTools(machine, { releaseAgeMinutes, now });
|
|
3771
4437
|
const rows = [
|
|
3772
4438
|
...packageRows(outdated, auditResult ? advisoriesFromAudit(auditResult) : []),
|
|
3773
4439
|
...runtimes.updates.map(runtimeRow)
|
|
@@ -3776,6 +4442,7 @@ async function runAudit(config) {
|
|
|
3776
4442
|
spinnerSuccess("all packages and runtimes are up to date");
|
|
3777
4443
|
printNotes(forkPath, runtimes.unchecked);
|
|
3778
4444
|
console.info();
|
|
4445
|
+
printLocalTools(localTools);
|
|
3779
4446
|
await runSecurityPass(forkPath, { apply: !config.list, releaseAgeMinutes });
|
|
3780
4447
|
return;
|
|
3781
4448
|
}
|
|
@@ -3783,6 +4450,7 @@ async function runAudit(config) {
|
|
|
3783
4450
|
printOutdatedResults(rows, forkPath);
|
|
3784
4451
|
printNotes(forkPath, runtimes.unchecked);
|
|
3785
4452
|
console.info();
|
|
4453
|
+
printLocalTools(localTools);
|
|
3786
4454
|
if (config.list) {
|
|
3787
4455
|
await runSecurityPass(forkPath, { apply: false, releaseAgeMinutes });
|
|
3788
4456
|
return;
|
|
@@ -3798,7 +4466,8 @@ async function runAudit(config) {
|
|
|
3798
4466
|
function packageRows(outdated, advisories) {
|
|
3799
4467
|
const severityOf = (name) => SEVERITIES.find((severity) => advisories.some((a) => a.module === name && a.severity === severity)) ?? null;
|
|
3800
4468
|
const readDependentPkg = createDependentPkgReader();
|
|
3801
|
-
const rows = Object.entries(outdated).map(([
|
|
4469
|
+
const rows = Object.entries(outdated).map(([key, pkg]) => {
|
|
4470
|
+
const name = outdatedPackageName(key);
|
|
3802
4471
|
const repoUrl = getRepoUrl(pkg.latestManifest?.repository?.url);
|
|
3803
4472
|
const current = pkg.current ?? "missing";
|
|
3804
4473
|
const pinnedIn = [];
|
|
@@ -3809,6 +4478,7 @@ function packageRows(outdated, advisories) {
|
|
|
3809
4478
|
}
|
|
3810
4479
|
}
|
|
3811
4480
|
return {
|
|
4481
|
+
key,
|
|
3812
4482
|
name,
|
|
3813
4483
|
current,
|
|
3814
4484
|
latest: pkg.latest,
|
|
@@ -3825,6 +4495,7 @@ function packageRows(outdated, advisories) {
|
|
|
3825
4495
|
}
|
|
3826
4496
|
function runtimeRow(update) {
|
|
3827
4497
|
return {
|
|
4498
|
+
key: `runtime:${update.file}:${update.name}`,
|
|
3828
4499
|
name: update.name,
|
|
3829
4500
|
current: update.current,
|
|
3830
4501
|
latest: update.target,
|
|
@@ -3838,7 +4509,6 @@ function runtimeRow(update) {
|
|
|
3838
4509
|
runtime: update
|
|
3839
4510
|
};
|
|
3840
4511
|
}
|
|
3841
|
-
var rowKey = (row) => row.runtime ? `runtime:${row.runtime.file}:${row.name}` : row.name;
|
|
3842
4512
|
var isRed = (row) => row.severity === "critical" || row.severity === "high";
|
|
3843
4513
|
var inText = (row) => row.runtime ? pinLocation(row.runtime.file) : formatDependents(row.dependentPackages.map((d) => d.name));
|
|
3844
4514
|
function printOutdatedResults(rows, forkPath) {
|
|
@@ -3882,7 +4552,7 @@ function printOutdatedResults(rows, forkPath) {
|
|
|
3882
4552
|
const dependentMap = /* @__PURE__ */ new Map();
|
|
3883
4553
|
for (const pkg of rows) {
|
|
3884
4554
|
for (const { name, location } of pkg.dependentPackages) {
|
|
3885
|
-
const dependentName = name ||
|
|
4555
|
+
const dependentName = name || basename5(location);
|
|
3886
4556
|
const existing = dependentMap.get(dependentName);
|
|
3887
4557
|
if (existing) {
|
|
3888
4558
|
existing.count++;
|
|
@@ -3894,7 +4564,7 @@ function printOutdatedResults(rows, forkPath) {
|
|
|
3894
4564
|
const sortedDependents = [...dependentMap.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
3895
4565
|
const maxDependentNameLen = Math.max(0, ...sortedDependents.map(([name]) => name.length));
|
|
3896
4566
|
for (const [dependentName, { location, count: count3 }] of sortedDependents) {
|
|
3897
|
-
const packageJsonPath2 =
|
|
4567
|
+
const packageJsonPath2 = join13(location, "package.json");
|
|
3898
4568
|
const relativePath = relative3(forkPath, packageJsonPath2);
|
|
3899
4569
|
const paddedName = dependentName.padEnd(maxDependentNameLen);
|
|
3900
4570
|
console.info(
|
|
@@ -3905,7 +4575,7 @@ function printOutdatedResults(rows, forkPath) {
|
|
|
3905
4575
|
function printNotes(forkPath, unchecked) {
|
|
3906
4576
|
let held = [];
|
|
3907
4577
|
try {
|
|
3908
|
-
const updateConfig = readTopLevel(readFileSync4(
|
|
4578
|
+
const updateConfig = readTopLevel(readFileSync4(join13(forkPath, "pnpm-workspace.yaml"), "utf8"), "updateConfig");
|
|
3909
4579
|
const ignored = updateConfig?.ignoreDependencies;
|
|
3910
4580
|
held = Array.isArray(ignored) ? ignored.map(String) : [];
|
|
3911
4581
|
} catch {
|
|
@@ -3914,6 +4584,16 @@ function printNotes(forkPath, unchecked) {
|
|
|
3914
4584
|
if (unchecked.length > 0)
|
|
3915
4585
|
console.info(` ${colors_default.dim(`not checked, no answer from the feed: ${unchecked.join(", ")}`)}`);
|
|
3916
4586
|
}
|
|
4587
|
+
function printLocalTools(checks) {
|
|
4588
|
+
const width = Math.max(...checks.map((tool) => `${tool.name} ${tool.version}`.length));
|
|
4589
|
+
console.info(colors_default.bold("this machine"));
|
|
4590
|
+
for (const tool of checks) {
|
|
4591
|
+
const label = `${tool.name} ${tool.version}`.padEnd(width);
|
|
4592
|
+
const detail = tool.severity ? `${tool.note} ${colors_default.dim("\xB7")} ${tool.fix}` : colors_default.dim(tool.note || "no missing security fixes");
|
|
4593
|
+
console.info(`${tool.severity ? getVulnIcon(tool.severity) : " "} ${label} ${detail}`);
|
|
4594
|
+
}
|
|
4595
|
+
console.info();
|
|
4596
|
+
}
|
|
3917
4597
|
function buildUpdateChoices(rows) {
|
|
3918
4598
|
const makeChoice = (row) => {
|
|
3919
4599
|
const devTag = row.isDev ? colors_default.dim(" (dev)") : "";
|
|
@@ -3925,7 +4605,7 @@ function buildUpdateChoices(rows) {
|
|
|
3925
4605
|
].filter(Boolean);
|
|
3926
4606
|
return {
|
|
3927
4607
|
name: `${row.name}${devTag} ${version}${notes.length > 0 ? ` ${notes.join(" ")}` : ""}`,
|
|
3928
|
-
value:
|
|
4608
|
+
value: row.key,
|
|
3929
4609
|
checked: isRed(row)
|
|
3930
4610
|
};
|
|
3931
4611
|
};
|
|
@@ -3985,17 +4665,17 @@ async function planEdits(forkPath, rows, selected, nodeImageSuffixes) {
|
|
|
3985
4665
|
const summary = /* @__PURE__ */ new Map();
|
|
3986
4666
|
const bumped = [];
|
|
3987
4667
|
const edit = (file, label, item, resolves, change) => {
|
|
3988
|
-
const entry = contents.get(file) ?? { content: readFileSync4(
|
|
4668
|
+
const entry = contents.get(file) ?? { content: readFileSync4(join13(forkPath, file), "utf8"), resolves: false };
|
|
3989
4669
|
const next = change(entry.content);
|
|
3990
4670
|
if (next === entry.content) return;
|
|
3991
4671
|
contents.set(file, { content: next, resolves: entry.resolves || resolves });
|
|
3992
4672
|
summary.set(label, [...summary.get(label) ?? [], item]);
|
|
3993
4673
|
};
|
|
3994
|
-
const chosen = rows.filter((row) => selected.includes(
|
|
4674
|
+
const chosen = rows.filter((row) => selected.includes(row.key));
|
|
3995
4675
|
for (const row of chosen) {
|
|
3996
4676
|
if (row.runtime) continue;
|
|
3997
4677
|
for (const { name: dependent, location } of row.dependentPackages) {
|
|
3998
|
-
const file = relative3(forkPath,
|
|
4678
|
+
const file = relative3(forkPath, join13(location, "package.json"));
|
|
3999
4679
|
edit(
|
|
4000
4680
|
file,
|
|
4001
4681
|
dependent || file,
|
|
@@ -4068,8 +4748,8 @@ import {
|
|
|
4068
4748
|
import { select as select3 } from "@inquirer/prompts";
|
|
4069
4749
|
|
|
4070
4750
|
// src/services/contrib-core.ts
|
|
4071
|
-
import { rmSync as
|
|
4072
|
-
import { dirname as
|
|
4751
|
+
import { rmSync as rmSync3 } from "fs";
|
|
4752
|
+
import { dirname as dirname9, join as join14, resolve as resolve4 } from "path";
|
|
4073
4753
|
async function listFilesAtRef(repoPath, ref) {
|
|
4074
4754
|
const raw = await git(["ls-tree", "-r", "--name-only", ref], repoPath);
|
|
4075
4755
|
return new Set(raw.split("\n").filter(Boolean));
|
|
@@ -4085,10 +4765,10 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4085
4765
|
const baseFiles = await listFilesAtRef(repoPath, baseRef);
|
|
4086
4766
|
const baseDirs = /* @__PURE__ */ new Set(["."]);
|
|
4087
4767
|
for (const f of baseFiles) {
|
|
4088
|
-
let dir =
|
|
4768
|
+
let dir = dirname9(f);
|
|
4089
4769
|
while (dir !== ".") {
|
|
4090
4770
|
baseDirs.add(dir);
|
|
4091
|
-
dir =
|
|
4771
|
+
dir = dirname9(dir);
|
|
4092
4772
|
}
|
|
4093
4773
|
}
|
|
4094
4774
|
const files = [];
|
|
@@ -4101,7 +4781,7 @@ async function detectContributableFiles(repoPath, baseRef, forkRef, config, fork
|
|
|
4101
4781
|
if (inCella && !inFork) {
|
|
4102
4782
|
kind = "deleted";
|
|
4103
4783
|
} else if (!inCella && inFork) {
|
|
4104
|
-
if (!baseDirs.has(
|
|
4784
|
+
if (!baseDirs.has(dirname9(file.path))) continue;
|
|
4105
4785
|
kind = "created";
|
|
4106
4786
|
} else {
|
|
4107
4787
|
kind = "modified";
|
|
@@ -4125,7 +4805,7 @@ async function readBlobInfo(repoPath, ref, filePath) {
|
|
|
4125
4805
|
async function buildContribBranch(repoPath, baseRef, forkRef, files, forkName) {
|
|
4126
4806
|
const branch = `contrib/${forkName}`;
|
|
4127
4807
|
const gitDir = resolve4(repoPath, await git(["rev-parse", "--git-dir"], repoPath));
|
|
4128
|
-
const tmpIndex =
|
|
4808
|
+
const tmpIndex = join14(gitDir, `tmp-contrib-index-${forkName.replace(/[^\w.-]/g, "_")}`);
|
|
4129
4809
|
const indexEnv = { GIT_INDEX_FILE: tmpIndex };
|
|
4130
4810
|
try {
|
|
4131
4811
|
await git(["read-tree", baseRef], repoPath, { env: indexEnv });
|
|
@@ -4155,25 +4835,25 @@ ${commitBody}`;
|
|
|
4155
4835
|
await git(["update-ref", `refs/heads/${branch}`, commitHash], repoPath);
|
|
4156
4836
|
return { branch, appliedFiles };
|
|
4157
4837
|
} finally {
|
|
4158
|
-
|
|
4838
|
+
rmSync3(tmpIndex, { force: true });
|
|
4159
4839
|
}
|
|
4160
4840
|
}
|
|
4161
4841
|
|
|
4162
4842
|
// src/services/fork-utils.ts
|
|
4163
|
-
import { existsSync as
|
|
4164
|
-
import { dirname as
|
|
4843
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4844
|
+
import { dirname as dirname10, resolve as resolve5 } from "path";
|
|
4165
4845
|
async function resolveForkBasePath(repoPath) {
|
|
4166
4846
|
const commonDir = await git(["rev-parse", "--git-common-dir"], repoPath, { ignoreErrors: true });
|
|
4167
|
-
return commonDir ?
|
|
4847
|
+
return commonDir ? dirname10(resolve5(repoPath, commonDir)) : repoPath;
|
|
4168
4848
|
}
|
|
4169
4849
|
function validateForkPath(fork, basePath, requireConfig = false) {
|
|
4170
4850
|
const resolvedPath = resolve5(basePath, fork.localPath);
|
|
4171
4851
|
if (!resolvedPath.startsWith(resolve5(basePath, ".."))) {
|
|
4172
4852
|
return { fork, resolvedPath, valid: false, error: "path resolves outside parent directory" };
|
|
4173
4853
|
}
|
|
4174
|
-
if (!
|
|
4175
|
-
if (!
|
|
4176
|
-
if (requireConfig && !
|
|
4854
|
+
if (!existsSync10(resolvedPath)) return { fork, resolvedPath, valid: false, error: "path does not exist" };
|
|
4855
|
+
if (!existsSync10(`${resolvedPath}/.git`)) return { fork, resolvedPath, valid: false, error: "not a git repository" };
|
|
4856
|
+
if (requireConfig && !existsSync10(`${resolvedPath}/${CONFIG_FILE}`)) {
|
|
4177
4857
|
return { fork, resolvedPath, valid: false, error: `missing ${CONFIG_FILE}` };
|
|
4178
4858
|
}
|
|
4179
4859
|
return { fork, resolvedPath, valid: true };
|
|
@@ -4479,7 +5159,7 @@ async function runContributions(config) {
|
|
|
4479
5159
|
|
|
4480
5160
|
// src/services/forks.ts
|
|
4481
5161
|
import { resolve as resolve6 } from "path";
|
|
4482
|
-
import
|
|
5162
|
+
import process10 from "process";
|
|
4483
5163
|
import { Separator as Separator2, select as select5 } from "@inquirer/prompts";
|
|
4484
5164
|
|
|
4485
5165
|
// src/services/sync.ts
|
|
@@ -4516,6 +5196,28 @@ function listOpenSyncPrs(cwd, prefix) {
|
|
|
4516
5196
|
}
|
|
4517
5197
|
return parsed.filter((pr) => pr.headRefName.startsWith(`${prefix}/`)).sort((a, b) => b.number - a.number);
|
|
4518
5198
|
}
|
|
5199
|
+
function createPr(cwd, repoSlug, pr) {
|
|
5200
|
+
const repo = repoSlug ?? "{owner}/{repo}";
|
|
5201
|
+
const fields = ["title", "head", "base", "body"];
|
|
5202
|
+
const created = runGh(cwd, [
|
|
5203
|
+
"api",
|
|
5204
|
+
`repos/${repo}/pulls`,
|
|
5205
|
+
"-X",
|
|
5206
|
+
"POST",
|
|
5207
|
+
...fields.flatMap((field) => ["-f", `${field}=${pr[field]}`]),
|
|
5208
|
+
"--jq",
|
|
5209
|
+
".html_url"
|
|
5210
|
+
]);
|
|
5211
|
+
if (created.ok) return { ...created, url: created.output || void 0 };
|
|
5212
|
+
const owner = repoSlug ? repoSlug.split("/")[0] : "{owner}";
|
|
5213
|
+
const existing = runGh(cwd, [
|
|
5214
|
+
"api",
|
|
5215
|
+
`repos/${repo}/pulls?state=open&head=${owner}:${pr.head}`,
|
|
5216
|
+
"--jq",
|
|
5217
|
+
".[0].html_url // empty"
|
|
5218
|
+
]);
|
|
5219
|
+
return { ...created, url: existing.ok && existing.output ? existing.output : void 0 };
|
|
5220
|
+
}
|
|
4519
5221
|
function mergePrSquash(cwd, ref, options = {}) {
|
|
4520
5222
|
return runGh(cwd, ["pr", "merge", String(ref), "--squash", ...options.deleteBranch ? ["--delete-branch"] : []]);
|
|
4521
5223
|
}
|
|
@@ -4524,12 +5226,12 @@ function closePr(cwd, ref) {
|
|
|
4524
5226
|
}
|
|
4525
5227
|
|
|
4526
5228
|
// src/services/packages.ts
|
|
4527
|
-
import { existsSync as
|
|
4528
|
-
import { join as
|
|
5229
|
+
import { existsSync as existsSync12, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
5230
|
+
import { join as join16 } from "path";
|
|
4529
5231
|
|
|
4530
5232
|
// src/services/package-usage.ts
|
|
4531
|
-
import { existsSync as
|
|
4532
|
-
import { join as
|
|
5233
|
+
import { existsSync as existsSync11, readFileSync as readFileSync5, statSync } from "fs";
|
|
5234
|
+
import { join as join15 } from "path";
|
|
4533
5235
|
var MAX_FILE_BYTES = 1024 * 1024;
|
|
4534
5236
|
function escapeRegExp(value) {
|
|
4535
5237
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -4545,7 +5247,7 @@ function readSearchable(filePath) {
|
|
|
4545
5247
|
}
|
|
4546
5248
|
function fileSource(forkPath, path) {
|
|
4547
5249
|
let text;
|
|
4548
|
-
return { path, label: path, text: () => text ??= readSearchable(
|
|
5250
|
+
return { path, label: path, text: () => text ??= readSearchable(join15(forkPath, path)) };
|
|
4549
5251
|
}
|
|
4550
5252
|
async function readScriptsAtRef(forkPath, ref, path) {
|
|
4551
5253
|
try {
|
|
@@ -4557,7 +5259,7 @@ async function readScriptsAtRef(forkPath, ref, path) {
|
|
|
4557
5259
|
async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
4558
5260
|
let forkScripts;
|
|
4559
5261
|
try {
|
|
4560
|
-
forkScripts = JSON.parse(readFileSync5(
|
|
5262
|
+
forkScripts = JSON.parse(readFileSync5(join15(forkPath, path), "utf8")).scripts ?? {};
|
|
4561
5263
|
} catch {
|
|
4562
5264
|
return [];
|
|
4563
5265
|
}
|
|
@@ -4567,9 +5269,9 @@ async function scriptSources(forkPath, upstreamRef, baseRef, path) {
|
|
|
4567
5269
|
}
|
|
4568
5270
|
function binNames(forkPath, location, name) {
|
|
4569
5271
|
const bareName = name.split("/").pop();
|
|
4570
|
-
for (const dir of [
|
|
4571
|
-
const manifest =
|
|
4572
|
-
if (!
|
|
5272
|
+
for (const dir of [join15(forkPath, location), forkPath]) {
|
|
5273
|
+
const manifest = join15(dir, "node_modules", name, "package.json");
|
|
5274
|
+
if (!existsSync11(manifest)) continue;
|
|
4573
5275
|
try {
|
|
4574
5276
|
const { bin } = JSON.parse(readFileSync5(manifest, "utf8"));
|
|
4575
5277
|
if (typeof bin === "string") return [bareName];
|
|
@@ -4750,7 +5452,7 @@ function setTopLevelKey(pkg, key, value) {
|
|
|
4750
5452
|
Object.assign(pkg, Object.fromEntries(entries));
|
|
4751
5453
|
}
|
|
4752
5454
|
function readPackageJson(filePath) {
|
|
4753
|
-
if (!
|
|
5455
|
+
if (!existsSync12(filePath)) return null;
|
|
4754
5456
|
try {
|
|
4755
5457
|
return JSON.parse(readFileSync6(filePath, "utf8"));
|
|
4756
5458
|
} catch (error) {
|
|
@@ -4784,13 +5486,13 @@ async function syncPackageJson(config, baseRef, usage, relativePath, keysToSync)
|
|
|
4784
5486
|
const changes = [];
|
|
4785
5487
|
const kept = [];
|
|
4786
5488
|
const pkgRelPath = packageJsonPath(relativePath);
|
|
4787
|
-
const pkgPath =
|
|
5489
|
+
const pkgPath = join16(forkPath, pkgRelPath);
|
|
4788
5490
|
const forkPkg = readPackageJson(pkgPath);
|
|
4789
5491
|
const upstream = await readPackageJsonAtRef(forkPath, upstreamRef, relativePath);
|
|
4790
5492
|
const basePkg = baseRef ? (await readPackageJsonAtRef(forkPath, baseRef, relativePath))?.data : void 0;
|
|
4791
5493
|
if (!upstream) return { updated: false, changes, kept };
|
|
4792
5494
|
if (!forkPkg) {
|
|
4793
|
-
const workspaceArrived = relativePath !== "" &&
|
|
5495
|
+
const workspaceArrived = relativePath !== "" && existsSync12(join16(forkPath, relativePath));
|
|
4794
5496
|
if (basePkg || !workspaceArrived || isIgnored(pkgRelPath, config)) return { updated: false, changes, kept };
|
|
4795
5497
|
writeFileSync3(pkgPath, upstream.raw.endsWith("\n") ? upstream.raw : `${upstream.raw}
|
|
4796
5498
|
`, "utf8");
|
|
@@ -4996,9 +5698,43 @@ async function runSync(config, options) {
|
|
|
4996
5698
|
const staged = result.files.some((file) => SYNC_APPLIED_STATUSES.includes(file.status));
|
|
4997
5699
|
const stagedBranch = options?.stagedBranch && result.conflicts.length === 0 && staged ? options.stagedBranch : void 0;
|
|
4998
5700
|
printSyncComplete(result, { stagedBranch });
|
|
4999
|
-
await reportReviewPage(config.forkPath, result, "sync");
|
|
5000
|
-
|
|
5001
|
-
|
|
5701
|
+
const reviewPage = await reportReviewPage(config.forkPath, result, "sync");
|
|
5702
|
+
return { ...result, reviewPage };
|
|
5703
|
+
}
|
|
5704
|
+
var CONFLICTS_LISTED = 10;
|
|
5705
|
+
async function syncAlignmentsStep(config, run) {
|
|
5706
|
+
if (run?.alignments && run.upstreamCommit) {
|
|
5707
|
+
const { open, arrived } = run.alignments;
|
|
5708
|
+
return alignmentsStep(config, { ref: run.upstreamCommit.hash, open, arrived });
|
|
5709
|
+
}
|
|
5710
|
+
const view = await readOpenAlignments(config);
|
|
5711
|
+
return view ? alignmentsStep(config, view) : void 0;
|
|
5712
|
+
}
|
|
5713
|
+
function syncReviewStep(config, run) {
|
|
5714
|
+
const page = run ? run.reviewPage : existingSyncReviewPage(config.forkPath);
|
|
5715
|
+
return page ? reviewStep(page) : void 0;
|
|
5716
|
+
}
|
|
5717
|
+
async function conflictSteps(config, conflicts, run) {
|
|
5718
|
+
const listed = conflicts.slice(0, CONFLICTS_LISTED).map((file) => colors_default.dim(file));
|
|
5719
|
+
if (conflicts.length > CONFLICTS_LISTED) listed.push(colors_default.dim(`+ ${conflicts.length - CONFLICTS_LISTED} more`));
|
|
5720
|
+
const steps = [
|
|
5721
|
+
{
|
|
5722
|
+
label: "resolve",
|
|
5723
|
+
line: `${plural(conflicts.length, "conflict")} in your editor, then stage them`,
|
|
5724
|
+
details: listed
|
|
5725
|
+
},
|
|
5726
|
+
{
|
|
5727
|
+
label: "finish",
|
|
5728
|
+
line: "pnpm cella sync",
|
|
5729
|
+
details: [
|
|
5730
|
+
colors_default.dim("commits the sync and stops for drift triage"),
|
|
5731
|
+
colors_default.dim("let the rerun commit: a manual `git commit` records a merge commit that bloats the PR")
|
|
5732
|
+
]
|
|
5733
|
+
},
|
|
5734
|
+
syncReviewStep(config, run),
|
|
5735
|
+
await syncAlignmentsStep(config, run)
|
|
5736
|
+
];
|
|
5737
|
+
return steps.filter((step) => !!step);
|
|
5002
5738
|
}
|
|
5003
5739
|
var SYNC_PR_TITLE = "chore: sync upstream cella";
|
|
5004
5740
|
async function buildSyncCommitMessage(forkPath, upstreamRef) {
|
|
@@ -5013,7 +5749,7 @@ function qualifyPrRefs(subject, repoSlug) {
|
|
|
5013
5749
|
return subject.replace(/(^|[^\w/])#(\d+)\b/g, `$1${repoSlug}#$2`);
|
|
5014
5750
|
}
|
|
5015
5751
|
function buildSyncPrBody(input) {
|
|
5016
|
-
const { repoSlug, version, fromSha, toSha, commits, totalCount,
|
|
5752
|
+
const { repoSlug, version, fromSha, toSha, commits, totalCount, alignments } = input;
|
|
5017
5753
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5018
5754
|
const short = (sha) => sha.slice(0, 7);
|
|
5019
5755
|
const commitRef = (sha) => githubUrl ? `[\`${short(sha)}\`](${githubUrl}/commit/${sha})` : `\`${short(sha)}\``;
|
|
@@ -5028,17 +5764,19 @@ function buildSyncPrBody(input) {
|
|
|
5028
5764
|
lines.push(`- ${commitRef(commit.hash)} ${qualifyPrRefs(commit.message, repoSlug)}`);
|
|
5029
5765
|
}
|
|
5030
5766
|
}
|
|
5031
|
-
if (
|
|
5032
|
-
const { total, open } =
|
|
5033
|
-
if (open.length === 0) lines.push("", `**
|
|
5767
|
+
if (alignments && alignments.total > 0) {
|
|
5768
|
+
const { total, open } = alignments;
|
|
5769
|
+
if (open.length === 0) lines.push("", `**Alignments**: all ${total} handled.`);
|
|
5034
5770
|
else {
|
|
5035
5771
|
lines.push(
|
|
5036
5772
|
"",
|
|
5037
|
-
`**
|
|
5773
|
+
`**Alignments**: ${Math.max(0, total - open.length)} of ${total} handled, open (\`pnpm cella align\`):`,
|
|
5038
5774
|
""
|
|
5039
5775
|
);
|
|
5040
|
-
for (const
|
|
5041
|
-
lines.push(
|
|
5776
|
+
for (const alignment of open)
|
|
5777
|
+
lines.push(
|
|
5778
|
+
`- ${alignment.url ? `[${alignment.title}](${alignment.url})` : alignment.title} (\`${alignment.id}\`)`
|
|
5779
|
+
);
|
|
5042
5780
|
}
|
|
5043
5781
|
}
|
|
5044
5782
|
return `${lines.join("\n")}
|
|
@@ -5052,17 +5790,25 @@ async function buildSyncPrBodyForBranch(forkPath, base) {
|
|
|
5052
5790
|
const version = manifest.upstream.release ?? await readPackageVersionAtRef(forkPath, toSha).catch(() => null);
|
|
5053
5791
|
const totalCount = fromSha ? await countCommitsBetween(forkPath, fromSha, toSha).catch(() => 0) : 0;
|
|
5054
5792
|
const commits = fromSha && totalCount > 0 ? await listCommitsBetween(forkPath, fromSha, toSha, COMMIT_LIST_MAX) : [];
|
|
5055
|
-
const
|
|
5056
|
-
return buildSyncPrBody({
|
|
5793
|
+
const alignments = await readPrBodyAlignments(forkPath, toSha, manifest.upstream.repo).catch(() => void 0);
|
|
5794
|
+
return buildSyncPrBody({
|
|
5795
|
+
repoSlug: manifest.upstream.repo,
|
|
5796
|
+
version,
|
|
5797
|
+
fromSha,
|
|
5798
|
+
toSha,
|
|
5799
|
+
commits,
|
|
5800
|
+
totalCount,
|
|
5801
|
+
alignments
|
|
5802
|
+
});
|
|
5057
5803
|
}
|
|
5058
|
-
async function
|
|
5804
|
+
async function readPrBodyAlignments(forkPath, toSha, repoSlug) {
|
|
5059
5805
|
const githubUrl = repoSlug ? `https://github.com/${repoSlug}` : void 0;
|
|
5060
|
-
const [ids, pending] = await Promise.all([
|
|
5806
|
+
const [ids, pending] = await Promise.all([listAlignmentIds(forkPath, toSha), readPending(forkPath)]);
|
|
5061
5807
|
const open = await Promise.all(
|
|
5062
5808
|
pending.map(async (id) => ({
|
|
5063
5809
|
id,
|
|
5064
|
-
title: (await
|
|
5065
|
-
url:
|
|
5810
|
+
title: (await readAlignment(forkPath, toSha, id))?.title ?? id,
|
|
5811
|
+
url: alignmentUrl(githubUrl, toSha, id)
|
|
5066
5812
|
}))
|
|
5067
5813
|
);
|
|
5068
5814
|
return { total: ids.length, open };
|
|
@@ -5103,16 +5849,11 @@ async function shipSyncBranch(config, branch) {
|
|
|
5103
5849
|
}
|
|
5104
5850
|
if (ghAvailable()) {
|
|
5105
5851
|
console.info(colors_default.dim("opening a pull request..."));
|
|
5106
|
-
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase);
|
|
5107
|
-
const
|
|
5108
|
-
const pr =
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5112
|
-
});
|
|
5113
|
-
prUrl = `${pr.stdout ?? ""}
|
|
5114
|
-
${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
5115
|
-
prOpened = pr.status === 0;
|
|
5852
|
+
const prBody = await buildSyncPrBodyForBranch(forkPath, cutBase) ?? "";
|
|
5853
|
+
const repoSlug = (await readOriginGitHubUrl(forkPath))?.replace("https://github.com/", "") ?? null;
|
|
5854
|
+
const pr = createPr(forkPath, repoSlug, { base, head: branch, title: prTitle, body: prBody });
|
|
5855
|
+
prUrl = pr.url;
|
|
5856
|
+
prOpened = pr.ok;
|
|
5116
5857
|
if (!prOpened) {
|
|
5117
5858
|
console.info(colors_default.yellow("could not open the PR automatically (it may already exist). open it with:"));
|
|
5118
5859
|
if (prUrl) console.info(colors_default.dim(` ${prUrl}`));
|
|
@@ -5131,7 +5872,15 @@ ${pr.stderr ?? ""}`.match(/https?:\/\/\S+/)?.[0];
|
|
|
5131
5872
|
console.info(`${checkMark} sync branch pushed`);
|
|
5132
5873
|
console.info(colors_default.dim(` '${branch}' is on origin, ${position}`));
|
|
5133
5874
|
}
|
|
5134
|
-
|
|
5875
|
+
const steps = [
|
|
5876
|
+
prUrl ? {
|
|
5877
|
+
label: "merge",
|
|
5878
|
+
line: hyperlink(colors_default.cyan(prUrl), prUrl),
|
|
5879
|
+
details: [colors_default.dim(`squash-merge it into '${base}'; the next sync offers to while it is open`)]
|
|
5880
|
+
} : void 0,
|
|
5881
|
+
await syncAlignmentsStep(config)
|
|
5882
|
+
];
|
|
5883
|
+
printNextSteps(steps.filter((step) => !!step));
|
|
5135
5884
|
}
|
|
5136
5885
|
function finalizeWorkspace(forkPath) {
|
|
5137
5886
|
for (const args of [["install"], ["check"]]) {
|
|
@@ -5170,40 +5919,49 @@ async function runSyncCycle(config) {
|
|
|
5170
5919
|
if (config.settings.syncWithPackages !== false) {
|
|
5171
5920
|
await runPackages(config, { conflictedFiles: result.conflicts });
|
|
5172
5921
|
}
|
|
5173
|
-
if (result.conflicts.length > 0) return { status: "conflicts", branch };
|
|
5922
|
+
if (result.conflicts.length > 0) return { status: "conflicts", branch, result };
|
|
5174
5923
|
if (!await mergeInProgress(forkPath)) {
|
|
5175
5924
|
await discardTemporarySyncBranch(forkPath, branch);
|
|
5176
|
-
return { status: "noop" };
|
|
5925
|
+
return { status: "noop", result };
|
|
5177
5926
|
}
|
|
5178
|
-
return { status: "staged", branch };
|
|
5927
|
+
return { status: "staged", branch, result };
|
|
5179
5928
|
}
|
|
5180
|
-
async function commitSyncMerge(config, branch) {
|
|
5929
|
+
async function commitSyncMerge(config, branch, run) {
|
|
5181
5930
|
const { forkPath } = config;
|
|
5182
5931
|
const conflicts = await getConflictedFiles(forkPath);
|
|
5183
5932
|
console.info();
|
|
5184
5933
|
if (conflicts.length > 0) {
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
console.info(colors_default.dim("resolve and stage them, then re-run `pnpm cella sync` to finish (it commits for you)."));
|
|
5934
|
+
printStep("conflicts", `${plural(conflicts.length, "file")} still conflict on '${branch}'`, warningMark);
|
|
5935
|
+
printNextSteps(await conflictSteps(config, conflicts, run));
|
|
5188
5936
|
return;
|
|
5189
5937
|
}
|
|
5190
5938
|
await stageAll(forkPath);
|
|
5191
5939
|
if (!finalizeWorkspace(forkPath)) {
|
|
5192
5940
|
console.info();
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5941
|
+
printStep("checks", "`pnpm install` or `pnpm check` failed", warningMark);
|
|
5942
|
+
printNextSteps([
|
|
5943
|
+
{ label: "fix", line: "what failed above", details: [colors_default.dim(`the merge stays staged on '${branch}'`)] },
|
|
5944
|
+
{ label: "finish", line: "pnpm cella sync", details: [colors_default.dim("commits the sync and stops for drift triage")] }
|
|
5945
|
+
]);
|
|
5196
5946
|
return;
|
|
5197
5947
|
}
|
|
5198
5948
|
const message = await buildSyncCommitMessage(forkPath, "MERGE_HEAD");
|
|
5199
5949
|
await stageAll(forkPath);
|
|
5200
5950
|
await commitSquash(forkPath, message);
|
|
5201
5951
|
console.info();
|
|
5202
|
-
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5952
|
+
printStep("committed", `${message}
|
|
5953
|
+
${colors_default.dim(`on '${branch}', not pushed`)}`);
|
|
5954
|
+
const steps = [
|
|
5955
|
+
syncReviewStep(config, run),
|
|
5956
|
+
{
|
|
5957
|
+
label: "triage",
|
|
5958
|
+
line: "pnpm cella analyze",
|
|
5959
|
+
details: [colors_default.dim("drift triage: commit any follow-ups on this branch")]
|
|
5960
|
+
},
|
|
5961
|
+
await syncAlignmentsStep(config, run),
|
|
5962
|
+
{ label: "ship", line: "pnpm cella sync", details: [colors_default.dim("pushes the branch and opens the squash-merge PR")] }
|
|
5963
|
+
];
|
|
5964
|
+
printNextSteps(steps.filter((step) => !!step));
|
|
5207
5965
|
}
|
|
5208
5966
|
async function mergeOpenSyncPrs(config, open) {
|
|
5209
5967
|
const { forkPath, settings } = config;
|
|
@@ -5287,25 +6045,26 @@ commit them first (\`git commit --amend --no-edit\` or a new commit).`
|
|
|
5287
6045
|
}
|
|
5288
6046
|
if (await guardAgainstOpenSyncPr(config) === "cancel") return;
|
|
5289
6047
|
const outcome = await runSyncCycle(config);
|
|
5290
|
-
console.info();
|
|
5291
6048
|
if (outcome.status === "noop") {
|
|
6049
|
+
console.info();
|
|
5292
6050
|
if (outcome.behind) {
|
|
5293
6051
|
console.info(colors_default.green("nothing to sync: the last sync already went past this upstream point."));
|
|
5294
6052
|
for (const line of outcome.behind.split("\n")) console.info(colors_default.dim(` ${line}`));
|
|
5295
6053
|
} else {
|
|
5296
6054
|
console.info(colors_default.green("already up to date with upstream, nothing to sync."));
|
|
5297
6055
|
}
|
|
6056
|
+
const align = await syncAlignmentsStep(config, outcome.result);
|
|
6057
|
+
if (align) printNextSteps([align]);
|
|
5298
6058
|
return;
|
|
5299
6059
|
}
|
|
5300
6060
|
const { temporaryBranch } = outcome.branch;
|
|
5301
6061
|
if (outcome.status === "conflicts") {
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
console.info(colors_default.dim(" let the rerun commit: a manual `git commit` records a merge commit that bloats the PR."));
|
|
6062
|
+
const conflicts = await getConflictedFiles(forkPath);
|
|
6063
|
+
printStep("conflicts", `${plural(conflicts.length, "file")} on '${temporaryBranch}'`, warningMark);
|
|
6064
|
+
printNextSteps(await conflictSteps(config, conflicts, outcome.result));
|
|
5306
6065
|
return;
|
|
5307
6066
|
}
|
|
5308
|
-
await commitSyncMerge(config, temporaryBranch);
|
|
6067
|
+
await commitSyncMerge(config, temporaryBranch, outcome.result);
|
|
5309
6068
|
}
|
|
5310
6069
|
|
|
5311
6070
|
// src/services/forks.ts
|
|
@@ -5402,7 +6161,7 @@ async function runForks(config) {
|
|
|
5402
6161
|
loop: false
|
|
5403
6162
|
});
|
|
5404
6163
|
if (selectedPath === "_exit") {
|
|
5405
|
-
|
|
6164
|
+
process10.exit(0);
|
|
5406
6165
|
}
|
|
5407
6166
|
const resolvedForkPath = resolve6(forkBasePath, selectedPath);
|
|
5408
6167
|
const selectedFork = forks.find((f) => f.localPath === selectedPath);
|
|
@@ -5417,11 +6176,11 @@ async function runForks(config) {
|
|
|
5417
6176
|
|
|
5418
6177
|
// src/services/stats.ts
|
|
5419
6178
|
import { readFile as readFile3 } from "fs/promises";
|
|
5420
|
-
import { join as
|
|
6179
|
+
import { join as join18 } from "path";
|
|
5421
6180
|
|
|
5422
6181
|
// src/utils/coverage-utils.ts
|
|
5423
|
-
import { existsSync as
|
|
5424
|
-
import { join as
|
|
6182
|
+
import { existsSync as existsSync13, readFileSync as readFileSync7, statSync as statSync2 } from "fs";
|
|
6183
|
+
import { join as join17, relative as relative4, sep as sep3 } from "path";
|
|
5425
6184
|
var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
5426
6185
|
function colorPct(pct) {
|
|
5427
6186
|
if (typeof pct !== "number" || !Number.isFinite(pct)) return colors_default.dim("n/a");
|
|
@@ -5439,11 +6198,11 @@ function formatAge(ms) {
|
|
|
5439
6198
|
return `${mins} minute${mins === 1 ? "" : "s"} ago`;
|
|
5440
6199
|
}
|
|
5441
6200
|
function printCoverageSummary(forkPath) {
|
|
5442
|
-
const file =
|
|
6201
|
+
const file = join17(forkPath, ".coverage", "coverage-summary.json");
|
|
5443
6202
|
let summary;
|
|
5444
6203
|
let mtimeMs = 0;
|
|
5445
6204
|
let problem = "no coverage data found";
|
|
5446
|
-
if (
|
|
6205
|
+
if (existsSync13(file)) {
|
|
5447
6206
|
try {
|
|
5448
6207
|
mtimeMs = statSync2(file).mtimeMs;
|
|
5449
6208
|
summary = JSON.parse(readFileSync7(file, "utf8"));
|
|
@@ -5464,7 +6223,7 @@ function printCoverageSummary(forkPath) {
|
|
|
5464
6223
|
if (key === "total") continue;
|
|
5465
6224
|
const rel = relative4(forkPath, key);
|
|
5466
6225
|
if (rel.startsWith("..")) continue;
|
|
5467
|
-
const pkg = rel.split(
|
|
6226
|
+
const pkg = rel.split(sep3)[0];
|
|
5468
6227
|
const acc = perPackage.get(pkg) ?? { covered: 0, total: 0 };
|
|
5469
6228
|
acc.covered += entry.lines.covered;
|
|
5470
6229
|
acc.total += entry.lines.total;
|
|
@@ -5490,7 +6249,7 @@ function printCoverageSummary(forkPath) {
|
|
|
5490
6249
|
// src/services/stats.ts
|
|
5491
6250
|
var sourceExtensions = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "json", "jsonc", "css", "html"]);
|
|
5492
6251
|
async function getWorkspacePackages(forkPath) {
|
|
5493
|
-
const content = await readFile3(
|
|
6252
|
+
const content = await readFile3(join18(forkPath, "pnpm-workspace.yaml"), "utf8");
|
|
5494
6253
|
const patterns = readTopLevel(content, "packages");
|
|
5495
6254
|
return (Array.isArray(patterns) ? patterns.map(String) : []).map((pattern) => {
|
|
5496
6255
|
if (pattern.endsWith("/*")) {
|
|
@@ -5538,9 +6297,9 @@ async function collectStats(forkPath) {
|
|
|
5538
6297
|
const entries = [];
|
|
5539
6298
|
for (const line of output.split("\n")) {
|
|
5540
6299
|
if (!line) continue;
|
|
5541
|
-
const
|
|
5542
|
-
if (
|
|
5543
|
-
entries.push({ path: line.slice(0,
|
|
6300
|
+
const sep4 = line.lastIndexOf(":");
|
|
6301
|
+
if (sep4 === -1) continue;
|
|
6302
|
+
entries.push({ path: line.slice(0, sep4), loc: Number.parseInt(line.slice(sep4 + 1), 10) });
|
|
5544
6303
|
}
|
|
5545
6304
|
const emptyCat = () => ({
|
|
5546
6305
|
test: { files: 0, loc: 0 },
|
|
@@ -5843,26 +6602,26 @@ function getForkPath() {
|
|
|
5843
6602
|
const envPath = getEnv("CELLA_FORK_PATH");
|
|
5844
6603
|
if (envPath) {
|
|
5845
6604
|
const resolved = resolve7(envPath);
|
|
5846
|
-
if (!
|
|
6605
|
+
if (!existsSync14(resolved) || !statSync3(resolved).isDirectory()) {
|
|
5847
6606
|
throw new Error(`CELLA_FORK_PATH is not a valid directory: ${resolved}`);
|
|
5848
6607
|
}
|
|
5849
6608
|
return resolved;
|
|
5850
6609
|
}
|
|
5851
|
-
let cwd =
|
|
6610
|
+
let cwd = process11.cwd();
|
|
5852
6611
|
if (cwd.endsWith("/cli/cella") || cwd.endsWith("\\cli\\cella")) {
|
|
5853
6612
|
cwd = resolve7(cwd, "../..");
|
|
5854
6613
|
}
|
|
5855
6614
|
return cwd;
|
|
5856
6615
|
}
|
|
5857
6616
|
function preflight(forkPath) {
|
|
5858
|
-
if (!
|
|
6617
|
+
if (!existsSync14(join19(forkPath, ".git"))) {
|
|
5859
6618
|
throw new Error(`not a git repository: ${forkPath}`);
|
|
5860
6619
|
}
|
|
5861
6620
|
}
|
|
5862
6621
|
var services = {
|
|
5863
6622
|
analyze: runAnalyze,
|
|
5864
6623
|
sync: runSyncCommand,
|
|
5865
|
-
|
|
6624
|
+
align: runAlign,
|
|
5866
6625
|
audit: runAudit,
|
|
5867
6626
|
forks: runForks,
|
|
5868
6627
|
contributions: runContributions,
|
|
@@ -5873,7 +6632,7 @@ var services = {
|
|
|
5873
6632
|
}
|
|
5874
6633
|
};
|
|
5875
6634
|
function isHelpOrVersionRequest() {
|
|
5876
|
-
return
|
|
6635
|
+
return process11.argv.slice(2).some((arg) => arg === "--help" || arg === "-h" || arg === "--version" || arg === "-v") || process11.argv[2] === "help";
|
|
5877
6636
|
}
|
|
5878
6637
|
async function main() {
|
|
5879
6638
|
try {
|
|
@@ -5892,7 +6651,7 @@ async function main() {
|
|
|
5892
6651
|
} catch (error) {
|
|
5893
6652
|
console.error();
|
|
5894
6653
|
console.error(`${colors_default.red("\u2717")} ${errorMessage(error)}`);
|
|
5895
|
-
|
|
6654
|
+
process11.exit(1);
|
|
5896
6655
|
}
|
|
5897
6656
|
}
|
|
5898
6657
|
main();
|