@cellajs/cli 0.6.5 → 0.6.7

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/dist/index.js CHANGED
@@ -49,6 +49,7 @@ async function importFromApp(forkPath, file) {
49
49
  setEnv("NODE_ENV", "development");
50
50
  return import(pathToFileURL(path).href);
51
51
  }
52
+ var movedPath = (forkPath, current, legacy) => !existsSync(join(forkPath, current)) && existsSync(join(forkPath, legacy)) ? legacy : current;
52
53
  async function loadAppFacts(forkPath) {
53
54
  const parsed2 = appConfigSchema.safeParse((await importFromApp(forkPath, "shared/index.ts")).appConfig);
54
55
  if (!parsed2.success)
@@ -233,7 +234,7 @@ import process6 from "process";
233
234
  // package.json
234
235
  var package_default = {
235
236
  name: "@cellajs/cli",
236
- version: "0.6.5",
237
+ version: "0.6.7",
237
238
  private: false,
238
239
  license: "MIT",
239
240
  description: "CLI for managing your monorepo and upgrading it with upstream Cella.",
@@ -761,6 +762,15 @@ async function gitMv(cwd, oldPath, newPath) {
761
762
  await mkdir2(newDir, { recursive: true });
762
763
  await git(["mv", "-f", oldPath, newPath], cwd);
763
764
  }
765
+ async function moveFromRef(cwd, ref, from, to) {
766
+ const [mode, , blob] = (await git(["ls-tree", ref, "--", from], cwd)).split(/\s+/);
767
+ if (!blob) throw new Error(`${ref} holds no ${from}`);
768
+ await gitRm(cwd, [to], { cached: true });
769
+ await git(["update-index", "--add", "--cacheinfo", `${mode},${blob},${to}`], cwd);
770
+ await mkdir2(dirname3(join3(cwd, to)), { recursive: true });
771
+ await git(["checkout-index", "-f", "--", to], cwd);
772
+ await removeFileFully(cwd, from);
773
+ }
764
774
  async function fileExistsInWorktree(cwd, filePath) {
765
775
  return existsSync3(join3(cwd, filePath));
766
776
  }
@@ -1309,6 +1319,9 @@ function printUpstreamOverrideChanges(result) {
1309
1319
  for (const entry of report[list2].removed) {
1310
1320
  lines.push(` ${colors_default.red("\u2212")} ${list2}: ${entry}${colors_default.dim(" \xB7 dropped upstream, still in your config")}`);
1311
1321
  }
1322
+ for (const { from, to } of report[list2].moved ?? []) {
1323
+ lines.push(colors_default.dim(` ~ ${list2}: ${from} \u2192 ${to} \xB7 renamed upstream, your config never had it`));
1324
+ }
1312
1325
  }
1313
1326
  console.info(
1314
1327
  `${warningMark} ${colors_default.yellow(`upstream changed its CLI config \xB7 ${plural(lines.length, "entry", "entries")} to review`)}`
@@ -1373,11 +1386,12 @@ var STARTUP_TIMEOUT_MS = 5 * 6e4;
1373
1386
  var STOP_TIMEOUT_MS = 1e4;
1374
1387
  var DATABASE = "testbed";
1375
1388
  var DATABASE_SERVICE = "db_testbed";
1376
- var showcasePath = (forkPath) => join4(forkPath, "json/showcase.json");
1389
+ var showcaseFile = (forkPath) => movedPath(forkPath, "backend/scripts/seeds/showcase.json", "json/showcase.json");
1390
+ var showcasePath = (forkPath) => join4(forkPath, showcaseFile(forkPath));
1377
1391
  var hasShowcase = (forkPath) => existsSync4(showcasePath(forkPath));
1378
1392
  function showcaseAdmin(forkPath) {
1379
1393
  const { admin } = JSON.parse(readFileSync2(showcasePath(forkPath), "utf8"));
1380
- if (typeof admin?.email !== "string") throw new Error("json/showcase.json names no admin email");
1394
+ if (typeof admin?.email !== "string") throw new Error(`${showcaseFile(forkPath)} names no admin email`);
1381
1395
  return admin.email;
1382
1396
  }
1383
1397
  var shiftPort = (url) => {
@@ -1392,8 +1406,8 @@ async function resolveTestbed(forkPath, data = "seed") {
1392
1406
  throw new Error("shared/src/local-database.ts names no testbed database: upgrade the app from cella first");
1393
1407
  }
1394
1408
  const composeEnv = databases.composeEnv();
1395
- const envFile = join4(forkPath, "backend/.env");
1396
- const fileEnv = existsSync4(envFile) ? parseEnv(readFileSync2(envFile, "utf8")) : {};
1409
+ const envFile = [join4(forkPath, ".env"), join4(forkPath, "backend/.env")].find((path) => existsSync4(path));
1410
+ const fileEnv = envFile ? parseEnv(readFileSync2(envFile, "utf8")) : {};
1397
1411
  const portOffset = facts.devPortOffset + PORT_SHIFT;
1398
1412
  return {
1399
1413
  forkPath,
@@ -1402,7 +1416,7 @@ async function resolveTestbed(forkPath, data = "seed") {
1402
1416
  databasePort,
1403
1417
  portOffset,
1404
1418
  env: {
1405
- // The seed creates the admin that backend/.env names, and a command signs in as that user
1419
+ // The seed creates the admin that the env file names, and a command signs in as that user
1406
1420
  ...fileEnv.ADMIN_EMAIL ? { ADMIN_EMAIL: fileEnv.ADMIN_EMAIL } : {},
1407
1421
  ...getEnvSnapshot(),
1408
1422
  ...data === "showcase" ? { ADMIN_EMAIL: showcaseAdmin(forkPath) } : {},
@@ -1666,7 +1680,7 @@ async function withTestbed(forkPath, use, { fresh = false, destructive = false,
1666
1680
  }
1667
1681
  async function runTestbed(forkPath, options) {
1668
1682
  if (options.job === "stop") return stopTestbed(await resolveTestbed(forkPath));
1669
- if (options.showcase && !hasShowcase(forkPath)) throw new Error("this app keeps no json/showcase.json");
1683
+ if (options.showcase && !hasShowcase(forkPath)) throw new Error(`this app keeps no ${showcaseFile(forkPath)}`);
1670
1684
  const data = options.showcase ? "showcase" : "seed";
1671
1685
  if (options.command.length > 0) {
1672
1686
  return withTestbed(forkPath, (testbed2) => runCommand(forkPath, testbed2.env, options.command), { ...options, data });
@@ -1701,7 +1715,7 @@ var testbedCommand = {
1701
1715
  ],
1702
1716
  options: [
1703
1717
  ["--fresh", "start from an empty database: migrate and seed it again"],
1704
- ["--showcase", "hold the showcase of json/showcase.json, which the screenshots shoot, for a review loop"]
1718
+ ["--showcase", "hold the showcase seed, which the screenshots shoot, for a review loop"]
1705
1719
  ],
1706
1720
  passthrough: [
1707
1721
  "command",
@@ -1716,8 +1730,8 @@ var testbedCommand = {
1716
1730
  };
1717
1731
 
1718
1732
  // src/tools/a11y/audit.ts
1719
- import { mkdirSync as mkdirSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1720
- import { join as join9 } from "path";
1733
+ import { mkdirSync as mkdirSync3, statSync, writeFileSync as writeFileSync5 } from "fs";
1734
+ import { join as join10 } from "path";
1721
1735
 
1722
1736
  // src/tools/a11y/axe-scan.ts
1723
1737
  import axe from "axe-core";
@@ -2387,8 +2401,97 @@ function recordDecisions(forkPath, decisions) {
2387
2401
  }
2388
2402
 
2389
2403
  // src/tools/a11y/probes/page-structure.ts
2390
- import { readFileSync as readFileSync4 } from "fs";
2404
+ import { readFileSync as readFileSync5 } from "fs";
2405
+ import { join as join8 } from "path";
2406
+
2407
+ // src/tools/i18n/locales.ts
2408
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2391
2409
  import { join as join7 } from "path";
2410
+ var localesDir = (forkPath) => movedPath(forkPath, "shared/locales", "locales");
2411
+ var MANIFEST = "namespaces.json";
2412
+ var SOURCE_LANGUAGE = "en";
2413
+ var entryPattern = /^(\s+)("(?:[^"\\]|\\.)*")\s*:\s*("(?:[^"\\]|\\.)*"),?\s*$/;
2414
+ function parseLocaleFile(path, text2) {
2415
+ if (text2.trim() === "{}") return { path, indent: " ", entries: [] };
2416
+ const lines = text2.replace(/\n$/, "").split("\n");
2417
+ if (lines[0] !== "{" || lines.at(-1) !== "}") return "not one JSON object with one entry per line";
2418
+ const entries = [];
2419
+ let indent = " ";
2420
+ for (const line of lines.slice(1, -1)) {
2421
+ const match = entryPattern.exec(line);
2422
+ if (!match) return `cannot read the line ${JSON.stringify(line.trim())}`;
2423
+ indent = match[1];
2424
+ entries.push({ key: JSON.parse(match[2]), value: JSON.parse(match[3]), raw: [match[2], match[3]] });
2425
+ }
2426
+ return { path, indent, entries };
2427
+ }
2428
+ function serializeLocaleFile(file) {
2429
+ if (file.entries.length === 0) return "{}\n";
2430
+ return `{
2431
+ ${file.entries.map(({ raw }) => `${file.indent}${raw[0]}: ${raw[1]}`).join(",\n")}
2432
+ }
2433
+ `;
2434
+ }
2435
+ function writeLocaleFile(forkPath, file) {
2436
+ writeFileSync3(join7(forkPath, file.path), serializeLocaleFile(file));
2437
+ }
2438
+ var textsOf = (file) => Object.fromEntries((file?.entries ?? []).map(({ key, value }) => [key, value]));
2439
+ var readJsonObject = (path) => {
2440
+ const parsed2 = JSON.parse(readFileSync4(path, "utf8"));
2441
+ if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) throw new Error(`${path} is not a JSON object`);
2442
+ return Object.fromEntries(Object.entries(parsed2));
2443
+ };
2444
+ function readLocales(forkPath) {
2445
+ const localesPath = localesDir(forkPath);
2446
+ const dir = join7(forkPath, localesPath);
2447
+ if (!existsSync7(join7(dir, SOURCE_LANGUAGE))) throw new Error(`no ${localesPath}/${SOURCE_LANGUAGE}/ in ${forkPath}`);
2448
+ const languages = readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort((a, b) => a === SOURCE_LANGUAGE ? -1 : b === SOURCE_LANGUAGE ? 1 : a.localeCompare(b));
2449
+ const files2 = /* @__PURE__ */ new Map();
2450
+ for (const language2 of languages) {
2451
+ for (const name of readdirSync(join7(dir, language2)).filter((name2) => name2.endsWith(".json"))) {
2452
+ const path = `${localesPath}/${language2}/${name}`;
2453
+ const parsed2 = parseLocaleFile(path, readFileSync4(join7(forkPath, path), "utf8"));
2454
+ if (typeof parsed2 === "string") throw new Error(`${path}: ${parsed2}; pnpm lint:fix writes one entry per line`);
2455
+ files2.set(`${language2}/${name.slice(0, -".json".length)}`, parsed2);
2456
+ }
2457
+ }
2458
+ const manifestPath = join7(dir, MANIFEST);
2459
+ if (!existsSync7(manifestPath)) throw new Error(`no ${localesPath}/${MANIFEST}: upgrade the app from cella first`);
2460
+ const merged = readJsonObject(manifestPath);
2461
+ const namespaces = /* @__PURE__ */ new Map();
2462
+ for (const [namespace, sources] of Object.entries(merged)) {
2463
+ if (!Array.isArray(sources) || !sources.every((source) => typeof source === "string")) {
2464
+ throw new Error(`${localesPath}/${MANIFEST}: ${namespace} must list file names`);
2465
+ }
2466
+ namespaces.set(namespace, sources);
2467
+ }
2468
+ const mergedFiles = new Set([...namespaces.values()].flat());
2469
+ const ownFiles = [...new Set([...files2.keys()].map((key) => key.split("/")[1]))].filter(
2470
+ (file) => !mergedFiles.has(file)
2471
+ );
2472
+ for (const file of ownFiles.sort()) namespaces.set(file, [file]);
2473
+ return { dir: localesPath, languages, namespaces, files: files2 };
2474
+ }
2475
+ function namespaceTexts(locales, language2, namespace) {
2476
+ const sources = locales.namespaces.get(namespace) ?? [];
2477
+ return Object.assign({}, ...sources.map((file) => textsOf(locales.files.get(`${language2}/${file}`))));
2478
+ }
2479
+ var lineOf = (file, key) => file.entries.findIndex((entry) => entry.key === key) + 2;
2480
+ var pluralSuffix = /_(ordinal_)?(zero|one|two|few|many|other)$/;
2481
+ function pluralForm(key) {
2482
+ const match = pluralSuffix.exec(key);
2483
+ if (!match) return void 0;
2484
+ return { base: key.slice(0, match.index), category: match[2], ordinal: !!match[1] };
2485
+ }
2486
+ var withPluralForms = (key) => [
2487
+ key,
2488
+ ...["zero", "one", "two", "few", "many", "other"].flatMap((category) => [
2489
+ `${key}_${category}`,
2490
+ `${key}_ordinal_${category}`
2491
+ ])
2492
+ ];
2493
+
2494
+ // src/tools/a11y/probes/page-structure.ts
2392
2495
  var titles = { criteria: ["2.4.2"], check: "probe:titles", what: "A distinct, descriptive page title" };
2393
2496
  var language = {
2394
2497
  criteria: ["3.1.1"],
@@ -2422,7 +2525,7 @@ var pageStructure = async (page, state, { app }) => {
2422
2525
  var languageSwitch = async (page, state, { app, forkPath }) => {
2423
2526
  if (state.open) return [];
2424
2527
  const names = JSON.parse(
2425
- readFileSync4(join7(forkPath, "locales", app.defaultLanguage, "common.json"), "utf8")
2528
+ readFileSync5(join8(forkPath, localesDir(forkPath), app.defaultLanguage, "common.json"), "utf8")
2426
2529
  );
2427
2530
  const findings2 = [];
2428
2531
  for (const other of app.languages.filter((lng) => lng !== app.defaultLanguage)) {
@@ -2756,15 +2859,15 @@ var inputs = async (page, state) => {
2756
2859
  };
2757
2860
 
2758
2861
  // src/tools/a11y/review-packet.ts
2759
- import { mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2760
- import { join as join8 } from "path";
2761
- var packetDir = (run, stateId) => join8(run.resultsDir, "review", stateId);
2862
+ import { mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
2863
+ import { join as join9 } from "path";
2864
+ var packetDir = (run, stateId) => join9(run.resultsDir, "review", stateId);
2762
2865
  function writePacketFile(run, stateId, name, content) {
2763
2866
  mkdirSync2(packetDir(run, stateId), { recursive: true });
2764
- writeFileSync3(join8(packetDir(run, stateId), name), content);
2867
+ writeFileSync4(join9(packetDir(run, stateId), name), content);
2765
2868
  }
2766
2869
  function removePacketFile(run, stateId, name) {
2767
- rmSync2(join8(packetDir(run, stateId), name), { force: true });
2870
+ rmSync2(join9(packetDir(run, stateId), name), { force: true });
2768
2871
  }
2769
2872
  function pageFacts() {
2770
2873
  const text2 = (el) => (el?.textContent ?? "").trim().replace(/\s+/g, " ");
@@ -3224,8 +3327,8 @@ var seconds = (since) => `${((Date.now() - since) / 1e3).toFixed(1)}s`;
3224
3327
  async function runAudit(run, scope, testbed, options) {
3225
3328
  const startedAt = Date.now();
3226
3329
  const email = options.email ?? testbed.env.ADMIN_EMAIL;
3227
- if (!email) throw new Error("no audit user: pass --email, or set ADMIN_EMAIL in backend/.env");
3228
- const builtAt = statSync(join9(run.forkPath, "frontend/dist/index.html"), { throwIfNoEntry: false })?.mtime;
3330
+ if (!email) throw new Error("no audit user: pass --email, or set ADMIN_EMAIL in .env");
3331
+ const builtAt = statSync(join10(run.forkPath, "frontend/dist/index.html"), { throwIfNoEntry: false })?.mtime;
3229
3332
  printStep("auditing", `${testbed.frontendUrl}, frontend build of ${builtAt?.toLocaleString() ?? "unknown date"}`);
3230
3333
  const states = options.states.length > 0 ? scope.states.filter((state) => options.states.includes(state.id)) : scope.states;
3231
3334
  const session = await startAudit(run.forkPath, scope, { email, env: testbed.env });
@@ -3265,12 +3368,12 @@ async function runAudit(run, scope, testbed, options) {
3265
3368
  mkdirSync3(run.resultsDir, { recursive: true });
3266
3369
  const axe2 = summarizeAxe(scans);
3267
3370
  for (const [id, items] of axe2.evidence) for (const item of items) evidence.add([id], item);
3268
- writeFileSync4(join9(run.resultsDir, "axe.json"), `${JSON.stringify({ ...axe2.report, errors }, null, 2)}
3371
+ writeFileSync5(join10(run.resultsDir, "axe.json"), `${JSON.stringify({ ...axe2.report, errors }, null, 2)}
3269
3372
  `);
3270
3373
  evidence.addFindings(acrossStates(findings2));
3271
3374
  runCodeChecks(evidence, run, scope.sources);
3272
3375
  const skipped2 = Object.keys(errors).length;
3273
- const target = options.states.length > 0 || skipped2 ? join9(run.resultsDir, "ledger-partial.json") : join9(run.forkPath, ledgerFile);
3376
+ const target = options.states.length > 0 || skipped2 ? join10(run.resultsDir, "ledger-partial.json") : join10(run.forkPath, ledgerFile);
3274
3377
  const ledger = writeLedger(
3275
3378
  run.forkPath,
3276
3379
  evidence.byCriterion,
@@ -3294,10 +3397,10 @@ async function runAudit(run, scope, testbed, options) {
3294
3397
  }
3295
3398
 
3296
3399
  // src/tools/a11y/decide.ts
3297
- import { readFileSync as readFileSync5 } from "fs";
3400
+ import { readFileSync as readFileSync6 } from "fs";
3298
3401
  import { resolve as resolve4 } from "path";
3299
3402
  function runDecide(forkPath, { file, decision }) {
3300
- const input2 = file ? JSON.parse(readFileSync5(resolve4(file), "utf8")) : [decision];
3403
+ const input2 = file ? JSON.parse(readFileSync6(resolve4(file), "utf8")) : [decision];
3301
3404
  if (!Array.isArray(input2)) throw new Error(`${file} holds no list of decisions`);
3302
3405
  const decisions = input2.map(parseDecision);
3303
3406
  const ledger = recordDecisions(forkPath, decisions);
@@ -3311,8 +3414,8 @@ function runDecide(forkPath, { file, decision }) {
3311
3414
 
3312
3415
  // src/tools/a11y/report.ts
3313
3416
  import { spawnSync as spawnSync5 } from "child_process";
3314
- import { copyFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3315
- import { dirname as dirname4, join as join10 } from "path";
3417
+ import { copyFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
3418
+ import { dirname as dirname4, join as join11 } from "path";
3316
3419
  var statusLabel = {
3317
3420
  supports: "Supports",
3318
3421
  "partially-supports": "Partially Supports",
@@ -3431,7 +3534,7 @@ Not evaluated.
3431
3534
 
3432
3535
  This report describes the product as tested on the report date. Accessibility can change as the product changes.
3433
3536
  `;
3434
- const tracked = spawnSync5("git", ["ls-files", "frontend/src", "backend/src", "locales"], {
3537
+ const tracked = spawnSync5("git", ["ls-files", "frontend/src", "backend/src", "shared/locales", "locales"], {
3435
3538
  cwd: forkPath,
3436
3539
  encoding: "utf8"
3437
3540
  }).stdout.split("\n");
@@ -3458,7 +3561,7 @@ NVDA and Firefox on Windows, keyboard only, in light and dark mode.
3458
3561
 
3459
3562
  ## Confirm: decided by an agent (${byAgent.length})
3460
3563
 
3461
- The agent judged these from the review packets in \`${join10(resultsDir, "review")}/\`. Check its evidence, then record your own
3564
+ The agent judged these from the review packets in \`${join11(resultsDir, "review")}/\`. Check its evidence, then record your own
3462
3565
  decision; a wrong Supports here ends up in the published report.
3463
3566
 
3464
3567
  ${byAgent.map((r) => {
@@ -3502,11 +3605,11 @@ ${failing.map((r) => {
3502
3605
  }).join("\n")}
3503
3606
  `;
3504
3607
  mkdirSync4(resultsDir, { recursive: true });
3505
- writeFileSync5(join10(resultsDir, "vpat-draft.md"), vpat);
3506
- writeFileSync5(join10(resultsDir, "manual-pass.md"), checklist);
3608
+ writeFileSync6(join11(resultsDir, "vpat-draft.md"), vpat);
3609
+ writeFileSync6(join11(resultsDir, "manual-pass.md"), checklist);
3507
3610
  const html = toHtml(vpat, `${app.name} Accessibility Conformance Report`);
3508
- const pdfPath = join10(resultsDir, "conformance-report.pdf");
3509
- writeFileSync5(join10(resultsDir, "conformance-report.html"), html);
3611
+ const pdfPath = join11(resultsDir, "conformance-report.pdf");
3612
+ writeFileSync6(join11(resultsDir, "conformance-report.html"), html);
3510
3613
  const { chromium } = await loadPlaywright(forkPath);
3511
3614
  const browser = await chromium.launch();
3512
3615
  try {
@@ -3526,12 +3629,12 @@ ${failing.map((r) => {
3526
3629
  if (unconfirmed && !draft) {
3527
3630
  const ids = [...open, ...byAgent].map((r) => r.id).join(", ");
3528
3631
  throw new Error(
3529
- `not published: ${open.length} criteria are open and ${byAgent.length} are decided by an agent only (${ids}). See ${join10(resultsDir, "manual-pass.md")}, or add --draft to publish it as a draft`
3632
+ `not published: ${open.length} criteria are open and ${byAgent.length} are decided by an agent only (${ids}). See ${join11(resultsDir, "manual-pass.md")}, or add --draft to publish it as a draft`
3530
3633
  );
3531
3634
  }
3532
3635
  const publicPath = "frontend/public/static/common/accessibility-conformance-report.pdf";
3533
- mkdirSync4(dirname4(join10(forkPath, publicPath)), { recursive: true });
3534
- copyFileSync(pdfPath, join10(forkPath, publicPath));
3636
+ mkdirSync4(dirname4(join11(forkPath, publicPath)), { recursive: true });
3637
+ copyFileSync(pdfPath, join11(forkPath, publicPath));
3535
3638
  const count3 = (status) => ledger.criteria.filter((r) => r.status === status).length;
3536
3639
  const review = {
3537
3640
  standard: ledger.standard,
@@ -3622,7 +3725,7 @@ var a11yCommand = {
3622
3725
  options: [
3623
3726
  ["--states <ids>", "audit: these states only, comma separated; writes ledger-partial.json, never the ledger"],
3624
3727
  ["--workers <n>", "audit: states audited at the same time (default 4)"],
3625
- ["--email <address>", "audit: the audit user (default ADMIN_EMAIL in backend/.env)"],
3728
+ ["--email <address>", "audit: the audit user (default ADMIN_EMAIL in .env)"],
3626
3729
  ["--publish", "report: put the PDF where the app serves it and print what the statement needs"],
3627
3730
  ["--draft", "report: publish while rows still need a person; the statement calls the results provisional"],
3628
3731
  ["--file <path>", "decide: a JSON list of decisions"],
@@ -3638,8 +3741,8 @@ var a11yCommand = {
3638
3741
  };
3639
3742
 
3640
3743
  // src/tools/bench/command.ts
3641
- import { existsSync as existsSync7, readdirSync } from "fs";
3642
- import { basename, join as join11 } from "path";
3744
+ import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
3745
+ import { basename, join as join12 } from "path";
3643
3746
 
3644
3747
  // src/tools/bench/request.ts
3645
3748
  var BENCH_REQUEST_ENV = "CELLA_BENCH";
@@ -3648,11 +3751,11 @@ var BENCH_REQUEST_ENV = "CELLA_BENCH";
3648
3751
  var BENCH_DIR = "backend/bench";
3649
3752
  var ENTRY_COMMAND = ["pnpm", "exec", "tsx", "--no-experimental-webstorage", "bench/run.ts"];
3650
3753
  async function runBench(forkPath, options) {
3651
- if (!existsSync7(join11(forkPath, BENCH_DIR, "run.ts"))) {
3754
+ if (!existsSync8(join12(forkPath, BENCH_DIR, "run.ts"))) {
3652
3755
  throw new Error(`${BENCH_DIR}/run.ts is missing: upgrade the app from cella first`);
3653
3756
  }
3654
- const scenariosDir = join11(forkPath, BENCH_DIR, "scenarios");
3655
- const names = readdirSync(scenariosDir).filter((file) => file.endsWith(".ts")).map((file) => basename(file, ".ts")).sort();
3757
+ const scenariosDir = join12(forkPath, BENCH_DIR, "scenarios");
3758
+ const names = readdirSync2(scenariosDir).filter((file) => file.endsWith(".ts")).map((file) => basename(file, ".ts")).sort();
3656
3759
  if (options.scenario && !names.includes(options.scenario)) {
3657
3760
  throw new Error(`unknown scenario '${options.scenario}'. expected one of: ${names.join(", ")}`);
3658
3761
  }
@@ -3664,7 +3767,7 @@ async function runBench(forkPath, options) {
3664
3767
  scenariosDir,
3665
3768
  dataDir: toolDir(forkPath, "bench", "repository")
3666
3769
  };
3667
- const run = (env) => runCommand(join11(forkPath, "backend"), { ...env, [BENCH_REQUEST_ENV]: JSON.stringify(request) }, ENTRY_COMMAND);
3770
+ const run = (env) => runCommand(join12(forkPath, "backend"), { ...env, [BENCH_REQUEST_ENV]: JSON.stringify(request) }, ENTRY_COMMAND);
3668
3771
  return options.isolated ? withTestbed(forkPath, (testbed) => run(testbed.env)) : run(getEnvSnapshot());
3669
3772
  }
3670
3773
  var benchCommand = {
@@ -3703,98 +3806,12 @@ import { spawnSync as spawnSync6 } from "child_process";
3703
3806
  import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
3704
3807
  import { join as join14 } from "path";
3705
3808
 
3706
- // src/tools/i18n/locales.ts
3707
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
3708
- import { join as join12 } from "path";
3709
- var LOCALES_DIR = "locales";
3710
- var MANIFEST = "namespaces.json";
3711
- var SOURCE_LANGUAGE = "en";
3712
- var entryPattern = /^(\s+)("(?:[^"\\]|\\.)*")\s*:\s*("(?:[^"\\]|\\.)*"),?\s*$/;
3713
- function parseLocaleFile(path, text2) {
3714
- if (text2.trim() === "{}") return { path, indent: " ", entries: [] };
3715
- const lines = text2.replace(/\n$/, "").split("\n");
3716
- if (lines[0] !== "{" || lines.at(-1) !== "}") return "not one JSON object with one entry per line";
3717
- const entries = [];
3718
- let indent = " ";
3719
- for (const line of lines.slice(1, -1)) {
3720
- const match = entryPattern.exec(line);
3721
- if (!match) return `cannot read the line ${JSON.stringify(line.trim())}`;
3722
- indent = match[1];
3723
- entries.push({ key: JSON.parse(match[2]), value: JSON.parse(match[3]), raw: [match[2], match[3]] });
3724
- }
3725
- return { path, indent, entries };
3726
- }
3727
- function serializeLocaleFile(file) {
3728
- if (file.entries.length === 0) return "{}\n";
3729
- return `{
3730
- ${file.entries.map(({ raw }) => `${file.indent}${raw[0]}: ${raw[1]}`).join(",\n")}
3731
- }
3732
- `;
3733
- }
3734
- function writeLocaleFile(forkPath, file) {
3735
- writeFileSync6(join12(forkPath, file.path), serializeLocaleFile(file));
3736
- }
3737
- var textsOf = (file) => Object.fromEntries((file?.entries ?? []).map(({ key, value }) => [key, value]));
3738
- var readJsonObject = (path) => {
3739
- const parsed2 = JSON.parse(readFileSync6(path, "utf8"));
3740
- if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) throw new Error(`${path} is not a JSON object`);
3741
- return Object.fromEntries(Object.entries(parsed2));
3742
- };
3743
- function readLocales(forkPath) {
3744
- const dir = join12(forkPath, LOCALES_DIR);
3745
- if (!existsSync8(join12(dir, SOURCE_LANGUAGE))) throw new Error(`no ${LOCALES_DIR}/${SOURCE_LANGUAGE}/ in ${forkPath}`);
3746
- const languages = readdirSync2(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".")).map((entry) => entry.name).sort((a, b) => a === SOURCE_LANGUAGE ? -1 : b === SOURCE_LANGUAGE ? 1 : a.localeCompare(b));
3747
- const files2 = /* @__PURE__ */ new Map();
3748
- for (const language2 of languages) {
3749
- for (const name of readdirSync2(join12(dir, language2)).filter((name2) => name2.endsWith(".json"))) {
3750
- const path = `${LOCALES_DIR}/${language2}/${name}`;
3751
- const parsed2 = parseLocaleFile(path, readFileSync6(join12(forkPath, path), "utf8"));
3752
- if (typeof parsed2 === "string") throw new Error(`${path}: ${parsed2}; pnpm lint:fix writes one entry per line`);
3753
- files2.set(`${language2}/${name.slice(0, -".json".length)}`, parsed2);
3754
- }
3755
- }
3756
- const manifestPath = join12(dir, MANIFEST);
3757
- if (!existsSync8(manifestPath)) throw new Error(`no ${LOCALES_DIR}/${MANIFEST}: upgrade the app from cella first`);
3758
- const merged = readJsonObject(manifestPath);
3759
- const namespaces = /* @__PURE__ */ new Map();
3760
- for (const [namespace, sources] of Object.entries(merged)) {
3761
- if (!Array.isArray(sources) || !sources.every((source) => typeof source === "string")) {
3762
- throw new Error(`${LOCALES_DIR}/${MANIFEST}: ${namespace} must list file names`);
3763
- }
3764
- namespaces.set(namespace, sources);
3765
- }
3766
- const mergedFiles = new Set([...namespaces.values()].flat());
3767
- const ownFiles = [...new Set([...files2.keys()].map((key) => key.split("/")[1]))].filter(
3768
- (file) => !mergedFiles.has(file)
3769
- );
3770
- for (const file of ownFiles.sort()) namespaces.set(file, [file]);
3771
- return { languages, namespaces, files: files2 };
3772
- }
3773
- function namespaceTexts(locales, language2, namespace) {
3774
- const sources = locales.namespaces.get(namespace) ?? [];
3775
- return Object.assign({}, ...sources.map((file) => textsOf(locales.files.get(`${language2}/${file}`))));
3776
- }
3777
- var lineOf = (file, key) => file.entries.findIndex((entry) => entry.key === key) + 2;
3778
- var pluralSuffix = /_(ordinal_)?(zero|one|two|few|many|other)$/;
3779
- function pluralForm(key) {
3780
- const match = pluralSuffix.exec(key);
3781
- if (!match) return void 0;
3782
- return { base: key.slice(0, match.index), category: match[2], ordinal: !!match[1] };
3783
- }
3784
- var withPluralForms = (key) => [
3785
- key,
3786
- ...["zero", "one", "two", "few", "many", "other"].flatMap((category) => [
3787
- `${key}_${category}`,
3788
- `${key}_ordinal_${category}`
3789
- ])
3790
- ];
3791
-
3792
3809
  // src/tools/i18n/scan.ts
3793
3810
  import { execFileSync as execFileSync2 } from "child_process";
3794
3811
  import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3795
3812
  import { dirname as dirname5, join as join13 } from "path";
3796
3813
  var codeFile = /\.(?:[cm]?[jt]sx?)$/;
3797
- var skipped = (path) => path.endsWith(".d.ts") || path.startsWith("locales/") || path.startsWith("cella/alignments/") || path.split("/").some((segment) => segment === "node_modules" || segment === "gen" || segment.includes(".gen."));
3814
+ var skipped = (path) => path.endsWith(".d.ts") || path.startsWith("locales/") || path.startsWith("shared/locales/") || path.startsWith("cella/alignments/") || path.split("/").some((segment) => segment === "node_modules" || segment === "gen" || segment.includes(".gen."));
3798
3815
  var MAX_FILE_BYTES = 1e6;
3799
3816
  var MAX_COMBINATIONS = 500;
3800
3817
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4211,10 +4228,10 @@ function printList(title, items) {
4211
4228
  console.info(` ${colors_default.dim(`${items.slice(0, LIST_MAX).join(", ")}${more}`)}`);
4212
4229
  }
4213
4230
  var describe = ({ key, missing, extra }) => [key, ...missing.map((name) => `-${name}`), ...extra.map((name) => `+${name}`)].join(" ");
4214
- function printStatus(report) {
4231
+ function printStatus(report, dir) {
4215
4232
  const label = ({ name, files: files2 }) => files2.length > 1 || files2[0] !== name ? `${name} (${files2.join(" + ")})` : name;
4216
4233
  console.info(
4217
- `${colors_default.bold(`${LOCALES_DIR}/`)} ${colors_default.dim(`${report.languages.join(", ")} \xB7 namespaces ${report.namespaces.map(label).join(", ")}`)}`
4234
+ `${colors_default.bold(`${dir}/`)} ${colors_default.dim(`${report.languages.join(", ")} \xB7 namespaces ${report.namespaces.map(label).join(", ")}`)}`
4218
4235
  );
4219
4236
  console.info();
4220
4237
  console.info(colors_default.dim(row2(columns.map(([title]) => title))));
@@ -4286,9 +4303,10 @@ function printStatus(report) {
4286
4303
  console.info(` ${colors_default.dim("missing and same-as-English keys: --json lists them \xB7 cella i18n unused")}`);
4287
4304
  }
4288
4305
  function runStatus(forkPath, json) {
4289
- const report = buildStatus(readLocales(forkPath));
4306
+ const locales = readLocales(forkPath);
4307
+ const report = buildStatus(locales);
4290
4308
  if (json) writeStdout(JSON.stringify(report, null, 2));
4291
- else printStatus(report);
4309
+ else printStatus(report, locales.dir);
4292
4310
  return defectCount(report) === 0 ? 0 : 1;
4293
4311
  }
4294
4312
 
@@ -5391,7 +5409,7 @@ async function spinnersGone(page, quiet = 1e3, cap = 45e3) {
5391
5409
  var hasMfaScript = (forkPath) => Boolean(JSON.parse(readFileSync13(join21(forkPath, "backend/package.json"), "utf8")).scripts?.["showcase:mfa"]);
5392
5410
  async function shoot(forkPath, list2, testbed, options) {
5393
5411
  const email = options.email ?? testbed.env.ADMIN_EMAIL;
5394
- if (!email) throw new Error("no shot user: pass --email, or set ADMIN_EMAIL in backend/.env");
5412
+ if (!email) throw new Error("no shot user: pass --email, or set ADMIN_EMAIL in .env");
5395
5413
  const todo = options.ids.length > 0 ? list2.shots.filter((shot2) => options.ids.includes(shot2.id)) : list2.shots;
5396
5414
  const user = mintSession(email, { forkPath, env: testbed.env });
5397
5415
  const scratch = mkdtempSync(join21(tmpdir(), "cella-screenshots-"));
@@ -5520,7 +5538,7 @@ var screenshotsCommand = {
5520
5538
  operands: ["[ids...]", "the shots to take (default: all)"],
5521
5539
  options: [
5522
5540
  ["--out <dir>", "write into this folder, a trial run that leaves the working tree alone"],
5523
- ["--email <address>", "the shot user (default the showcase admin, else ADMIN_EMAIL in backend/.env)"]
5541
+ ["--email <address>", "the shot user (default the showcase admin, else ADMIN_EMAIL in .env)"]
5524
5542
  ],
5525
5543
  parse: ({ operands, opts }) => ({
5526
5544
  ids: operands,
@@ -5779,7 +5797,9 @@ var alignmentExempt = [
5779
5797
  "json/",
5780
5798
  "locales/",
5781
5799
  "sdk/gen",
5782
- "shared/config"
5800
+ "shared/config",
5801
+ "shared/json/",
5802
+ "shared/locales/"
5783
5803
  ];
5784
5804
  function ownedPaths(configSource) {
5785
5805
  const lists = [...configSource.matchAll(/\b(?:ignored|pinned): \[([\s\S]*?)\n {4}\]/g)].map((match) => match[1]);
@@ -7035,13 +7055,27 @@ function parseOverrideLists(source) {
7035
7055
  }
7036
7056
  }
7037
7057
  function diffOverrideLists(base, incoming, fork) {
7058
+ const basenameOf2 = (entry) => entry.slice(entry.lastIndexOf("/") + 1);
7038
7059
  const diff = (key) => {
7039
7060
  const baseSet = new Set(base[key].map(normalizeEntry).filter(Boolean));
7040
7061
  const incomingSet = new Set(incoming[key].map(normalizeEntry).filter(Boolean));
7041
7062
  const forkEntries = fork[key].map(normalizeEntry).filter(Boolean);
7063
+ const newEntries = [...incomingSet].filter((entry) => !baseSet.has(entry) && !isUnderAnyFolder(entry, forkEntries));
7064
+ const renameSources = [...baseSet].filter(
7065
+ (entry) => !incomingSet.has(entry) && !forkEntries.includes(entry) && !isUnderAnyFolder(entry, forkEntries)
7066
+ );
7067
+ const moved = [];
7068
+ const added = newEntries.filter((entry) => {
7069
+ const sourceAt = renameSources.findIndex((source) => basenameOf2(source) === basenameOf2(entry));
7070
+ if (sourceAt === -1) return true;
7071
+ moved.push({ from: renameSources[sourceAt], to: entry });
7072
+ renameSources.splice(sourceAt, 1);
7073
+ return false;
7074
+ });
7042
7075
  return {
7043
- added: [...incomingSet].filter((entry) => !baseSet.has(entry) && !isUnderAnyFolder(entry, forkEntries)),
7044
- removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry))
7076
+ added,
7077
+ removed: [...baseSet].filter((entry) => !incomingSet.has(entry) && forkEntries.includes(entry)),
7078
+ ...moved.length > 0 ? { moved } : {}
7045
7079
  };
7046
7080
  };
7047
7081
  const diffKeys = () => {
@@ -7069,9 +7103,17 @@ async function compareUpstreamOverrides(repoPath, mergeBaseRef, incomingRef, for
7069
7103
  packageJsonKeys: forkConfig.settings.packageJsonKeys ?? DEFAULT_PACKAGE_JSON_KEYS
7070
7104
  };
7071
7105
  const changes = diffOverrideLists(base, incoming, fork);
7072
- const count3 = Object.values(changes).reduce((n, list2) => n + list2.added.length + list2.removed.length, 0);
7106
+ const count3 = Object.values(changes).reduce(
7107
+ (n, list2) => n + list2.added.length + list2.removed.length + (list2.moved?.length ?? 0),
7108
+ 0
7109
+ );
7073
7110
  return count3 > 0 ? { kind: "changes", ...changes } : void 0;
7074
7111
  }
7112
+ function hasBlockingOverrideChanges(report) {
7113
+ if (report.kind !== "changes") return false;
7114
+ const lists = [report.pinned, report.ignored, report.packageJsonKeys];
7115
+ return lists.some((list2) => list2.added.length > 0 || list2.removed.length > 0);
7116
+ }
7075
7117
 
7076
7118
  // src/utils/overrides.ts
7077
7119
  function normalizeEntry(entry) {
@@ -7120,8 +7162,9 @@ async function validateOverrides(config, forkPath, upstreamRef) {
7120
7162
  const path = normalizeEntry(entry);
7121
7163
  const ignoredEntry = ignored.find((folder) => isUnderAnyFolder(path, [folder]));
7122
7164
  if (/[*?]/.test(entry)) warnings.push(`pin uses glob (not supported, use a path or folder): ${entry}`);
7123
- else if (!existsSync21(join28(forkPath, path))) warnings.push(`pin not found: ${entry}`);
7124
- else if (ignoredEntry) warnings.push(`pin does nothing, '${normalizeEntry(ignoredEntry)}' is ignored: ${entry}`);
7165
+ else if (!existsSync21(join28(forkPath, path))) {
7166
+ if (!upstream || !await fileExistsAtRef(forkPath, upstream, path)) warnings.push(`pin not found: ${entry}`);
7167
+ } else if (ignoredEntry) warnings.push(`pin does nothing, '${normalizeEntry(ignoredEntry)}' is ignored: ${entry}`);
7125
7168
  else if (!upstream) continue;
7126
7169
  else if (await fileExistsAtRef(forkPath, upstream, path)) pinsUpstreamHas.push(entry);
7127
7170
  else warnings.push(`pin does nothing, upstream no longer has it: ${entry}`);
@@ -7142,6 +7185,7 @@ async function validateOverrides(config, forkPath, upstreamRef) {
7142
7185
  warnings.push(`ignored entry uses glob (not supported, use a path or folder): ${entry}`);
7143
7186
  } else if (!existsSync21(join28(forkPath, path))) {
7144
7187
  if (upstreamRef && await fileExistsAtRef(forkPath, upstreamRef, path)) continue;
7188
+ if (isGeneratedFile(path)) continue;
7145
7189
  warnings.push(`ignored entry not found: ${entry}`);
7146
7190
  }
7147
7191
  }
@@ -7472,6 +7516,109 @@ import { existsSync as existsSync22 } from "fs";
7472
7516
  import { rm as rm2 } from "fs/promises";
7473
7517
  import { join as join29 } from "path";
7474
7518
 
7519
+ // src/utils/audit-utils.ts
7520
+ import { execFile as execFile2 } from "child_process";
7521
+ import { promisify as promisify2 } from "util";
7522
+ var execFileAsync2 = promisify2(execFile2);
7523
+ function outdatedPackageName(key) {
7524
+ const name = key.split(" ")[0];
7525
+ const at = name.indexOf("@", 1);
7526
+ return at === -1 ? name : name.slice(0, at);
7527
+ }
7528
+ function getRepoUrl(repositoryUrl) {
7529
+ if (!repositoryUrl) return null;
7530
+ let url = repositoryUrl;
7531
+ if (url.startsWith("git+")) {
7532
+ url = url.slice(4);
7533
+ }
7534
+ if (url.endsWith(".git")) {
7535
+ url = url.slice(0, -4);
7536
+ }
7537
+ if (url.startsWith("git://")) {
7538
+ url = url.replace("git://", "https://");
7539
+ }
7540
+ return url;
7541
+ }
7542
+ function isGitHubRepoUrl(repoUrl) {
7543
+ if (!repoUrl) return false;
7544
+ try {
7545
+ const parsed2 = new URL(repoUrl);
7546
+ return parsed2.hostname === "github.com" || parsed2.hostname === "www.github.com";
7547
+ } catch {
7548
+ return false;
7549
+ }
7550
+ }
7551
+ function getReleasesUrl(repoUrl) {
7552
+ if (!isGitHubRepoUrl(repoUrl)) return null;
7553
+ return `${repoUrl}/releases`;
7554
+ }
7555
+ async function runPnpm(args, cwd) {
7556
+ try {
7557
+ const { stdout, stderr } = await execFileAsync2("pnpm", args, {
7558
+ encoding: "utf8",
7559
+ maxBuffer: 10 * 1024 * 1024,
7560
+ cwd
7561
+ });
7562
+ return { ok: true, stdout, stderr };
7563
+ } catch (error) {
7564
+ const { stdout = "", stderr = "" } = error;
7565
+ return { ok: false, stdout, stderr: stdout || stderr ? stderr : String(error) };
7566
+ }
7567
+ }
7568
+ async function runPnpmJson(args, cwd) {
7569
+ try {
7570
+ return JSON.parse((await runPnpm([...args, "--loglevel=error"], cwd)).stdout);
7571
+ } catch {
7572
+ return null;
7573
+ }
7574
+ }
7575
+ async function runPnpmAudit(cwd) {
7576
+ const result = await runPnpmJson(["audit", "--json"], cwd);
7577
+ if (!result || typeof result.advisories !== "object" || result.advisories === null) return null;
7578
+ return result;
7579
+ }
7580
+ async function readReleaseAgeMinutes(cwd) {
7581
+ const minutes = Number.parseInt((await runPnpm(["config", "get", "minimumReleaseAge"], cwd)).stdout, 10);
7582
+ return Number.isNaN(minutes) ? 0 : minutes;
7583
+ }
7584
+ async function fetchPublishTimes(name, cwd) {
7585
+ const times = await runPnpmJson(["view", name, "time", "--json"], cwd);
7586
+ return times && typeof times === "object" && !("error" in times) ? times : null;
7587
+ }
7588
+ function editJsonText(content, change) {
7589
+ const json = JSON.parse(content);
7590
+ change(json);
7591
+ const indent = content.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
7592
+ return `${JSON.stringify(json, null, indent)}${content.endsWith("\n") ? "\n" : ""}`;
7593
+ }
7594
+ function rewriteSpecifier(spec, latest) {
7595
+ const match = spec.match(/^([\^~]?)\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?$/);
7596
+ return match ? `${match[1]}${latest}` : null;
7597
+ }
7598
+ var vulnIcons = {
7599
+ critical: colors_default.red("\u25CF"),
7600
+ high: colors_default.red("\u25CF"),
7601
+ moderate: colors_default.yellow("\u25CF"),
7602
+ low: colors_default.blue("\u25CF"),
7603
+ info: colors_default.gray("\u25CF")
7604
+ };
7605
+ function getVulnIcon(severity) {
7606
+ return vulnIcons[severity] ?? colors_default.gray("\u25CF");
7607
+ }
7608
+ function middleTruncate(str2, maxLen) {
7609
+ if (str2.length <= maxLen) return str2;
7610
+ const ellipsis = "\u2026";
7611
+ const charsToShow = maxLen - ellipsis.length;
7612
+ const frontChars = Math.ceil(charsToShow / 2);
7613
+ const backChars = Math.floor(charsToShow / 2);
7614
+ return str2.slice(0, frontChars) + ellipsis + str2.slice(-backChars);
7615
+ }
7616
+ function formatDependents(dependents) {
7617
+ if (dependents.length === 0) return "";
7618
+ if (dependents.length === 1) return dependents[0];
7619
+ return `${dependents[0]} +${dependents.length - 1}`;
7620
+ }
7621
+
7475
7622
  // src/utils/versions.ts
7476
7623
  function parseComparableVersion(version) {
7477
7624
  const trimmed = version.trim();
@@ -7535,6 +7682,10 @@ async function readUpstreamCliRange(repoPath, ref) {
7535
7682
  return null;
7536
7683
  }
7537
7684
  }
7685
+ function releaseAgeExcludeHint(range) {
7686
+ const version = range.match(/\d+\S*/)?.[0] ?? range;
7687
+ return ` pnpm delays fresh releases (minimumReleaseAge): if the install is refused, add '${CLI_PACKAGE}@${version}' to minimumReleaseAgeExclude in pnpm-workspace.yaml.`;
7688
+ }
7538
7689
  function cliVersionMismatch(running, range) {
7539
7690
  if (/^\s*(<|>(?!=))/.test(range)) return null;
7540
7691
  const required = parseComparableVersion(range);
@@ -7550,6 +7701,81 @@ function cliVersionMismatch(running, range) {
7550
7701
  function isReportableIgnored(filePath) {
7551
7702
  return !isManagedFile(filePath) && !isGeneratedFile(filePath);
7552
7703
  }
7704
+ var basenameOf = (path) => path.slice(path.lastIndexOf("/") + 1);
7705
+ var dirnameOf = (path) => path.includes("/") ? path.slice(0, path.lastIndexOf("/")) : "";
7706
+ function directoryMoves(changes) {
7707
+ const votes = /* @__PURE__ */ new Map();
7708
+ for (const [newPath, change] of changes) {
7709
+ if (change.status !== "R" || !change.oldPath || basenameOf(change.oldPath) !== basenameOf(newPath)) continue;
7710
+ const from = dirnameOf(change.oldPath);
7711
+ const counts = votes.get(from) ?? /* @__PURE__ */ new Map();
7712
+ counts.set(dirnameOf(newPath), (counts.get(dirnameOf(newPath)) ?? 0) + 1);
7713
+ votes.set(from, counts);
7714
+ }
7715
+ const moves = /* @__PURE__ */ new Map();
7716
+ for (const [from, counts] of votes) {
7717
+ const ranked = [...counts.entries()].sort((a, b) => b[1] - a[1]);
7718
+ if (ranked.length === 1 || ranked[0][1] > ranked[1][1]) moves.set(from, ranked[0][0]);
7719
+ }
7720
+ return moves;
7721
+ }
7722
+ var joinDir = (dir, name) => dir ? `${dir}/${name}` : name;
7723
+ function repairIncomingRenames(changes, context) {
7724
+ const dirMoves = directoryMoves(changes);
7725
+ const snapshot = new Map(changes);
7726
+ const crossed = [];
7727
+ const keptTargets = /* @__PURE__ */ new Set();
7728
+ const renameSources = /* @__PURE__ */ new Set();
7729
+ for (const [newPath, change] of changes) {
7730
+ if (change.status !== "R" || !change.oldPath) continue;
7731
+ renameSources.add(change.oldPath);
7732
+ if (basenameOf(change.oldPath) === basenameOf(newPath)) keptTargets.add(newPath);
7733
+ else crossed.push({ from: newPath, oldPath: change.oldPath, change });
7734
+ }
7735
+ const desired = /* @__PURE__ */ new Map();
7736
+ for (const pair of crossed) {
7737
+ const name = basenameOf(pair.oldPath);
7738
+ const mappedDir = dirMoves.get(dirnameOf(pair.oldPath));
7739
+ const candidates = [
7740
+ ...mappedDir === void 0 ? [] : [joinDir(mappedDir, name)],
7741
+ joinDir(dirnameOf(pair.from), name)
7742
+ ];
7743
+ const target = candidates.find(
7744
+ (path) => context.incomingPaths.has(path) && !context.basePaths.has(path) && !keptTargets.has(path)
7745
+ );
7746
+ desired.set(pair.from, target ?? pair.from);
7747
+ }
7748
+ const targets = [...desired.values()];
7749
+ if (new Set(targets).size === targets.length) {
7750
+ const targetSet = new Set(targets);
7751
+ for (const pair of crossed) {
7752
+ const target = desired.get(pair.from);
7753
+ if (target === pair.from) continue;
7754
+ changes.set(target, {
7755
+ status: "R",
7756
+ oldPath: pair.oldPath,
7757
+ baseHash: pair.change.baseHash,
7758
+ targetHash: snapshot.get(target)?.targetHash ?? null
7759
+ });
7760
+ }
7761
+ for (const pair of crossed) {
7762
+ if (desired.get(pair.from) === pair.from || targetSet.has(pair.from)) continue;
7763
+ changes.set(pair.from, { status: "A", baseHash: null, targetHash: snapshot.get(pair.from)?.targetHash ?? null });
7764
+ }
7765
+ }
7766
+ for (const oldPath of context.localPaths) {
7767
+ if (context.incomingPaths.has(oldPath) || !context.basePaths.has(oldPath)) continue;
7768
+ if (renameSources.has(oldPath) || !context.isProtected(oldPath)) continue;
7769
+ const mappedDir = dirMoves.get(dirnameOf(oldPath));
7770
+ if (mappedDir === void 0) continue;
7771
+ const target = joinDir(mappedDir, basenameOf(oldPath));
7772
+ if (!context.incomingPaths.has(target) || context.basePaths.has(target) || changes.get(target)?.status !== "A")
7773
+ continue;
7774
+ changes.set(target, { status: "R", oldPath, baseHash: null, targetHash: changes.get(target)?.targetHash ?? null });
7775
+ changes.delete(oldPath);
7776
+ renameSources.add(oldPath);
7777
+ }
7778
+ }
7553
7779
  async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predicates, onProgress) {
7554
7780
  onProgress?.("collecting file hashes (batch)...");
7555
7781
  const [forkHashes, upstreamHashes, baseHashes] = await Promise.all([
@@ -7559,6 +7785,12 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
7559
7785
  ]);
7560
7786
  const upstreamChanges = await getFileChanges(repoPath, mergeBaseRef, incomingRef);
7561
7787
  const forkChanges = await getFileChanges(repoPath, mergeBaseRef, localRef);
7788
+ repairIncomingRenames(upstreamChanges, {
7789
+ basePaths: new Set(baseHashes.keys()),
7790
+ incomingPaths: new Set(upstreamHashes.keys()),
7791
+ localPaths: new Set(forkHashes.keys()),
7792
+ isProtected: (path) => predicates.isIgnored(path) || predicates.isPinned(path)
7793
+ });
7562
7794
  const upstreamRenames = /* @__PURE__ */ new Map();
7563
7795
  for (const [newPath, change] of upstreamChanges) {
7564
7796
  if (change.status === "R" && change.oldPath) {
@@ -7615,8 +7847,9 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
7615
7847
  const forkModifiedOld = forkOldHash !== baseOldHash;
7616
7848
  const oldPathPinned = predicates.isPinned(oldPath);
7617
7849
  let upstreamChanged2;
7618
- if (fileIsPinned || oldPathPinned || predicates.isIgnored(oldPath)) {
7850
+ if (fileIsPinned || fileIsIgnored || oldPathPinned || predicates.isIgnored(oldPath)) {
7619
7851
  status = "pinned";
7852
+ if (oldPathInFork) renamedFrom = oldPath;
7620
7853
  if (forkModifiedOld && upstreamHash !== baseOldHash) {
7621
7854
  upstreamChanged2 = true;
7622
7855
  renamedProtected.add(filePath);
@@ -7642,7 +7875,7 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
7642
7875
  status = "ignored";
7643
7876
  } else if (!inFork && inUpstream) {
7644
7877
  const locallyDeleted = baseHash !== null && upstreamHash === baseHash;
7645
- status = fileIsPinned || locallyDeleted ? "deleted" : "behind";
7878
+ status = fileIsPinned && baseHash !== null || locallyDeleted ? "deleted" : "behind";
7646
7879
  } else if (inFork && !inUpstream) {
7647
7880
  if (baseHash !== null) {
7648
7881
  status = fileIsPinned ? "ahead" : "behind";
@@ -7754,6 +7987,9 @@ async function readOriginGitHubUrl(forkPath) {
7754
7987
  function isProtectedFile(file) {
7755
7988
  return file.isIgnored || file.isPinned;
7756
7989
  }
7990
+ function isPinnedFirstArrival(file) {
7991
+ return file.isPinned && !file.isIgnored && !file.existsInFork && file.existsInUpstream && file.status === "behind";
7992
+ }
7757
7993
  function syncPredicates(config) {
7758
7994
  return {
7759
7995
  isIgnored: (path) => isIgnored(path, config),
@@ -7772,9 +8008,11 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
7772
8008
  );
7773
8009
  const batchRestorePaths = [];
7774
8010
  const batchRemovePaths = [];
8011
+ const movedProtected = [];
7775
8012
  for (const file of analyzedFiles) {
7776
- if (file.status === "identical" || file.status === "ahead") continue;
7777
- if (file.isIgnored || file.isPinned && !file.renamedFrom) {
8013
+ if (file.status === "identical" || file.status === "ahead" || isPinnedFirstArrival(file)) continue;
8014
+ if (file.status === "pinned" && file.renamedFrom) movedProtected.push({ from: file.renamedFrom, to: file.path });
8015
+ else if (file.isIgnored || file.isPinned) {
7778
8016
  (file.existsInFork ? batchRestorePaths : batchRemovePaths).push(file.path);
7779
8017
  }
7780
8018
  }
@@ -7791,16 +8029,23 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
7791
8029
  await removeFileFromWorktree(forkPath, filePath);
7792
8030
  }
7793
8031
  }
8032
+ for (const { from, to } of movedProtected) {
8033
+ onProgress?.(`\u2192 ${from} \u2192 ${to}: keeping your copy (moved in upstream)`);
8034
+ await moveFromRef(forkPath, "HEAD", from, to);
8035
+ }
7794
8036
  for (const file of analyzedFiles) {
7795
8037
  const { path: filePath, status, existsInFork } = file;
7796
- if (status === "identical" || status === "ahead" || isProtectedFile(file)) continue;
8038
+ if (status === "identical" || status === "ahead" || isProtectedFile(file) && !isPinnedFirstArrival(file))
8039
+ continue;
7797
8040
  if (status === "diverged") {
7798
8041
  onProgress?.(`\u2192 ${filePath}: using git merge result (diverged)`);
7799
8042
  continue;
7800
8043
  }
7801
8044
  if (status === "behind") {
7802
8045
  if (!existsInFork) {
7803
- onProgress?.(`\u2192 ${filePath}: adding from upstream (new file)`);
8046
+ onProgress?.(
8047
+ `\u2192 ${filePath}: adding from upstream (${isPinnedFirstArrival(file) ? "pinned, upstream's copy is your first" : "new file"})`
8048
+ );
7804
8049
  await checkoutFromRef(forkPath, upstreamRef, [filePath]);
7805
8050
  } else if (!file.existsInUpstream) {
7806
8051
  onProgress?.(`\u2192 ${filePath}: removing (deleted in upstream)`);
@@ -7939,7 +8184,11 @@ async function prepareUpstream(config, onProgress, onStep) {
7939
8184
  config.upstreamRef = upstreamRef;
7940
8185
  const cliRange = await readUpstreamCliRange(forkPath, upstreamRef);
7941
8186
  const cliMismatch = cliRange ? cliVersionMismatch(VERSION, cliRange) : null;
7942
- if (cliMismatch) throw new Error(cliMismatch);
8187
+ if (cliMismatch && cliRange) {
8188
+ const delayed = await readReleaseAgeMinutes(forkPath) > 0;
8189
+ throw new Error(delayed ? `${cliMismatch}
8190
+ ${releaseAgeExcludeHint(cliRange)}` : cliMismatch);
8191
+ }
7943
8192
  onProgress?.("checking sync base...");
7944
8193
  await ensureSyncBase(forkPath, "HEAD", upstreamRef);
7945
8194
  const upstreamGitHubUrl = getGitHubBaseUrl(config.settings.upstreamUrl) ?? void 0;
@@ -7982,7 +8231,7 @@ async function assertSyncConfigFollowed(config, ctx) {
7982
8231
  const report = await compareUpstreamOverrides(config.forkPath, ctx.mergeBase, ctx.upstreamRef, config).catch(
7983
8232
  () => void 0
7984
8233
  );
7985
- if (report?.kind === "changes") throw new UpstreamConfigChangedError(report);
8234
+ if (report && hasBlockingOverrideChanges(report)) throw new UpstreamConfigChangedError(report);
7986
8235
  }
7987
8236
  function resultMeta(config, ctx) {
7988
8237
  return {
@@ -9045,109 +9294,6 @@ import { readFileSync as readFileSync19 } from "fs";
9045
9294
  import { basename as basename9, join as join35, relative as relative4 } from "path";
9046
9295
  import { checkbox, Separator } from "@inquirer/prompts";
9047
9296
 
9048
- // src/utils/audit-utils.ts
9049
- import { execFile as execFile2 } from "child_process";
9050
- import { promisify as promisify2 } from "util";
9051
- var execFileAsync2 = promisify2(execFile2);
9052
- function outdatedPackageName(key) {
9053
- const name = key.split(" ")[0];
9054
- const at = name.indexOf("@", 1);
9055
- return at === -1 ? name : name.slice(0, at);
9056
- }
9057
- function getRepoUrl(repositoryUrl) {
9058
- if (!repositoryUrl) return null;
9059
- let url = repositoryUrl;
9060
- if (url.startsWith("git+")) {
9061
- url = url.slice(4);
9062
- }
9063
- if (url.endsWith(".git")) {
9064
- url = url.slice(0, -4);
9065
- }
9066
- if (url.startsWith("git://")) {
9067
- url = url.replace("git://", "https://");
9068
- }
9069
- return url;
9070
- }
9071
- function isGitHubRepoUrl(repoUrl) {
9072
- if (!repoUrl) return false;
9073
- try {
9074
- const parsed2 = new URL(repoUrl);
9075
- return parsed2.hostname === "github.com" || parsed2.hostname === "www.github.com";
9076
- } catch {
9077
- return false;
9078
- }
9079
- }
9080
- function getReleasesUrl(repoUrl) {
9081
- if (!isGitHubRepoUrl(repoUrl)) return null;
9082
- return `${repoUrl}/releases`;
9083
- }
9084
- async function runPnpm(args, cwd) {
9085
- try {
9086
- const { stdout, stderr } = await execFileAsync2("pnpm", args, {
9087
- encoding: "utf8",
9088
- maxBuffer: 10 * 1024 * 1024,
9089
- cwd
9090
- });
9091
- return { ok: true, stdout, stderr };
9092
- } catch (error) {
9093
- const { stdout = "", stderr = "" } = error;
9094
- return { ok: false, stdout, stderr: stdout || stderr ? stderr : String(error) };
9095
- }
9096
- }
9097
- async function runPnpmJson(args, cwd) {
9098
- try {
9099
- return JSON.parse((await runPnpm([...args, "--loglevel=error"], cwd)).stdout);
9100
- } catch {
9101
- return null;
9102
- }
9103
- }
9104
- async function runPnpmAudit(cwd) {
9105
- const result = await runPnpmJson(["audit", "--json"], cwd);
9106
- if (!result || typeof result.advisories !== "object" || result.advisories === null) return null;
9107
- return result;
9108
- }
9109
- async function readReleaseAgeMinutes(cwd) {
9110
- const minutes = Number.parseInt((await runPnpm(["config", "get", "minimumReleaseAge"], cwd)).stdout, 10);
9111
- return Number.isNaN(minutes) ? 0 : minutes;
9112
- }
9113
- async function fetchPublishTimes(name, cwd) {
9114
- const times = await runPnpmJson(["view", name, "time", "--json"], cwd);
9115
- return times && typeof times === "object" && !("error" in times) ? times : null;
9116
- }
9117
- function editJsonText(content, change) {
9118
- const json = JSON.parse(content);
9119
- change(json);
9120
- const indent = content.match(/^[ \t]+(?=")/m)?.[0] ?? " ";
9121
- return `${JSON.stringify(json, null, indent)}${content.endsWith("\n") ? "\n" : ""}`;
9122
- }
9123
- function rewriteSpecifier(spec, latest) {
9124
- const match = spec.match(/^([\^~]?)\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?$/);
9125
- return match ? `${match[1]}${latest}` : null;
9126
- }
9127
- var vulnIcons = {
9128
- critical: colors_default.red("\u25CF"),
9129
- high: colors_default.red("\u25CF"),
9130
- moderate: colors_default.yellow("\u25CF"),
9131
- low: colors_default.blue("\u25CF"),
9132
- info: colors_default.gray("\u25CF")
9133
- };
9134
- function getVulnIcon(severity) {
9135
- return vulnIcons[severity] ?? colors_default.gray("\u25CF");
9136
- }
9137
- function middleTruncate(str2, maxLen) {
9138
- if (str2.length <= maxLen) return str2;
9139
- const ellipsis = "\u2026";
9140
- const charsToShow = maxLen - ellipsis.length;
9141
- const frontChars = Math.ceil(charsToShow / 2);
9142
- const backChars = Math.floor(charsToShow / 2);
9143
- return str2.slice(0, frontChars) + ellipsis + str2.slice(-backChars);
9144
- }
9145
- function formatDependents(dependents) {
9146
- if (dependents.length === 0) return "";
9147
- if (dependents.length === 1) return dependents[0];
9148
- return `${dependents[0]} +${dependents.length - 1}`;
9149
- }
9150
-
9151
9297
  // src/utils/local-tools.ts
9152
9298
  import { execFile as execFile3 } from "child_process";
9153
9299
  import { existsSync as existsSync24, realpathSync as realpathSync2 } from "fs";
@@ -10897,9 +11043,18 @@ function readSearchable(filePath) {
10897
11043
  return "";
10898
11044
  }
10899
11045
  }
11046
+ var codeFilePattern = /\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;
11047
+ var proseFilePattern = /\.(?:md|mdx|markdown)$/i;
11048
+ function stripComments(text2) {
11049
+ return text2.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
11050
+ }
10900
11051
  function fileSource(forkPath, path) {
10901
11052
  let text2;
10902
- return { path, label: path, text: () => text2 ??= readSearchable(join37(forkPath, path)) };
11053
+ const read = () => {
11054
+ const content = readSearchable(join37(forkPath, path));
11055
+ return codeFilePattern.test(path) ? stripComments(content) : content;
11056
+ };
11057
+ return { path, label: path, text: () => text2 ??= read() };
10903
11058
  }
10904
11059
  async function readScriptsAtRef(forkPath, ref, path) {
10905
11060
  try {
@@ -10952,7 +11107,7 @@ async function loadForkUsage(forkPath, upstreamRef, baseRef) {
10952
11107
  const sources = [];
10953
11108
  for (const path of diff.split("\0").filter(Boolean)) {
10954
11109
  if (isPackageJson(path)) sources.push(...await scriptSources(forkPath, upstreamRef, baseRef, path));
10955
- else if (!isManagedFile(path)) sources.push(fileSource(forkPath, path));
11110
+ else if (!isManagedFile(path) && !proseFilePattern.test(path)) sources.push(fileSource(forkPath, path));
10956
11111
  }
10957
11112
  const findIn = (scoped, patterns) => scoped.find((source) => patterns.some((pattern) => pattern.test(source.text())))?.label;
10958
11113
  return {