@cellajs/cli 0.6.4 → 0.6.6

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.4",
237
+ version: "0.6.6",
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
  }
@@ -1373,12 +1383,20 @@ var STARTUP_TIMEOUT_MS = 5 * 6e4;
1373
1383
  var STOP_TIMEOUT_MS = 1e4;
1374
1384
  var DATABASE = "testbed";
1375
1385
  var DATABASE_SERVICE = "db_testbed";
1386
+ var showcaseFile = (forkPath) => movedPath(forkPath, "backend/scripts/seeds/showcase.json", "json/showcase.json");
1387
+ var showcasePath = (forkPath) => join4(forkPath, showcaseFile(forkPath));
1388
+ var hasShowcase = (forkPath) => existsSync4(showcasePath(forkPath));
1389
+ function showcaseAdmin(forkPath) {
1390
+ const { admin } = JSON.parse(readFileSync2(showcasePath(forkPath), "utf8"));
1391
+ if (typeof admin?.email !== "string") throw new Error(`${showcaseFile(forkPath)} names no admin email`);
1392
+ return admin.email;
1393
+ }
1376
1394
  var shiftPort = (url) => {
1377
1395
  const next = new URL(url);
1378
1396
  next.port = String(Number(next.port) + PORT_SHIFT);
1379
1397
  return next.toString().replace(/\/$/, "");
1380
1398
  };
1381
- async function resolveTestbed(forkPath) {
1399
+ async function resolveTestbed(forkPath, data = "seed") {
1382
1400
  const [facts, databases] = await Promise.all([loadAppFacts(forkPath), loadLocalDatabases(forkPath)]);
1383
1401
  const databasePort = databases.port(DATABASE);
1384
1402
  if (!Number.isInteger(databasePort)) {
@@ -1398,6 +1416,7 @@ async function resolveTestbed(forkPath) {
1398
1416
  // The seed creates the admin that backend/.env names, and a command signs in as that user
1399
1417
  ...fileEnv.ADMIN_EMAIL ? { ADMIN_EMAIL: fileEnv.ADMIN_EMAIL } : {},
1400
1418
  ...getEnvSnapshot(),
1419
+ ...data === "showcase" ? { ADMIN_EMAIL: showcaseAdmin(forkPath) } : {},
1401
1420
  ...composeEnv,
1402
1421
  NODE_ENV: "development",
1403
1422
  DATABASE_URL: databases.url(DATABASE, "runtime_role"),
@@ -1411,12 +1430,22 @@ async function resolveTestbed(forkPath) {
1411
1430
  ({ port, internalPort }) => (internalPort === void 0 ? [port] : [port, internalPort]).map((value) => value + PORT_SHIFT)
1412
1431
  ),
1413
1432
  container: `${composeEnv.PROJECT_SLUG}_${DATABASE_SERVICE}`,
1433
+ data,
1414
1434
  yjs: facts.services.yjs?.enabled ?? false,
1415
1435
  stateDir: toolDir(forkPath, "testbed", "repository")
1416
1436
  };
1417
1437
  }
1418
1438
  var lockPath = (testbed) => join4(testbed.stateDir, "lock.json");
1419
1439
  var logPath = (testbed) => join4(testbed.stateDir, "testbed.log");
1440
+ var dataPath = (testbed) => join4(testbed.stateDir, "data.json");
1441
+ function heldData(testbed) {
1442
+ try {
1443
+ return JSON.parse(readFileSync2(dataPath(testbed), "utf8")).data === "showcase" ? "showcase" : "seed";
1444
+ } catch {
1445
+ return "seed";
1446
+ }
1447
+ }
1448
+ var describeData = (data) => data === "showcase" ? "the showcase" : "the seed data";
1420
1449
  function isAlive(pid) {
1421
1450
  try {
1422
1451
  process8.kill(pid, 0);
@@ -1498,7 +1527,12 @@ async function startTestbed(testbed, fresh, drop = false) {
1498
1527
  mkdirSync(testbed.stateDir, { recursive: true });
1499
1528
  writeFileSync(
1500
1529
  lockPath(testbed),
1501
- JSON.stringify({ pid: process8.pid, forkPath: testbed.forkPath, frontendUrl: testbed.frontendUrl })
1530
+ JSON.stringify({
1531
+ pid: process8.pid,
1532
+ forkPath: testbed.forkPath,
1533
+ frontendUrl: testbed.frontendUrl,
1534
+ data: testbed.data
1535
+ })
1502
1536
  );
1503
1537
  const log = openSync(logPath(testbed), "w");
1504
1538
  let stack;
@@ -1525,14 +1559,15 @@ async function startTestbed(testbed, fresh, drop = false) {
1525
1559
  };
1526
1560
  const forget = onShutdown(stop);
1527
1561
  try {
1528
- if (fresh) removeDatabase(testbed);
1562
+ if (fresh || testbed.data === "showcase" || heldData(testbed) !== testbed.data) removeDatabase(testbed);
1529
1563
  const spinner = createSpinner(`starting the database on port ${testbed.databasePort}`);
1530
1564
  if (!compose(testbed, ["up", "-d", "--wait", DATABASE_SERVICE])) {
1531
1565
  throw new Error("the testbed database did not start: is Docker running?");
1532
1566
  }
1533
1567
  const seeded = isSeeded(testbed);
1534
- spinner.text = seeded ? "migrating the database" : "migrating and seeding the database (first start only)";
1535
- const prepare = seeded ? ["--filter", "backend", "migrate"] : ["seed"];
1568
+ spinner.text = seeded ? "migrating the database" : `migrating the database and seeding ${describeData(testbed.data)}`;
1569
+ const seed = testbed.data === "showcase" ? ["seed", "showcase"] : ["seed"];
1570
+ const prepare = seeded ? ["--filter", "backend", "migrate"] : seed;
1536
1571
  const prepared = spawnSync3("pnpm", prepare, {
1537
1572
  cwd: testbed.forkPath,
1538
1573
  env: testbed.env,
@@ -1540,7 +1575,11 @@ async function startTestbed(testbed, fresh, drop = false) {
1540
1575
  });
1541
1576
  if (prepared.status !== 0) throw new Error(`\`pnpm ${prepare.join(" ")}\` failed:
1542
1577
  ${logTail(testbed)}`);
1543
- printStep("database", `${seeded ? "migrated" : "seeded"}, port ${testbed.databasePort}`);
1578
+ writeFileSync(dataPath(testbed), JSON.stringify({ data: testbed.data }));
1579
+ printStep(
1580
+ "database",
1581
+ `${seeded ? "migrated" : `seeded with ${describeData(testbed.data)}`}, port ${testbed.databasePort}`
1582
+ );
1544
1583
  const relay = testbed.yjs ? "pnpm --filter yjs-worker dev & " : "";
1545
1584
  stack = spawn("sh", ["-c", `${relay}pnpm offline`], {
1546
1585
  cwd: testbed.forkPath,
@@ -1608,14 +1647,19 @@ function ownLock(testbed) {
1608
1647
  }
1609
1648
  return lock;
1610
1649
  }
1611
- async function withTestbed(forkPath, use, { fresh = false, destructive = false } = {}) {
1612
- const testbed = await resolveTestbed(forkPath);
1650
+ async function withTestbed(forkPath, use, { fresh = false, destructive = false, data = "seed" } = {}) {
1651
+ const testbed = await resolveTestbed(forkPath, data);
1613
1652
  const lock = ownLock(testbed);
1614
1653
  if (lock && destructive) {
1615
1654
  throw new Error(
1616
1655
  `a testbed runs at ${lock.frontendUrl}: this command needs one of its own, so stop that one first (\`pnpm cella testbed stop\`)`
1617
1656
  );
1618
1657
  }
1658
+ if (lock && (lock.data ?? "seed") !== data) {
1659
+ throw new Error(
1660
+ `a testbed with ${describeData(lock.data ?? "seed")} runs at ${lock.frontendUrl}: this command needs ${describeData(data)}, so stop that one first (\`pnpm cella testbed stop\`)`
1661
+ );
1662
+ }
1619
1663
  if (lock) {
1620
1664
  await untilAnswering(
1621
1665
  testbed,
@@ -1633,10 +1677,12 @@ async function withTestbed(forkPath, use, { fresh = false, destructive = false }
1633
1677
  }
1634
1678
  async function runTestbed(forkPath, options) {
1635
1679
  if (options.job === "stop") return stopTestbed(await resolveTestbed(forkPath));
1680
+ if (options.showcase && !hasShowcase(forkPath)) throw new Error(`this app keeps no ${showcaseFile(forkPath)}`);
1681
+ const data = options.showcase ? "showcase" : "seed";
1636
1682
  if (options.command.length > 0) {
1637
- return withTestbed(forkPath, (testbed2) => runCommand(forkPath, testbed2.env, options.command), options);
1683
+ return withTestbed(forkPath, (testbed2) => runCommand(forkPath, testbed2.env, options.command), { ...options, data });
1638
1684
  }
1639
- const testbed = await resolveTestbed(forkPath);
1685
+ const testbed = await resolveTestbed(forkPath, data);
1640
1686
  const lock = ownLock(testbed);
1641
1687
  if (lock) {
1642
1688
  throw new Error(
@@ -1664,7 +1710,10 @@ var testbedCommand = {
1664
1710
  ["start", "start it and hold it until ctrl-c"],
1665
1711
  ["stop", "stop the testbed that runs, or one left behind"]
1666
1712
  ],
1667
- options: [["--fresh", "start from an empty database: migrate and seed it again"]],
1713
+ options: [
1714
+ ["--fresh", "start from an empty database: migrate and seed it again"],
1715
+ ["--showcase", "hold the showcase seed, which the screenshots shoot, for a review loop"]
1716
+ ],
1668
1717
  passthrough: [
1669
1718
  "command",
1670
1719
  "after `--`: run it against the testbed (this checkout's running one, else one started for it and stopped after)"
@@ -1672,14 +1721,14 @@ var testbedCommand = {
1672
1721
  parse: ({ job = "start", opts, passthrough }) => {
1673
1722
  if (job !== "start" && job !== "stop") throw new Error(`unknown job '${job}'. expected one of: start, stop`);
1674
1723
  if (job === "stop" && passthrough.length > 0) throw new Error("stop takes no command");
1675
- return { job, fresh: opts.fresh === true, command: passthrough };
1724
+ return { job, fresh: opts.fresh === true, showcase: opts.showcase === true, command: passthrough };
1676
1725
  },
1677
1726
  run: runTestbed
1678
1727
  };
1679
1728
 
1680
1729
  // src/tools/a11y/audit.ts
1681
- import { mkdirSync as mkdirSync3, statSync, writeFileSync as writeFileSync4 } from "fs";
1682
- import { join as join9 } from "path";
1730
+ import { mkdirSync as mkdirSync3, statSync, writeFileSync as writeFileSync5 } from "fs";
1731
+ import { join as join10 } from "path";
1683
1732
 
1684
1733
  // src/tools/a11y/axe-scan.ts
1685
1734
  import axe from "axe-core";
@@ -2349,8 +2398,97 @@ function recordDecisions(forkPath, decisions) {
2349
2398
  }
2350
2399
 
2351
2400
  // src/tools/a11y/probes/page-structure.ts
2352
- import { readFileSync as readFileSync4 } from "fs";
2401
+ import { readFileSync as readFileSync5 } from "fs";
2402
+ import { join as join8 } from "path";
2403
+
2404
+ // src/tools/i18n/locales.ts
2405
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
2353
2406
  import { join as join7 } from "path";
2407
+ var localesDir = (forkPath) => movedPath(forkPath, "shared/locales", "locales");
2408
+ var MANIFEST = "namespaces.json";
2409
+ var SOURCE_LANGUAGE = "en";
2410
+ var entryPattern = /^(\s+)("(?:[^"\\]|\\.)*")\s*:\s*("(?:[^"\\]|\\.)*"),?\s*$/;
2411
+ function parseLocaleFile(path, text2) {
2412
+ if (text2.trim() === "{}") return { path, indent: " ", entries: [] };
2413
+ const lines = text2.replace(/\n$/, "").split("\n");
2414
+ if (lines[0] !== "{" || lines.at(-1) !== "}") return "not one JSON object with one entry per line";
2415
+ const entries = [];
2416
+ let indent = " ";
2417
+ for (const line of lines.slice(1, -1)) {
2418
+ const match = entryPattern.exec(line);
2419
+ if (!match) return `cannot read the line ${JSON.stringify(line.trim())}`;
2420
+ indent = match[1];
2421
+ entries.push({ key: JSON.parse(match[2]), value: JSON.parse(match[3]), raw: [match[2], match[3]] });
2422
+ }
2423
+ return { path, indent, entries };
2424
+ }
2425
+ function serializeLocaleFile(file) {
2426
+ if (file.entries.length === 0) return "{}\n";
2427
+ return `{
2428
+ ${file.entries.map(({ raw }) => `${file.indent}${raw[0]}: ${raw[1]}`).join(",\n")}
2429
+ }
2430
+ `;
2431
+ }
2432
+ function writeLocaleFile(forkPath, file) {
2433
+ writeFileSync3(join7(forkPath, file.path), serializeLocaleFile(file));
2434
+ }
2435
+ var textsOf = (file) => Object.fromEntries((file?.entries ?? []).map(({ key, value }) => [key, value]));
2436
+ var readJsonObject = (path) => {
2437
+ const parsed2 = JSON.parse(readFileSync4(path, "utf8"));
2438
+ if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) throw new Error(`${path} is not a JSON object`);
2439
+ return Object.fromEntries(Object.entries(parsed2));
2440
+ };
2441
+ function readLocales(forkPath) {
2442
+ const localesPath = localesDir(forkPath);
2443
+ const dir = join7(forkPath, localesPath);
2444
+ if (!existsSync7(join7(dir, SOURCE_LANGUAGE))) throw new Error(`no ${localesPath}/${SOURCE_LANGUAGE}/ in ${forkPath}`);
2445
+ 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));
2446
+ const files2 = /* @__PURE__ */ new Map();
2447
+ for (const language2 of languages) {
2448
+ for (const name of readdirSync(join7(dir, language2)).filter((name2) => name2.endsWith(".json"))) {
2449
+ const path = `${localesPath}/${language2}/${name}`;
2450
+ const parsed2 = parseLocaleFile(path, readFileSync4(join7(forkPath, path), "utf8"));
2451
+ if (typeof parsed2 === "string") throw new Error(`${path}: ${parsed2}; pnpm lint:fix writes one entry per line`);
2452
+ files2.set(`${language2}/${name.slice(0, -".json".length)}`, parsed2);
2453
+ }
2454
+ }
2455
+ const manifestPath = join7(dir, MANIFEST);
2456
+ if (!existsSync7(manifestPath)) throw new Error(`no ${localesPath}/${MANIFEST}: upgrade the app from cella first`);
2457
+ const merged = readJsonObject(manifestPath);
2458
+ const namespaces = /* @__PURE__ */ new Map();
2459
+ for (const [namespace, sources] of Object.entries(merged)) {
2460
+ if (!Array.isArray(sources) || !sources.every((source) => typeof source === "string")) {
2461
+ throw new Error(`${localesPath}/${MANIFEST}: ${namespace} must list file names`);
2462
+ }
2463
+ namespaces.set(namespace, sources);
2464
+ }
2465
+ const mergedFiles = new Set([...namespaces.values()].flat());
2466
+ const ownFiles = [...new Set([...files2.keys()].map((key) => key.split("/")[1]))].filter(
2467
+ (file) => !mergedFiles.has(file)
2468
+ );
2469
+ for (const file of ownFiles.sort()) namespaces.set(file, [file]);
2470
+ return { dir: localesPath, languages, namespaces, files: files2 };
2471
+ }
2472
+ function namespaceTexts(locales, language2, namespace) {
2473
+ const sources = locales.namespaces.get(namespace) ?? [];
2474
+ return Object.assign({}, ...sources.map((file) => textsOf(locales.files.get(`${language2}/${file}`))));
2475
+ }
2476
+ var lineOf = (file, key) => file.entries.findIndex((entry) => entry.key === key) + 2;
2477
+ var pluralSuffix = /_(ordinal_)?(zero|one|two|few|many|other)$/;
2478
+ function pluralForm(key) {
2479
+ const match = pluralSuffix.exec(key);
2480
+ if (!match) return void 0;
2481
+ return { base: key.slice(0, match.index), category: match[2], ordinal: !!match[1] };
2482
+ }
2483
+ var withPluralForms = (key) => [
2484
+ key,
2485
+ ...["zero", "one", "two", "few", "many", "other"].flatMap((category) => [
2486
+ `${key}_${category}`,
2487
+ `${key}_ordinal_${category}`
2488
+ ])
2489
+ ];
2490
+
2491
+ // src/tools/a11y/probes/page-structure.ts
2354
2492
  var titles = { criteria: ["2.4.2"], check: "probe:titles", what: "A distinct, descriptive page title" };
2355
2493
  var language = {
2356
2494
  criteria: ["3.1.1"],
@@ -2384,7 +2522,7 @@ var pageStructure = async (page, state, { app }) => {
2384
2522
  var languageSwitch = async (page, state, { app, forkPath }) => {
2385
2523
  if (state.open) return [];
2386
2524
  const names = JSON.parse(
2387
- readFileSync4(join7(forkPath, "locales", app.defaultLanguage, "common.json"), "utf8")
2525
+ readFileSync5(join8(forkPath, localesDir(forkPath), app.defaultLanguage, "common.json"), "utf8")
2388
2526
  );
2389
2527
  const findings2 = [];
2390
2528
  for (const other of app.languages.filter((lng) => lng !== app.defaultLanguage)) {
@@ -2718,15 +2856,15 @@ var inputs = async (page, state) => {
2718
2856
  };
2719
2857
 
2720
2858
  // src/tools/a11y/review-packet.ts
2721
- import { mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
2722
- import { join as join8 } from "path";
2723
- var packetDir = (run, stateId) => join8(run.resultsDir, "review", stateId);
2859
+ import { mkdirSync as mkdirSync2, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
2860
+ import { join as join9 } from "path";
2861
+ var packetDir = (run, stateId) => join9(run.resultsDir, "review", stateId);
2724
2862
  function writePacketFile(run, stateId, name, content) {
2725
2863
  mkdirSync2(packetDir(run, stateId), { recursive: true });
2726
- writeFileSync3(join8(packetDir(run, stateId), name), content);
2864
+ writeFileSync4(join9(packetDir(run, stateId), name), content);
2727
2865
  }
2728
2866
  function removePacketFile(run, stateId, name) {
2729
- rmSync2(join8(packetDir(run, stateId), name), { force: true });
2867
+ rmSync2(join9(packetDir(run, stateId), name), { force: true });
2730
2868
  }
2731
2869
  function pageFacts() {
2732
2870
  const text2 = (el) => (el?.textContent ?? "").trim().replace(/\s+/g, " ");
@@ -3187,7 +3325,7 @@ async function runAudit(run, scope, testbed, options) {
3187
3325
  const startedAt = Date.now();
3188
3326
  const email = options.email ?? testbed.env.ADMIN_EMAIL;
3189
3327
  if (!email) throw new Error("no audit user: pass --email, or set ADMIN_EMAIL in backend/.env");
3190
- const builtAt = statSync(join9(run.forkPath, "frontend/dist/index.html"), { throwIfNoEntry: false })?.mtime;
3328
+ const builtAt = statSync(join10(run.forkPath, "frontend/dist/index.html"), { throwIfNoEntry: false })?.mtime;
3191
3329
  printStep("auditing", `${testbed.frontendUrl}, frontend build of ${builtAt?.toLocaleString() ?? "unknown date"}`);
3192
3330
  const states = options.states.length > 0 ? scope.states.filter((state) => options.states.includes(state.id)) : scope.states;
3193
3331
  const session = await startAudit(run.forkPath, scope, { email, env: testbed.env });
@@ -3227,12 +3365,12 @@ async function runAudit(run, scope, testbed, options) {
3227
3365
  mkdirSync3(run.resultsDir, { recursive: true });
3228
3366
  const axe2 = summarizeAxe(scans);
3229
3367
  for (const [id, items] of axe2.evidence) for (const item of items) evidence.add([id], item);
3230
- writeFileSync4(join9(run.resultsDir, "axe.json"), `${JSON.stringify({ ...axe2.report, errors }, null, 2)}
3368
+ writeFileSync5(join10(run.resultsDir, "axe.json"), `${JSON.stringify({ ...axe2.report, errors }, null, 2)}
3231
3369
  `);
3232
3370
  evidence.addFindings(acrossStates(findings2));
3233
3371
  runCodeChecks(evidence, run, scope.sources);
3234
3372
  const skipped2 = Object.keys(errors).length;
3235
- const target = options.states.length > 0 || skipped2 ? join9(run.resultsDir, "ledger-partial.json") : join9(run.forkPath, ledgerFile);
3373
+ const target = options.states.length > 0 || skipped2 ? join10(run.resultsDir, "ledger-partial.json") : join10(run.forkPath, ledgerFile);
3236
3374
  const ledger = writeLedger(
3237
3375
  run.forkPath,
3238
3376
  evidence.byCriterion,
@@ -3256,10 +3394,10 @@ async function runAudit(run, scope, testbed, options) {
3256
3394
  }
3257
3395
 
3258
3396
  // src/tools/a11y/decide.ts
3259
- import { readFileSync as readFileSync5 } from "fs";
3397
+ import { readFileSync as readFileSync6 } from "fs";
3260
3398
  import { resolve as resolve4 } from "path";
3261
3399
  function runDecide(forkPath, { file, decision }) {
3262
- const input2 = file ? JSON.parse(readFileSync5(resolve4(file), "utf8")) : [decision];
3400
+ const input2 = file ? JSON.parse(readFileSync6(resolve4(file), "utf8")) : [decision];
3263
3401
  if (!Array.isArray(input2)) throw new Error(`${file} holds no list of decisions`);
3264
3402
  const decisions = input2.map(parseDecision);
3265
3403
  const ledger = recordDecisions(forkPath, decisions);
@@ -3273,8 +3411,8 @@ function runDecide(forkPath, { file, decision }) {
3273
3411
 
3274
3412
  // src/tools/a11y/report.ts
3275
3413
  import { spawnSync as spawnSync5 } from "child_process";
3276
- import { copyFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
3277
- import { dirname as dirname4, join as join10 } from "path";
3414
+ import { copyFileSync, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "fs";
3415
+ import { dirname as dirname4, join as join11 } from "path";
3278
3416
  var statusLabel = {
3279
3417
  supports: "Supports",
3280
3418
  "partially-supports": "Partially Supports",
@@ -3393,7 +3531,7 @@ Not evaluated.
3393
3531
 
3394
3532
  This report describes the product as tested on the report date. Accessibility can change as the product changes.
3395
3533
  `;
3396
- const tracked = spawnSync5("git", ["ls-files", "frontend/src", "backend/src", "locales"], {
3534
+ const tracked = spawnSync5("git", ["ls-files", "frontend/src", "backend/src", "shared/locales", "locales"], {
3397
3535
  cwd: forkPath,
3398
3536
  encoding: "utf8"
3399
3537
  }).stdout.split("\n");
@@ -3420,7 +3558,7 @@ NVDA and Firefox on Windows, keyboard only, in light and dark mode.
3420
3558
 
3421
3559
  ## Confirm: decided by an agent (${byAgent.length})
3422
3560
 
3423
- The agent judged these from the review packets in \`${join10(resultsDir, "review")}/\`. Check its evidence, then record your own
3561
+ The agent judged these from the review packets in \`${join11(resultsDir, "review")}/\`. Check its evidence, then record your own
3424
3562
  decision; a wrong Supports here ends up in the published report.
3425
3563
 
3426
3564
  ${byAgent.map((r) => {
@@ -3464,11 +3602,11 @@ ${failing.map((r) => {
3464
3602
  }).join("\n")}
3465
3603
  `;
3466
3604
  mkdirSync4(resultsDir, { recursive: true });
3467
- writeFileSync5(join10(resultsDir, "vpat-draft.md"), vpat);
3468
- writeFileSync5(join10(resultsDir, "manual-pass.md"), checklist);
3605
+ writeFileSync6(join11(resultsDir, "vpat-draft.md"), vpat);
3606
+ writeFileSync6(join11(resultsDir, "manual-pass.md"), checklist);
3469
3607
  const html = toHtml(vpat, `${app.name} Accessibility Conformance Report`);
3470
- const pdfPath = join10(resultsDir, "conformance-report.pdf");
3471
- writeFileSync5(join10(resultsDir, "conformance-report.html"), html);
3608
+ const pdfPath = join11(resultsDir, "conformance-report.pdf");
3609
+ writeFileSync6(join11(resultsDir, "conformance-report.html"), html);
3472
3610
  const { chromium } = await loadPlaywright(forkPath);
3473
3611
  const browser = await chromium.launch();
3474
3612
  try {
@@ -3488,12 +3626,12 @@ ${failing.map((r) => {
3488
3626
  if (unconfirmed && !draft) {
3489
3627
  const ids = [...open, ...byAgent].map((r) => r.id).join(", ");
3490
3628
  throw new Error(
3491
- `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`
3629
+ `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`
3492
3630
  );
3493
3631
  }
3494
3632
  const publicPath = "frontend/public/static/common/accessibility-conformance-report.pdf";
3495
- mkdirSync4(dirname4(join10(forkPath, publicPath)), { recursive: true });
3496
- copyFileSync(pdfPath, join10(forkPath, publicPath));
3633
+ mkdirSync4(dirname4(join11(forkPath, publicPath)), { recursive: true });
3634
+ copyFileSync(pdfPath, join11(forkPath, publicPath));
3497
3635
  const count3 = (status) => ledger.criteria.filter((r) => r.status === status).length;
3498
3636
  const review = {
3499
3637
  standard: ledger.standard,
@@ -3600,8 +3738,8 @@ var a11yCommand = {
3600
3738
  };
3601
3739
 
3602
3740
  // src/tools/bench/command.ts
3603
- import { existsSync as existsSync7, readdirSync } from "fs";
3604
- import { basename, join as join11 } from "path";
3741
+ import { existsSync as existsSync8, readdirSync as readdirSync2 } from "fs";
3742
+ import { basename, join as join12 } from "path";
3605
3743
 
3606
3744
  // src/tools/bench/request.ts
3607
3745
  var BENCH_REQUEST_ENV = "CELLA_BENCH";
@@ -3610,11 +3748,11 @@ var BENCH_REQUEST_ENV = "CELLA_BENCH";
3610
3748
  var BENCH_DIR = "backend/bench";
3611
3749
  var ENTRY_COMMAND = ["pnpm", "exec", "tsx", "--no-experimental-webstorage", "bench/run.ts"];
3612
3750
  async function runBench(forkPath, options) {
3613
- if (!existsSync7(join11(forkPath, BENCH_DIR, "run.ts"))) {
3751
+ if (!existsSync8(join12(forkPath, BENCH_DIR, "run.ts"))) {
3614
3752
  throw new Error(`${BENCH_DIR}/run.ts is missing: upgrade the app from cella first`);
3615
3753
  }
3616
- const scenariosDir = join11(forkPath, BENCH_DIR, "scenarios");
3617
- const names = readdirSync(scenariosDir).filter((file) => file.endsWith(".ts")).map((file) => basename(file, ".ts")).sort();
3754
+ const scenariosDir = join12(forkPath, BENCH_DIR, "scenarios");
3755
+ const names = readdirSync2(scenariosDir).filter((file) => file.endsWith(".ts")).map((file) => basename(file, ".ts")).sort();
3618
3756
  if (options.scenario && !names.includes(options.scenario)) {
3619
3757
  throw new Error(`unknown scenario '${options.scenario}'. expected one of: ${names.join(", ")}`);
3620
3758
  }
@@ -3626,7 +3764,7 @@ async function runBench(forkPath, options) {
3626
3764
  scenariosDir,
3627
3765
  dataDir: toolDir(forkPath, "bench", "repository")
3628
3766
  };
3629
- const run = (env) => runCommand(join11(forkPath, "backend"), { ...env, [BENCH_REQUEST_ENV]: JSON.stringify(request) }, ENTRY_COMMAND);
3767
+ const run = (env) => runCommand(join12(forkPath, "backend"), { ...env, [BENCH_REQUEST_ENV]: JSON.stringify(request) }, ENTRY_COMMAND);
3630
3768
  return options.isolated ? withTestbed(forkPath, (testbed) => run(testbed.env)) : run(getEnvSnapshot());
3631
3769
  }
3632
3770
  var benchCommand = {
@@ -3665,98 +3803,12 @@ import { spawnSync as spawnSync6 } from "child_process";
3665
3803
  import { existsSync as existsSync9, readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
3666
3804
  import { join as join14 } from "path";
3667
3805
 
3668
- // src/tools/i18n/locales.ts
3669
- import { existsSync as existsSync8, readdirSync as readdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync6 } from "fs";
3670
- import { join as join12 } from "path";
3671
- var LOCALES_DIR = "locales";
3672
- var MANIFEST = "namespaces.json";
3673
- var SOURCE_LANGUAGE = "en";
3674
- var entryPattern = /^(\s+)("(?:[^"\\]|\\.)*")\s*:\s*("(?:[^"\\]|\\.)*"),?\s*$/;
3675
- function parseLocaleFile(path, text2) {
3676
- if (text2.trim() === "{}") return { path, indent: " ", entries: [] };
3677
- const lines = text2.replace(/\n$/, "").split("\n");
3678
- if (lines[0] !== "{" || lines.at(-1) !== "}") return "not one JSON object with one entry per line";
3679
- const entries = [];
3680
- let indent = " ";
3681
- for (const line of lines.slice(1, -1)) {
3682
- const match = entryPattern.exec(line);
3683
- if (!match) return `cannot read the line ${JSON.stringify(line.trim())}`;
3684
- indent = match[1];
3685
- entries.push({ key: JSON.parse(match[2]), value: JSON.parse(match[3]), raw: [match[2], match[3]] });
3686
- }
3687
- return { path, indent, entries };
3688
- }
3689
- function serializeLocaleFile(file) {
3690
- if (file.entries.length === 0) return "{}\n";
3691
- return `{
3692
- ${file.entries.map(({ raw }) => `${file.indent}${raw[0]}: ${raw[1]}`).join(",\n")}
3693
- }
3694
- `;
3695
- }
3696
- function writeLocaleFile(forkPath, file) {
3697
- writeFileSync6(join12(forkPath, file.path), serializeLocaleFile(file));
3698
- }
3699
- var textsOf = (file) => Object.fromEntries((file?.entries ?? []).map(({ key, value }) => [key, value]));
3700
- var readJsonObject = (path) => {
3701
- const parsed2 = JSON.parse(readFileSync6(path, "utf8"));
3702
- if (!parsed2 || typeof parsed2 !== "object" || Array.isArray(parsed2)) throw new Error(`${path} is not a JSON object`);
3703
- return Object.fromEntries(Object.entries(parsed2));
3704
- };
3705
- function readLocales(forkPath) {
3706
- const dir = join12(forkPath, LOCALES_DIR);
3707
- if (!existsSync8(join12(dir, SOURCE_LANGUAGE))) throw new Error(`no ${LOCALES_DIR}/${SOURCE_LANGUAGE}/ in ${forkPath}`);
3708
- 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));
3709
- const files2 = /* @__PURE__ */ new Map();
3710
- for (const language2 of languages) {
3711
- for (const name of readdirSync2(join12(dir, language2)).filter((name2) => name2.endsWith(".json"))) {
3712
- const path = `${LOCALES_DIR}/${language2}/${name}`;
3713
- const parsed2 = parseLocaleFile(path, readFileSync6(join12(forkPath, path), "utf8"));
3714
- if (typeof parsed2 === "string") throw new Error(`${path}: ${parsed2}; pnpm lint:fix writes one entry per line`);
3715
- files2.set(`${language2}/${name.slice(0, -".json".length)}`, parsed2);
3716
- }
3717
- }
3718
- const manifestPath = join12(dir, MANIFEST);
3719
- if (!existsSync8(manifestPath)) throw new Error(`no ${LOCALES_DIR}/${MANIFEST}: upgrade the app from cella first`);
3720
- const merged = readJsonObject(manifestPath);
3721
- const namespaces = /* @__PURE__ */ new Map();
3722
- for (const [namespace, sources] of Object.entries(merged)) {
3723
- if (!Array.isArray(sources) || !sources.every((source) => typeof source === "string")) {
3724
- throw new Error(`${LOCALES_DIR}/${MANIFEST}: ${namespace} must list file names`);
3725
- }
3726
- namespaces.set(namespace, sources);
3727
- }
3728
- const mergedFiles = new Set([...namespaces.values()].flat());
3729
- const ownFiles = [...new Set([...files2.keys()].map((key) => key.split("/")[1]))].filter(
3730
- (file) => !mergedFiles.has(file)
3731
- );
3732
- for (const file of ownFiles.sort()) namespaces.set(file, [file]);
3733
- return { languages, namespaces, files: files2 };
3734
- }
3735
- function namespaceTexts(locales, language2, namespace) {
3736
- const sources = locales.namespaces.get(namespace) ?? [];
3737
- return Object.assign({}, ...sources.map((file) => textsOf(locales.files.get(`${language2}/${file}`))));
3738
- }
3739
- var lineOf = (file, key) => file.entries.findIndex((entry) => entry.key === key) + 2;
3740
- var pluralSuffix = /_(ordinal_)?(zero|one|two|few|many|other)$/;
3741
- function pluralForm(key) {
3742
- const match = pluralSuffix.exec(key);
3743
- if (!match) return void 0;
3744
- return { base: key.slice(0, match.index), category: match[2], ordinal: !!match[1] };
3745
- }
3746
- var withPluralForms = (key) => [
3747
- key,
3748
- ...["zero", "one", "two", "few", "many", "other"].flatMap((category) => [
3749
- `${key}_${category}`,
3750
- `${key}_ordinal_${category}`
3751
- ])
3752
- ];
3753
-
3754
3806
  // src/tools/i18n/scan.ts
3755
3807
  import { execFileSync as execFileSync2 } from "child_process";
3756
3808
  import { readFileSync as readFileSync7, statSync as statSync2 } from "fs";
3757
3809
  import { dirname as dirname5, join as join13 } from "path";
3758
3810
  var codeFile = /\.(?:[cm]?[jt]sx?)$/;
3759
- 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."));
3811
+ 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."));
3760
3812
  var MAX_FILE_BYTES = 1e6;
3761
3813
  var MAX_COMBINATIONS = 500;
3762
3814
  var escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -4173,10 +4225,10 @@ function printList(title, items) {
4173
4225
  console.info(` ${colors_default.dim(`${items.slice(0, LIST_MAX).join(", ")}${more}`)}`);
4174
4226
  }
4175
4227
  var describe = ({ key, missing, extra }) => [key, ...missing.map((name) => `-${name}`), ...extra.map((name) => `+${name}`)].join(" ");
4176
- function printStatus(report) {
4228
+ function printStatus(report, dir) {
4177
4229
  const label = ({ name, files: files2 }) => files2.length > 1 || files2[0] !== name ? `${name} (${files2.join(" + ")})` : name;
4178
4230
  console.info(
4179
- `${colors_default.bold(`${LOCALES_DIR}/`)} ${colors_default.dim(`${report.languages.join(", ")} \xB7 namespaces ${report.namespaces.map(label).join(", ")}`)}`
4231
+ `${colors_default.bold(`${dir}/`)} ${colors_default.dim(`${report.languages.join(", ")} \xB7 namespaces ${report.namespaces.map(label).join(", ")}`)}`
4180
4232
  );
4181
4233
  console.info();
4182
4234
  console.info(colors_default.dim(row2(columns.map(([title]) => title))));
@@ -4248,9 +4300,10 @@ function printStatus(report) {
4248
4300
  console.info(` ${colors_default.dim("missing and same-as-English keys: --json lists them \xB7 cella i18n unused")}`);
4249
4301
  }
4250
4302
  function runStatus(forkPath, json) {
4251
- const report = buildStatus(readLocales(forkPath));
4303
+ const locales = readLocales(forkPath);
4304
+ const report = buildStatus(locales);
4252
4305
  if (json) writeStdout(JSON.stringify(report, null, 2));
4253
- else printStatus(report);
4306
+ else printStatus(report, locales.dir);
4254
4307
  return defectCount(report) === 0 ? 0 : 1;
4255
4308
  }
4256
4309
 
@@ -5270,7 +5323,7 @@ var pentestCommand = {
5270
5323
 
5271
5324
  // src/tools/screenshots/shoot.ts
5272
5325
  import { spawnSync as spawnSync9 } from "child_process";
5273
- import { mkdirSync as mkdirSync7, mkdtempSync, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync10 } from "fs";
5326
+ import { mkdirSync as mkdirSync7, mkdtempSync, readFileSync as readFileSync13, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync10 } from "fs";
5274
5327
  import { tmpdir } from "os";
5275
5328
  import { basename as basename4, dirname as dirname7, join as join21, resolve as resolve7 } from "path";
5276
5329
 
@@ -5350,6 +5403,7 @@ async function spinnersGone(page, quiet = 1e3, cap = 45e3) {
5350
5403
  await page.waitForTimeout(100);
5351
5404
  }
5352
5405
  }
5406
+ var hasMfaScript = (forkPath) => Boolean(JSON.parse(readFileSync13(join21(forkPath, "backend/package.json"), "utf8")).scripts?.["showcase:mfa"]);
5353
5407
  async function shoot(forkPath, list2, testbed, options) {
5354
5408
  const email = options.email ?? testbed.env.ADMIN_EMAIL;
5355
5409
  if (!email) throw new Error("no shot user: pass --email, or set ADMIN_EMAIL in backend/.env");
@@ -5376,15 +5430,17 @@ async function shoot(forkPath, list2, testbed, options) {
5376
5430
  if (encoded.status !== 0) throw new Error(`cwebp could not write ${path}: ${encoded.stderr.trim()}`);
5377
5431
  };
5378
5432
  try {
5379
- const showcase = spawnSync9("pnpm", ["--silent", "--filter", "backend", "showcase:mfa", email], {
5380
- cwd: forkPath,
5381
- env: testbed.env,
5382
- encoding: "utf8"
5383
- });
5384
- if (showcase.status !== 0) throw new Error(`showcase:mfa failed:
5433
+ if (testbed.data === "seed" && hasMfaScript(forkPath)) {
5434
+ const showcase = spawnSync9("pnpm", ["--silent", "--filter", "backend", "showcase:mfa", email], {
5435
+ cwd: forkPath,
5436
+ env: testbed.env,
5437
+ encoding: "utf8"
5438
+ });
5439
+ if (showcase.status !== 0) throw new Error(`showcase:mfa failed:
5385
5440
  ${showcase.stdout}
5386
5441
  ${showcase.stderr}`);
5387
- printStep("mfa", showcase.stdout.trim().split("\n").at(-1));
5442
+ printStep("mfa", showcase.stdout.trim().split("\n").at(-1));
5443
+ }
5388
5444
  for (const prepare of list2.prepare) await prepare(signedIn(user));
5389
5445
  const values = {};
5390
5446
  for (const [name, resolve12] of Object.entries(list2.placeholders)) values[name] = await resolve12(signedIn(user));
@@ -5468,7 +5524,8 @@ async function runScreenshots(forkPath, options) {
5468
5524
  }
5469
5525
  requireEncoder(list2.format);
5470
5526
  await requireChromium(forkPath);
5471
- return withTestbed(forkPath, (testbed) => shoot(forkPath, list2, testbed, options));
5527
+ const data = hasShowcase(forkPath) ? "showcase" : "seed";
5528
+ return withTestbed(forkPath, (testbed) => shoot(forkPath, list2, testbed, options), { data });
5472
5529
  }
5473
5530
  var screenshotsCommand = {
5474
5531
  name: "screenshots",
@@ -5478,7 +5535,7 @@ var screenshotsCommand = {
5478
5535
  operands: ["[ids...]", "the shots to take (default: all)"],
5479
5536
  options: [
5480
5537
  ["--out <dir>", "write into this folder, a trial run that leaves the working tree alone"],
5481
- ["--email <address>", "the shot user (default ADMIN_EMAIL in backend/.env)"]
5538
+ ["--email <address>", "the shot user (default the showcase admin, else ADMIN_EMAIL in backend/.env)"]
5482
5539
  ],
5483
5540
  parse: ({ operands, opts }) => ({
5484
5541
  ids: operands,
@@ -5489,7 +5546,7 @@ var screenshotsCommand = {
5489
5546
  };
5490
5547
 
5491
5548
  // src/tools/style/index.ts
5492
- import { readFileSync as readFileSync15 } from "fs";
5549
+ import { readFileSync as readFileSync16 } from "fs";
5493
5550
  import { join as join27 } from "path";
5494
5551
 
5495
5552
  // src/utils/alignments.ts
@@ -5737,7 +5794,9 @@ var alignmentExempt = [
5737
5794
  "json/",
5738
5795
  "locales/",
5739
5796
  "sdk/gen",
5740
- "shared/config"
5797
+ "shared/config",
5798
+ "shared/json/",
5799
+ "shared/locales/"
5741
5800
  ];
5742
5801
  function ownedPaths(configSource) {
5743
5802
  const lists = [...configSource.matchAll(/\b(?:ignored|pinned): \[([\s\S]*?)\n {4}\]/g)].map((match) => match[1]);
@@ -6042,7 +6101,7 @@ function docFindings(file, source) {
6042
6101
  }
6043
6102
 
6044
6103
  // src/tools/style/frontend-rules.ts
6045
- import { existsSync as existsSync19, readFileSync as readFileSync13 } from "fs";
6104
+ import { existsSync as existsSync19, readFileSync as readFileSync14 } from "fs";
6046
6105
  import { extname as extname4, join as join24 } from "path";
6047
6106
  function containsJsx(node) {
6048
6107
  return ts.isJsxElement(node) || ts.isJsxFragment(node) || ts.isJsxSelfClosingElement(node) || !!ts.forEachChild(node, containsJsx);
@@ -6214,7 +6273,7 @@ function inProductModule(file) {
6214
6273
  if (!folder) return false;
6215
6274
  const queryFile = join24(repoRoot, `${folder}query.ts`);
6216
6275
  if (!productModuleFolders.has(queryFile)) {
6217
- const source = existsSync19(queryFile) ? readFileSync13(queryFile, "utf8") : "";
6276
+ const source = existsSync19(queryFile) ? readFileSync14(queryFile, "utf8") : "";
6218
6277
  productModuleFolders.set(
6219
6278
  queryFile,
6220
6279
  markCalls(ts.createSourceFile(queryFile, source, ts.ScriptTarget.Latest)).registersRangeFetch
@@ -6238,7 +6297,7 @@ function checkProductCacheWrite(file, sourceFile, report) {
6238
6297
  function frontendStores(files2) {
6239
6298
  const stores = /* @__PURE__ */ new Set();
6240
6299
  for (const file of files2.filter(isFrontendSource)) {
6241
- const source = readFileSync13(join24(repoRoot, file), "utf8");
6300
+ const source = readFileSync14(join24(repoRoot, file), "utf8");
6242
6301
  if (source.includes("zustand")) collectStores(parseSource(file, source), stores);
6243
6302
  }
6244
6303
  return stores;
@@ -6324,7 +6383,7 @@ function stampFindings(file, source) {
6324
6383
  }
6325
6384
 
6326
6385
  // src/tools/style/tailwind-class-rules.ts
6327
- import { readFileSync as readFileSync14 } from "fs";
6386
+ import { readFileSync as readFileSync15 } from "fs";
6328
6387
  import { createRequire as createRequire2 } from "module";
6329
6388
  import { dirname as dirname9, join as join25, relative as relative2 } from "path";
6330
6389
 
@@ -6392,11 +6451,11 @@ function addMatches(target, text2, pattern) {
6392
6451
  function usedClassNames(files2) {
6393
6452
  const names = /* @__PURE__ */ new Set();
6394
6453
  for (const file of files2.filter(isFrontendStylesheet)) {
6395
- const css = readFileSync14(join25(repoRoot, file), "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
6454
+ const css = readFileSync15(join25(repoRoot, file), "utf8").replace(/\/\*[\s\S]*?\*\//g, "");
6396
6455
  addMatches(names, css, /(?<!\\)\.(-?[_a-zA-Z][\w-]*)/g);
6397
6456
  }
6398
6457
  for (const file of files2.filter(isFrontendScript)) {
6399
- const source = readFileSync14(join25(repoRoot, file), "utf8");
6458
+ const source = readFileSync15(join25(repoRoot, file), "utf8");
6400
6459
  for (const [chunk] of source.matchAll(/(?<![\w$)\]?.])\[[^\s'"`]*/g))
6401
6460
  addMatches(names, chunk, /(?<![a-zA-Z0-9$.)\]?!])\.(-?[_a-zA-Z][\w-]*)/g);
6402
6461
  for (const [, , text2] of source.matchAll(/(['"`])((?:\\.|(?!\1)[^\\\n])*)\1/g)) {
@@ -6412,14 +6471,14 @@ function usedClassNames(files2) {
6412
6471
  return names;
6413
6472
  }
6414
6473
  async function tailwindContext(files2, appMarkerClasses = {}) {
6415
- const entry = files2.filter(isFrontendStylesheet).find((file) => tailwindImport.test(readFileSync14(join25(repoRoot, file), "utf8")));
6474
+ const entry = files2.filter(isFrontendStylesheet).find((file) => tailwindImport.test(readFileSync15(join25(repoRoot, file), "utf8")));
6416
6475
  if (!entry) return void 0;
6417
6476
  const path = join25(repoRoot, entry);
6418
6477
  const knownClasses = usedClassNames(files2);
6419
6478
  for (const name of Object.keys({ ...markerClasses, ...appMarkerClasses })) knownClasses.add(name);
6420
6479
  const context = { entry, knownClasses, compiles: /* @__PURE__ */ new Map() };
6421
6480
  try {
6422
- context.designSystem = await (await loadTailwindNode()).__unstable__loadDesignSystem(readFileSync14(path, "utf8"), {
6481
+ context.designSystem = await (await loadTailwindNode()).__unstable__loadDesignSystem(readFileSync15(path, "utf8"), {
6423
6482
  base: dirname9(path)
6424
6483
  });
6425
6484
  } catch (error) {
@@ -6848,7 +6907,7 @@ async function runStyle(root, { paths }) {
6848
6907
  const files2 = repoFiles();
6849
6908
  const stores = frontendStores(files2);
6850
6909
  const tailwind = await tailwindContext(files2, allowlist.markerClasses);
6851
- const read = (file) => readFileSync15(join27(repoRoot, file), "utf8");
6910
+ const read = (file) => readFileSync16(join27(repoRoot, file), "utf8");
6852
6911
  const requested = files2.filter((file) => isRequested(file, paths));
6853
6912
  const findings2 = paths.filter((path) => !files2.some((file) => isRequested(file, [path]))).map((path) => ({ file: path, rule: "path-argument", message: "no file matches this path" }));
6854
6913
  findings2.push(...alignmentFindings(requested, read));
@@ -6856,7 +6915,7 @@ async function runStyle(root, { paths }) {
6856
6915
  findings2.push(...owedAlignmentFindings(await branchChanges(), read("cella/cella.config.ts")));
6857
6916
  }
6858
6917
  for (const file of requested) {
6859
- const content = readFileSync15(join27(repoRoot, file));
6918
+ const content = readFileSync16(join27(repoRoot, file));
6860
6919
  if (content.includes(0)) continue;
6861
6920
  const source = content.toString("utf8");
6862
6921
  const prose = allowlist.proseExclude?.some((prefix) => file.startsWith(prefix)) ? [] : [...docFindings(file, source), ...commentFindings(file, source)];
@@ -7573,8 +7632,9 @@ async function analyzeRefs(repoPath, localRef, incomingRef, mergeBaseRef, predic
7573
7632
  const forkModifiedOld = forkOldHash !== baseOldHash;
7574
7633
  const oldPathPinned = predicates.isPinned(oldPath);
7575
7634
  let upstreamChanged2;
7576
- if (fileIsPinned || oldPathPinned || predicates.isIgnored(oldPath)) {
7635
+ if (fileIsPinned || fileIsIgnored || oldPathPinned || predicates.isIgnored(oldPath)) {
7577
7636
  status = "pinned";
7637
+ if (oldPathInFork) renamedFrom = oldPath;
7578
7638
  if (forkModifiedOld && upstreamHash !== baseOldHash) {
7579
7639
  upstreamChanged2 = true;
7580
7640
  renamedProtected.add(filePath);
@@ -7730,9 +7790,11 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
7730
7790
  );
7731
7791
  const batchRestorePaths = [];
7732
7792
  const batchRemovePaths = [];
7793
+ const movedProtected = [];
7733
7794
  for (const file of analyzedFiles) {
7734
7795
  if (file.status === "identical" || file.status === "ahead") continue;
7735
- if (file.isIgnored || file.isPinned && !file.renamedFrom) {
7796
+ if (file.status === "pinned" && file.renamedFrom) movedProtected.push({ from: file.renamedFrom, to: file.path });
7797
+ else if (file.isIgnored || file.isPinned) {
7736
7798
  (file.existsInFork ? batchRestorePaths : batchRemovePaths).push(file.path);
7737
7799
  }
7738
7800
  }
@@ -7749,6 +7811,10 @@ async function applyDirectMerge(forkPath, upstreamRef, mergeBaseRef, config, onP
7749
7811
  await removeFileFromWorktree(forkPath, filePath);
7750
7812
  }
7751
7813
  }
7814
+ for (const { from, to } of movedProtected) {
7815
+ onProgress?.(`\u2192 ${from} \u2192 ${to}: keeping your copy (moved in upstream)`);
7816
+ await moveFromRef(forkPath, "HEAD", from, to);
7817
+ }
7752
7818
  for (const file of analyzedFiles) {
7753
7819
  const { path: filePath, status, existsInFork } = file;
7754
7820
  if (status === "identical" || status === "ahead" || isProtectedFile(file)) continue;
@@ -8999,7 +9065,7 @@ function syncStep(result) {
8999
9065
  }
9000
9066
 
9001
9067
  // src/services/audit.ts
9002
- import { readFileSync as readFileSync18 } from "fs";
9068
+ import { readFileSync as readFileSync19 } from "fs";
9003
9069
  import { basename as basename9, join as join35, relative as relative4 } from "path";
9004
9070
  import { checkbox, Separator } from "@inquirer/prompts";
9005
9071
 
@@ -9114,7 +9180,7 @@ import { delimiter, dirname as dirname13, join as join33, sep as sep2 } from "pa
9114
9180
  import { promisify as promisify3 } from "util";
9115
9181
 
9116
9182
  // src/utils/runtimes.ts
9117
- import { readFileSync as readFileSync16 } from "fs";
9183
+ import { readFileSync as readFileSync17 } from "fs";
9118
9184
  import { basename as basename8, dirname as dirname12, join as join32 } from "path";
9119
9185
  var DAY = 24 * 60 * 60 * 1e3;
9120
9186
  var FROZEN_AFTER = 180 * DAY;
@@ -9157,7 +9223,7 @@ function parseDockerfile(file, content) {
9157
9223
  async function readRuntimePins(forkPath) {
9158
9224
  let manifest = {};
9159
9225
  try {
9160
- manifest = JSON.parse(readFileSync16(join32(forkPath, "package.json"), "utf8"));
9226
+ manifest = JSON.parse(readFileSync17(join32(forkPath, "package.json"), "utf8"));
9161
9227
  } catch {
9162
9228
  }
9163
9229
  const node = manifest.engines?.node?.trim() ?? "";
@@ -9168,7 +9234,7 @@ async function readRuntimePins(forkPath) {
9168
9234
  for (const file of tracked) {
9169
9235
  let content;
9170
9236
  try {
9171
- content = readFileSync16(join32(forkPath, file), "utf8");
9237
+ content = readFileSync17(join32(forkPath, file), "utf8");
9172
9238
  } catch {
9173
9239
  continue;
9174
9240
  }
@@ -9684,14 +9750,14 @@ function writeAuditSettings(content, next) {
9684
9750
 
9685
9751
  // src/services/audit-security.ts
9686
9752
  import { spawnSync as spawnSync11 } from "child_process";
9687
- import { cpSync, existsSync as existsSync25, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync17, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
9753
+ import { cpSync, existsSync as existsSync25, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, readFileSync as readFileSync18, rmSync as rmSync7, writeFileSync as writeFileSync12 } from "fs";
9688
9754
  import { tmpdir as tmpdir3 } from "os";
9689
9755
  import { dirname as dirname14, join as join34, relative as relative3 } from "path";
9690
9756
  import { confirm, select as select2 } from "@inquirer/prompts";
9691
9757
  var WORKSPACE_FILE = "pnpm-workspace.yaml";
9692
9758
  var LOCK_FILE = "pnpm-lock.yaml";
9693
9759
  var RESOLUTION_FILES = ["package.json", LOCK_FILE, ".npmrc", ".pnpmfile.cjs"];
9694
- var readOr = (path, fallback = "") => existsSync25(path) ? readFileSync17(path, "utf8") : fallback;
9760
+ var readOr = (path, fallback = "") => existsSync25(path) ? readFileSync18(path, "utf8") : fallback;
9695
9761
  var count2 = (n, noun, plural2 = `${noun}s`) => `${n} ${n === 1 ? noun : plural2}`;
9696
9762
  var isBreaking = (item) => item.remedy.kind !== "none" && item.remedy.breaking;
9697
9763
  var needsDecision = (item) => item.standing === "new" && (item.remedy.kind !== "override" || item.remedy.breaking || item.direct);
@@ -10004,7 +10070,7 @@ function createDependentPkgReader() {
10004
10070
  let cached = cache.get(location);
10005
10071
  if (cached === void 0) {
10006
10072
  try {
10007
- cached = JSON.parse(readFileSync18(join35(location, "package.json"), "utf8"));
10073
+ cached = JSON.parse(readFileSync19(join35(location, "package.json"), "utf8"));
10008
10074
  } catch {
10009
10075
  cached = null;
10010
10076
  }
@@ -10176,7 +10242,7 @@ function printOutdatedResults(rows, forkPath) {
10176
10242
  function printNotes(forkPath, unchecked) {
10177
10243
  let held = [];
10178
10244
  try {
10179
- const updateConfig = readTopLevel(readFileSync18(join35(forkPath, "pnpm-workspace.yaml"), "utf8"), "updateConfig");
10245
+ const updateConfig = readTopLevel(readFileSync19(join35(forkPath, "pnpm-workspace.yaml"), "utf8"), "updateConfig");
10180
10246
  const ignored = updateConfig?.ignoreDependencies;
10181
10247
  held = Array.isArray(ignored) ? ignored.map(String) : [];
10182
10248
  } catch {
@@ -10300,7 +10366,7 @@ async function planEdits(forkPath, rows, selected, nodeImageSuffixes) {
10300
10366
  const summary = /* @__PURE__ */ new Map();
10301
10367
  const bumped = [];
10302
10368
  const edit = (file, label, item, resolves, change) => {
10303
- const entry = contents.get(file) ?? { content: readFileSync18(join35(forkPath, file), "utf8"), resolves: false };
10369
+ const entry = contents.get(file) ?? { content: readFileSync19(join35(forkPath, file), "utf8"), resolves: false };
10304
10370
  const next = change(entry.content);
10305
10371
  if (next === entry.content) return;
10306
10372
  contents.set(file, { content: next, resolves: entry.resolves || resolves });
@@ -10836,11 +10902,11 @@ function closePr(cwd, ref) {
10836
10902
  }
10837
10903
 
10838
10904
  // src/services/packages.ts
10839
- import { existsSync as existsSync27, readFileSync as readFileSync20, writeFileSync as writeFileSync13 } from "fs";
10905
+ import { existsSync as existsSync27, readFileSync as readFileSync21, writeFileSync as writeFileSync13 } from "fs";
10840
10906
  import { join as join38 } from "path";
10841
10907
 
10842
10908
  // src/services/package-usage.ts
10843
- import { existsSync as existsSync26, readFileSync as readFileSync19, statSync as statSync4 } from "fs";
10909
+ import { existsSync as existsSync26, readFileSync as readFileSync20, statSync as statSync4 } from "fs";
10844
10910
  import { join as join37 } from "path";
10845
10911
  var MAX_FILE_BYTES2 = 1024 * 1024;
10846
10912
  function escapeRegExp2(value) {
@@ -10849,7 +10915,7 @@ function escapeRegExp2(value) {
10849
10915
  function readSearchable(filePath) {
10850
10916
  try {
10851
10917
  if (statSync4(filePath).size > MAX_FILE_BYTES2) return "";
10852
- const text2 = readFileSync19(filePath, "utf8");
10918
+ const text2 = readFileSync20(filePath, "utf8");
10853
10919
  return text2.includes("\0") ? "" : text2;
10854
10920
  } catch {
10855
10921
  return "";
@@ -10869,7 +10935,7 @@ async function readScriptsAtRef(forkPath, ref, path) {
10869
10935
  async function scriptSources(forkPath, upstreamRef, baseRef, path) {
10870
10936
  let forkScripts;
10871
10937
  try {
10872
- forkScripts = JSON.parse(readFileSync19(join37(forkPath, path), "utf8")).scripts ?? {};
10938
+ forkScripts = JSON.parse(readFileSync20(join37(forkPath, path), "utf8")).scripts ?? {};
10873
10939
  } catch {
10874
10940
  return [];
10875
10941
  }
@@ -10883,7 +10949,7 @@ function binNames(forkPath, location, name) {
10883
10949
  const manifest = join37(dir, "node_modules", name, "package.json");
10884
10950
  if (!existsSync26(manifest)) continue;
10885
10951
  try {
10886
- const { bin } = JSON.parse(readFileSync19(manifest, "utf8"));
10952
+ const { bin } = JSON.parse(readFileSync20(manifest, "utf8"));
10887
10953
  if (typeof bin === "string") return [bareName];
10888
10954
  return bin && typeof bin === "object" ? Object.keys(bin) : [];
10889
10955
  } catch {
@@ -11064,7 +11130,7 @@ function setTopLevelKey(pkg, key, value) {
11064
11130
  function readPackageJson(filePath) {
11065
11131
  if (!existsSync27(filePath)) return null;
11066
11132
  try {
11067
- return JSON.parse(readFileSync20(filePath, "utf8"));
11133
+ return JSON.parse(readFileSync21(filePath, "utf8"));
11068
11134
  } catch (error) {
11069
11135
  throw new Error(`cannot parse ${filePath}: ${errorMessage(error)}`);
11070
11136
  }
@@ -11802,7 +11868,7 @@ import { readFile as readFile3 } from "fs/promises";
11802
11868
  import { join as join40 } from "path";
11803
11869
 
11804
11870
  // src/utils/coverage-utils.ts
11805
- import { existsSync as existsSync28, readFileSync as readFileSync21, statSync as statSync5 } from "fs";
11871
+ import { existsSync as existsSync28, readFileSync as readFileSync22, statSync as statSync5 } from "fs";
11806
11872
  import { join as join39, relative as relative5, sep as sep3 } from "path";
11807
11873
  var STALE_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1e3;
11808
11874
  function colorPct(pct) {
@@ -11828,7 +11894,7 @@ function printCoverageSummary(forkPath) {
11828
11894
  if (existsSync28(file)) {
11829
11895
  try {
11830
11896
  mtimeMs = statSync5(file).mtimeMs;
11831
- summary = JSON.parse(readFileSync21(file, "utf8"));
11897
+ summary = JSON.parse(readFileSync22(file, "utf8"));
11832
11898
  } catch {
11833
11899
  problem2 = "could not read coverage-summary.json";
11834
11900
  }