@reddoorla/maintenance 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -122,7 +122,7 @@ Each audit is `(ctx) => Promise<AuditResult>` and is exported from the package e
122
122
  | Name | What it checks |
123
123
  | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
124
  | `deps` | Diffs site's `package.json` against `src/configs/baseline-versions.ts`. Surfaces deps that drift from the canonical version map. |
125
- | `lighthouse` | Runs `@lhci/cli autorun` using the canonical `lighthouserc.json`. |
125
+ | `lighthouse` | Runs `@lhci/cli autorun` using the canonical `lighthouserc.json`. Hits `/dev/a11y-fixtures` by default; sites without that route can set `package.json#reddoor.lighthouseUrl` to override. |
126
126
  | `a11y` | Spawns Playwright + `@axe-core/playwright` against a canonical set of a11y routes. |
127
127
  | `security` | `pnpm audit --json --prod` with automatic fall-through to `npm audit` when pnpm can't run (missing lockfile, error envelope, etc.). Normalises advisory shapes from both tools into a single `AdvisoryEntry[]`. |
128
128
  | `lint` | ESLint + Prettier using the canonical configs (re-exported via `@reddoorla/maintenance/configs/eslint` and `@reddoorla/maintenance/configs/prettier`). |
package/dist/cli/bin.js CHANGED
@@ -138,8 +138,8 @@ __export(lighthouse_airtable_exports, {
138
138
  lighthouseScoresFromResult: () => lighthouseScoresFromResult,
139
139
  resolveSlugFromCwd: () => resolveSlugFromCwd
140
140
  });
141
- import { readFile as readFile6 } from "fs/promises";
142
- import { join as join6 } from "path";
141
+ import { readFile as readFile7 } from "fs/promises";
142
+ import { join as join7 } from "path";
143
143
  function lighthouseScoresFromResult(result) {
144
144
  if (result.audit !== "lighthouse") {
145
145
  throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);
@@ -156,8 +156,8 @@ function lighthouseScoresFromResult(result) {
156
156
  }
157
157
  async function resolveSlugFromCwd(cwd) {
158
158
  try {
159
- const pkgPath = join6(cwd, "package.json");
160
- const raw = await readFile6(pkgPath, "utf-8");
159
+ const pkgPath = join7(cwd, "package.json");
160
+ const raw = await readFile7(pkgPath, "utf-8");
161
161
  const pkg = JSON.parse(raw);
162
162
  if (!pkg.name) throw new Error("package.json has no 'name' field");
163
163
  return siteSlug(pkg.name);
@@ -283,13 +283,13 @@ var init_reports = __esm({
283
283
  });
284
284
 
285
285
  // src/reports/maintenance-email/assets/index.ts
286
- import { readFile as readFile12 } from "fs/promises";
287
- import { dirname as dirname2, join as join20 } from "path";
286
+ import { readFile as readFile13 } from "fs/promises";
287
+ import { dirname as dirname2, join as join21 } from "path";
288
288
  import { fileURLToPath as fileURLToPath2 } from "url";
289
289
  async function loadBundledImages() {
290
290
  const [check, blurred] = await Promise.all([
291
- readFile12(join20(here, "check.png")),
292
- readFile12(join20(here, "blurredTests.jpg"))
291
+ readFile13(join21(here, "check.png")),
292
+ readFile13(join21(here, "blurredTests.jpg"))
293
293
  ]);
294
294
  return {
295
295
  check: {
@@ -1079,9 +1079,9 @@ async function securityAudit(ctx) {
1079
1079
  }
1080
1080
 
1081
1081
  // src/audits/lighthouse.ts
1082
- import { readFile as readFile3, writeFile, mkdtemp, rm } from "fs/promises";
1082
+ import { readFile as readFile4, writeFile, mkdtemp, rm } from "fs/promises";
1083
1083
  import { tmpdir } from "os";
1084
- import { join as join3 } from "path";
1084
+ import { join as join4 } from "path";
1085
1085
 
1086
1086
  // src/configs/lighthouse.ts
1087
1087
  var lighthouseConfig = {
@@ -1114,10 +1114,37 @@ var lighthouseConfig = {
1114
1114
  }
1115
1115
  };
1116
1116
 
1117
+ // src/audits/util/site-config.ts
1118
+ import { readFile as readFile3 } from "fs/promises";
1119
+ import { join as join3 } from "path";
1120
+ async function readSiteConfig(sitePath) {
1121
+ let raw;
1122
+ try {
1123
+ raw = await readFile3(join3(sitePath, "package.json"), "utf-8");
1124
+ } catch {
1125
+ return {};
1126
+ }
1127
+ let pkg;
1128
+ try {
1129
+ pkg = JSON.parse(raw);
1130
+ } catch {
1131
+ return {};
1132
+ }
1133
+ if (!pkg || typeof pkg !== "object") return {};
1134
+ const cfg = pkg.reddoor;
1135
+ if (!cfg || typeof cfg !== "object") return {};
1136
+ const out = {};
1137
+ const url = cfg.lighthouseUrl;
1138
+ if (typeof url === "string" && url.length > 0) {
1139
+ out.lighthouseUrl = url;
1140
+ }
1141
+ return out;
1142
+ }
1143
+
1117
1144
  // src/audits/lighthouse.ts
1118
1145
  async function readJsonMaybe(path) {
1119
1146
  try {
1120
- const raw = await readFile3(path, "utf-8");
1147
+ const raw = await readFile4(path, "utf-8");
1121
1148
  return JSON.parse(raw);
1122
1149
  } catch {
1123
1150
  return null;
@@ -1153,15 +1180,28 @@ async function lighthouseAudit(ctx) {
1153
1180
  const spawn2 = ctx.spawn ?? defaultSpawn;
1154
1181
  const site = ctx.site;
1155
1182
  const label = siteLabel(site);
1156
- const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
1157
- const configPath = join3(configDir, "lighthouserc.json");
1158
- await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
1159
- const resultsDir = join3(site.path, ".lighthouseci");
1183
+ const siteCfg = await readSiteConfig(site.path);
1184
+ const resolvedConfig = siteCfg.lighthouseUrl ? {
1185
+ ...lighthouseConfig,
1186
+ ci: {
1187
+ ...lighthouseConfig.ci,
1188
+ collect: { ...lighthouseConfig.ci.collect, url: [siteCfg.lighthouseUrl] }
1189
+ }
1190
+ } : lighthouseConfig;
1191
+ const configDir = await mkdtemp(join4(tmpdir(), "reddoor-lhci-"));
1192
+ const configPath = join4(configDir, "lighthouserc.json");
1193
+ await writeFile(configPath, JSON.stringify(resolvedConfig), "utf-8");
1194
+ const resultsDir = join4(site.path, ".lighthouseci");
1160
1195
  await rm(resultsDir, { recursive: true, force: true });
1161
1196
  let raw;
1162
1197
  try {
1163
1198
  raw = await spawn2("npx", ["--yes", "@lhci/cli", "autorun", `--config=${configPath}`], {
1164
- cwd: site.path
1199
+ cwd: site.path,
1200
+ // lhci autorun boots the site's dev server, downloads Chrome on first
1201
+ // use, and runs the audit — easily 2–3 min on a cold tree. The shared
1202
+ // 30 s default in runAudits is fine for deps/lint/security but starves
1203
+ // lhci.
1204
+ timeoutMs: 5 * 6e4
1165
1205
  });
1166
1206
  } catch (err) {
1167
1207
  await rm(configDir, { recursive: true, force: true });
@@ -1177,7 +1217,7 @@ async function lighthouseAudit(ctx) {
1177
1217
  throw err;
1178
1218
  }
1179
1219
  await rm(configDir, { recursive: true, force: true });
1180
- const manifest = await readJsonMaybe(join3(resultsDir, "manifest.json"));
1220
+ const manifest = await readJsonMaybe(join4(resultsDir, "manifest.json"));
1181
1221
  if (!manifest || manifest.length === 0) {
1182
1222
  return {
1183
1223
  audit: "lighthouse",
@@ -1186,7 +1226,7 @@ async function lighthouseAudit(ctx) {
1186
1226
  summary: `lighthouse: no manifest written (exit ${raw.code})${raw.stderr ? ` \u2014 ${raw.stderr.slice(0, 200)}` : ""}`
1187
1227
  };
1188
1228
  }
1189
- const assertionResults = await readJsonMaybe(join3(resultsDir, "assertion-results.json")) ?? [];
1229
+ const assertionResults = await readJsonMaybe(join4(resultsDir, "assertion-results.json")) ?? [];
1190
1230
  const failed = assertionResults.filter((a) => !a.passed);
1191
1231
  const assertions = failed.map((a) => ({
1192
1232
  category: categoryFromAssertion(a),
@@ -1212,9 +1252,9 @@ async function lighthouseAudit(ctx) {
1212
1252
  }
1213
1253
 
1214
1254
  // src/audits/a11y.ts
1215
- import { readFile as readFile4, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1255
+ import { readFile as readFile5, writeFile as writeFile2, mkdtemp as mkdtemp2, rm as rm2 } from "fs/promises";
1216
1256
  import { tmpdir as tmpdir2 } from "os";
1217
- import { join as join4 } from "path";
1257
+ import { join as join5 } from "path";
1218
1258
 
1219
1259
  // src/configs/playwright-a11y.ts
1220
1260
  import { defineConfig, devices } from "@playwright/test";
@@ -1252,7 +1292,7 @@ var playwrightA11yConfig = defineConfig({
1252
1292
  var RESULTS_REL = ".reddoor-a11y/results.json";
1253
1293
  async function readJsonMaybe2(path) {
1254
1294
  try {
1255
- const raw = await readFile4(path, "utf-8");
1295
+ const raw = await readFile5(path, "utf-8");
1256
1296
  return JSON.parse(raw);
1257
1297
  } catch {
1258
1298
  return null;
@@ -1308,11 +1348,11 @@ async function a11yAudit(ctx) {
1308
1348
  const spawn2 = ctx.spawn ?? defaultSpawn;
1309
1349
  const site = ctx.site;
1310
1350
  const label = siteLabel(site);
1311
- const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
1312
- const specPath = join4(specDir, "a11y.spec.ts");
1351
+ const specDir = await mkdtemp2(join5(tmpdir2(), "reddoor-a11y-spec-"));
1352
+ const specPath = join5(specDir, "a11y.spec.ts");
1313
1353
  await writeFile2(specPath, buildSpec(), "utf-8");
1314
- const resultsPath = join4(site.path, RESULTS_REL);
1315
- await rm2(join4(site.path, ".reddoor-a11y"), { recursive: true, force: true });
1354
+ const resultsPath = join5(site.path, RESULTS_REL);
1355
+ await rm2(join5(site.path, ".reddoor-a11y"), { recursive: true, force: true });
1316
1356
  let raw;
1317
1357
  try {
1318
1358
  raw = await spawn2("npx", ["--yes", "playwright", "test", "--reporter=line", specPath], {
@@ -1405,7 +1445,7 @@ function localPath(path, opts = {}) {
1405
1445
  }
1406
1446
 
1407
1447
  // src/inventory/json.ts
1408
- import { readFile as readFile5 } from "fs/promises";
1448
+ import { readFile as readFile6 } from "fs/promises";
1409
1449
  import { isAbsolute } from "path";
1410
1450
  function validate(raw) {
1411
1451
  if (!Array.isArray(raw)) {
@@ -1435,7 +1475,7 @@ function validate(raw) {
1435
1475
  }
1436
1476
  function fromJsonFile(path) {
1437
1477
  return async () => {
1438
- const raw = JSON.parse(await readFile5(path, "utf-8"));
1478
+ const raw = JSON.parse(await readFile6(path, "utf-8"));
1439
1479
  return validate(raw);
1440
1480
  };
1441
1481
  }
@@ -1481,7 +1521,7 @@ async function resolveSites(input) {
1481
1521
 
1482
1522
  // src/cli/fleet/clone-if-needed.ts
1483
1523
  import { stat, readdir, mkdir } from "fs/promises";
1484
- import { isAbsolute as isAbsolute2, join as join5 } from "path";
1524
+ import { isAbsolute as isAbsolute2, join as join6 } from "path";
1485
1525
  function deriveNameFromRepoUrl(repoUrl) {
1486
1526
  const slash = repoUrl.split("/").pop() ?? repoUrl;
1487
1527
  return slash.replace(/\.git$/, "");
@@ -1522,7 +1562,7 @@ async function cloneIfNeeded(site, opts) {
1522
1562
  const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
1523
1563
  assertSafeName(name);
1524
1564
  assertSafeRepoUrl(site.repoUrl);
1525
- const target = join5(opts.workdir, name);
1565
+ const target = join6(opts.workdir, name);
1526
1566
  await mkdir(opts.workdir, { recursive: true });
1527
1567
  if (await isNonEmptyDir(target)) {
1528
1568
  return { ...site, name, path: target };
@@ -1609,12 +1649,12 @@ async function runAuditCommand(site, opts) {
1609
1649
  }
1610
1650
 
1611
1651
  // src/cli/commands/sync-configs.ts
1612
- import { readFile as readFile8 } from "fs/promises";
1613
- import { join as join8, resolve as resolve3 } from "path";
1652
+ import { readFile as readFile9 } from "fs/promises";
1653
+ import { join as join9, resolve as resolve3 } from "path";
1614
1654
 
1615
1655
  // src/recipes/sync-configs.ts
1616
- import { readFile as readFile7, writeFile as writeFile3 } from "fs/promises";
1617
- import { join as join7 } from "path";
1656
+ import { readFile as readFile8, writeFile as writeFile3 } from "fs/promises";
1657
+ import { join as join8 } from "path";
1618
1658
 
1619
1659
  // src/recipes/sync-configs/templates.ts
1620
1660
  var eslint = {
@@ -1878,7 +1918,7 @@ function isConfigName(value) {
1878
1918
  }
1879
1919
  async function readMaybe(path) {
1880
1920
  try {
1881
- return await readFile7(path, "utf-8");
1921
+ return await readFile8(path, "utf-8");
1882
1922
  } catch {
1883
1923
  return null;
1884
1924
  }
@@ -1886,13 +1926,13 @@ async function readMaybe(path) {
1886
1926
  async function planTemplateDiffs(cwd, templates) {
1887
1927
  const diffs = [];
1888
1928
  for (const t of templates) {
1889
- const existing = await readMaybe(join7(cwd, t.path));
1929
+ const existing = await readMaybe(join8(cwd, t.path));
1890
1930
  if (existing !== t.contents) diffs.push(t);
1891
1931
  }
1892
1932
  return diffs;
1893
1933
  }
1894
1934
  async function planGitignore(cwd) {
1895
- const existing = await readMaybe(join7(cwd, ".gitignore"));
1935
+ const existing = await readMaybe(join8(cwd, ".gitignore"));
1896
1936
  const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
1897
1937
  const tracked = await listTrackedFiles(cwd);
1898
1938
  const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
@@ -1900,7 +1940,7 @@ async function planGitignore(cwd) {
1900
1940
  return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
1901
1941
  }
1902
1942
  async function applyGitignore(cwd, plan) {
1903
- await writeFile3(join7(cwd, ".gitignore"), plan.content, "utf-8");
1943
+ await writeFile3(join8(cwd, ".gitignore"), plan.content, "utf-8");
1904
1944
  if (plan.toUntrack.length > 0) {
1905
1945
  await removeFromIndex(cwd, plan.toUntrack);
1906
1946
  }
@@ -1923,7 +1963,7 @@ async function syncConfigs(site, opts = {}) {
1923
1963
  },
1924
1964
  apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
1925
1965
  for (const t of templateDiffs) {
1926
- await writeFile3(join7(site.path, t.path), t.contents, "utf-8");
1966
+ await writeFile3(join8(site.path, t.path), t.contents, "utf-8");
1927
1967
  await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
1928
1968
  }
1929
1969
  if (gitignorePlan.kind === "apply") {
@@ -1952,7 +1992,7 @@ function parseOnly2(value) {
1952
1992
  async function dryPlanGitignore(cwd) {
1953
1993
  let existing;
1954
1994
  try {
1955
- existing = await readFile8(join8(cwd, ".gitignore"), "utf-8");
1995
+ existing = await readFile9(join9(cwd, ".gitignore"), "utf-8");
1956
1996
  } catch {
1957
1997
  return "would create .gitignore";
1958
1998
  }
@@ -1967,7 +2007,7 @@ async function dryPlan(cwd, which) {
1967
2007
  for (const t of templateTargets) {
1968
2008
  let existing = "";
1969
2009
  try {
1970
- existing = await readFile8(join8(cwd, t.path), "utf-8");
2010
+ existing = await readFile9(join9(cwd, t.path), "utf-8");
1971
2011
  } catch {
1972
2012
  }
1973
2013
  if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
@@ -2015,7 +2055,7 @@ import { resolve as resolve4 } from "path";
2015
2055
 
2016
2056
  // src/recipes/bump-deps.ts
2017
2057
  import { stat as stat2 } from "fs/promises";
2018
- import { join as join9 } from "path";
2058
+ import { join as join10 } from "path";
2019
2059
  async function exists(path) {
2020
2060
  try {
2021
2061
  await stat2(path);
@@ -2044,10 +2084,10 @@ async function bumpDeps(site, opts = {}) {
2044
2084
  // land on top of whatever else was in the tree.
2045
2085
  checkTreeFirst: true,
2046
2086
  plan: async () => {
2047
- const hasPnpmLock = await exists(join9(site.path, "pnpm-lock.yaml"));
2087
+ const hasPnpmLock = await exists(join10(site.path, "pnpm-lock.yaml"));
2048
2088
  if (!hasPnpmLock) {
2049
- const hasNpmLock = await exists(join9(site.path, "package-lock.json"));
2050
- const hasYarnLock = await exists(join9(site.path, "yarn.lock"));
2089
+ const hasNpmLock = await exists(join10(site.path, "package-lock.json"));
2090
+ const hasYarnLock = await exists(join10(site.path, "yarn.lock"));
2051
2091
  if (hasNpmLock || hasYarnLock) {
2052
2092
  const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
2053
2093
  return {
@@ -2117,12 +2157,12 @@ async function runBumpDepsCommand(site, opts) {
2117
2157
  import { resolve as resolve5 } from "path";
2118
2158
 
2119
2159
  // src/recipes/svelte-5/index.ts
2120
- import { join as join15 } from "path";
2160
+ import { join as join16 } from "path";
2121
2161
 
2122
2162
  // src/util/pkg.ts
2123
- import { readFile as readFile9, writeFile as writeFile4 } from "fs/promises";
2163
+ import { readFile as readFile10, writeFile as writeFile4 } from "fs/promises";
2124
2164
  async function readPackageJson(path) {
2125
- const raw = await readFile9(path, "utf-8");
2165
+ const raw = await readFile10(path, "utf-8");
2126
2166
  return JSON.parse(raw);
2127
2167
  }
2128
2168
  function detectIndentFromContent(raw) {
@@ -2132,7 +2172,7 @@ function detectIndentFromContent(raw) {
2132
2172
  async function writePackageJson(path, pkg) {
2133
2173
  let indent = " ";
2134
2174
  try {
2135
- const existing = await readFile9(path, "utf-8");
2175
+ const existing = await readFile10(path, "utf-8");
2136
2176
  indent = detectIndentFromContent(existing);
2137
2177
  } catch {
2138
2178
  }
@@ -2166,7 +2206,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
2166
2206
  }
2167
2207
 
2168
2208
  // src/recipes/svelte-5/step-bump-versions.ts
2169
- import { join as join10 } from "path";
2209
+ import { join as join11 } from "path";
2170
2210
  var SVELTE_5_VERSIONS = {
2171
2211
  svelte: "^5.55.5",
2172
2212
  "@sveltejs/kit": "^2.59.0",
@@ -2179,7 +2219,7 @@ var SVELTE_5_VERSIONS = {
2179
2219
  "typescript-svelte-plugin": "^0.3.52"
2180
2220
  };
2181
2221
  async function bumpToSvelte5Versions(cwd) {
2182
- const pkgPath = join10(cwd, "package.json");
2222
+ const pkgPath = join11(cwd, "package.json");
2183
2223
  const pkg = await readPackageJson(pkgPath);
2184
2224
  let next = pkg;
2185
2225
  for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
@@ -2191,8 +2231,8 @@ async function bumpToSvelte5Versions(cwd) {
2191
2231
  }
2192
2232
 
2193
2233
  // src/recipes/svelte-5/step-svelte-config.ts
2194
- import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
2195
- import { join as join11 } from "path";
2234
+ import { readFile as readFile11, writeFile as writeFile5 } from "fs/promises";
2235
+ import { join as join12 } from "path";
2196
2236
  var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
2197
2237
  var IMPORT_FROM_VITE_PLUGIN = new RegExp(
2198
2238
  String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
@@ -2233,10 +2273,10 @@ function dropPreprocessKey(source) {
2233
2273
  return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
2234
2274
  }
2235
2275
  async function migrateSvelteConfig(cwd) {
2236
- const path = join11(cwd, "svelte.config.js");
2276
+ const path = join12(cwd, "svelte.config.js");
2237
2277
  let src;
2238
2278
  try {
2239
- src = await readFile10(path, "utf-8");
2279
+ src = await readFile11(path, "utf-8");
2240
2280
  } catch {
2241
2281
  return false;
2242
2282
  }
@@ -2270,9 +2310,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
2270
2310
  }
2271
2311
 
2272
2312
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
2273
- import { join as join12 } from "path";
2313
+ import { join as join13 } from "path";
2274
2314
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
2275
- const pkg = await readPackageJson(join12(cwd, "package.json"));
2315
+ const pkg = await readPackageJson(join13(cwd, "package.json"));
2276
2316
  const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
2277
2317
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
2278
2318
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
@@ -2293,8 +2333,8 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
2293
2333
  }
2294
2334
 
2295
2335
  // src/recipes/svelte-5/step-gotchas.ts
2296
- import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
2297
- import { join as join13 } from "path";
2336
+ import { readFile as readFile12, writeFile as writeFile6 } from "fs/promises";
2337
+ import { join as join14 } from "path";
2298
2338
  import { glob as glob2 } from "tinyglobby";
2299
2339
 
2300
2340
  // src/recipes/svelte-5/codemods/on-event-to-handler.ts
@@ -2626,8 +2666,8 @@ async function planGotchaCodemods(cwd) {
2626
2666
  const changes = [];
2627
2667
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
2628
2668
  for (const rel of relPaths) {
2629
- const path = join13(cwd, rel);
2630
- const before = await readFile11(path, "utf-8");
2669
+ const path = join14(cwd, rel);
2670
+ const before = await readFile12(path, "utf-8");
2631
2671
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
2632
2672
  if (after !== before) changes.push({ rel, after });
2633
2673
  }
@@ -2636,7 +2676,7 @@ async function planGotchaCodemods(cwd) {
2636
2676
  async function applyGotchaCodemods(cwd) {
2637
2677
  const changes = await planGotchaCodemods(cwd);
2638
2678
  for (const c of changes) {
2639
- await writeFile6(join13(cwd, c.rel), c.after, "utf-8");
2679
+ await writeFile6(join14(cwd, c.rel), c.after, "utf-8");
2640
2680
  }
2641
2681
  return { filesChanged: changes.length };
2642
2682
  }
@@ -2660,7 +2700,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
2660
2700
 
2661
2701
  // src/recipes/svelte-5/step-summary.ts
2662
2702
  import { writeFile as writeFile7 } from "fs/promises";
2663
- import { join as join14 } from "path";
2703
+ import { join as join15 } from "path";
2664
2704
  async function writeMigrationSummary(input) {
2665
2705
  const lines = [
2666
2706
  `# Svelte 4 \u2192 5 migration summary`,
@@ -2677,7 +2717,7 @@ async function writeMigrationSummary(input) {
2677
2717
  `- Verify Playwright a11y tests still pass.`
2678
2718
  ];
2679
2719
  const content = lines.join("\n") + "\n";
2680
- const path = join14(input.cwd, "MIGRATION_SVELTE_5.md");
2720
+ const path = join15(input.cwd, "MIGRATION_SVELTE_5.md");
2681
2721
  await writeFile7(path, content, "utf-8");
2682
2722
  return path;
2683
2723
  }
@@ -2685,7 +2725,7 @@ async function writeMigrationSummary(input) {
2685
2725
  // src/recipes/svelte-5/index.ts
2686
2726
  async function alreadyOnSvelte5(cwd) {
2687
2727
  try {
2688
- const pkg = await readPackageJson(join15(cwd, "package.json"));
2728
+ const pkg = await readPackageJson(join16(cwd, "package.json"));
2689
2729
  const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
2690
2730
  return !!v && /^\^?5\./.test(v);
2691
2731
  } catch {
@@ -2780,7 +2820,7 @@ import { resolve as resolve6 } from "path";
2780
2820
 
2781
2821
  // src/recipes/convert-to-pnpm.ts
2782
2822
  import { rm as rm3, stat as stat3 } from "fs/promises";
2783
- import { join as join16 } from "path";
2823
+ import { join as join17 } from "path";
2784
2824
 
2785
2825
  // src/recipes/convert-to-pnpm/script-rewrites.ts
2786
2826
  function rewriteScriptForPnpm(script) {
@@ -2813,9 +2853,9 @@ async function exists2(path) {
2813
2853
  async function convertToPnpm(site, opts = {}) {
2814
2854
  const spawn2 = opts.spawn ?? defaultSpawn;
2815
2855
  const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
2816
- const pnpmLockPath = join16(site.path, "pnpm-lock.yaml");
2817
- const npmLockPath = join16(site.path, "package-lock.json");
2818
- const yarnLockPath = join16(site.path, "yarn.lock");
2856
+ const pnpmLockPath = join17(site.path, "pnpm-lock.yaml");
2857
+ const npmLockPath = join17(site.path, "package-lock.json");
2858
+ const yarnLockPath = join17(site.path, "yarn.lock");
2819
2859
  return withRecipe({
2820
2860
  name: "convert-to-pnpm",
2821
2861
  site,
@@ -2838,7 +2878,7 @@ async function convertToPnpm(site, opts = {}) {
2838
2878
  if (hasYarnLock) await rm3(yarnLockPath, { force: true });
2839
2879
  const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
2840
2880
  await commit2(`chore(pnpm): remove ${sourceLock}`);
2841
- const pkgPath = join16(cwd, "package.json");
2881
+ const pkgPath = join17(cwd, "package.json");
2842
2882
  const pkg = await readPackageJson(pkgPath);
2843
2883
  const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
2844
2884
  if (pkg.scripts && typeof pkg.scripts === "object") {
@@ -2851,7 +2891,7 @@ async function convertToPnpm(site, opts = {}) {
2851
2891
  }
2852
2892
  await writePackageJson(pkgPath, next);
2853
2893
  await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
2854
- await rm3(join16(cwd, "node_modules"), { recursive: true, force: true });
2894
+ await rm3(join17(cwd, "node_modules"), { recursive: true, force: true });
2855
2895
  const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
2856
2896
  if (installResult.code !== 0) {
2857
2897
  return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
@@ -2892,16 +2932,16 @@ import { resolve as resolve7 } from "path";
2892
2932
 
2893
2933
  // src/recipes/onboard.ts
2894
2934
  import { stat as stat4 } from "fs/promises";
2895
- import { join as join18 } from "path";
2935
+ import { join as join19 } from "path";
2896
2936
 
2897
2937
  // src/util/self-version.ts
2898
2938
  import { readFileSync } from "fs";
2899
2939
  import { fileURLToPath } from "url";
2900
- import { dirname, join as join17 } from "path";
2940
+ import { dirname, join as join18 } from "path";
2901
2941
  function selfPackageVersion(callerImportMetaUrl) {
2902
2942
  try {
2903
2943
  const here3 = dirname(fileURLToPath(callerImportMetaUrl));
2904
- const raw = readFileSync(join17(here3, "..", "..", "package.json"), "utf-8");
2944
+ const raw = readFileSync(join18(here3, "..", "..", "package.json"), "utf-8");
2905
2945
  const pkg = JSON.parse(raw);
2906
2946
  return pkg.version ?? "0.0.0";
2907
2947
  } catch {
@@ -2951,13 +2991,13 @@ async function onboard(site, opts = {}) {
2951
2991
  name: "onboard",
2952
2992
  site,
2953
2993
  plan: async () => {
2954
- if (!await exists3(join18(site.path, "pnpm-lock.yaml"))) {
2994
+ if (!await exists3(join19(site.path, "pnpm-lock.yaml"))) {
2955
2995
  return {
2956
2996
  kind: "failed",
2957
2997
  notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
2958
2998
  };
2959
2999
  }
2960
- const pkgPath = join18(site.path, "package.json");
3000
+ const pkgPath = join19(site.path, "package.json");
2961
3001
  const pkg = await readPackageJson(pkgPath);
2962
3002
  const toAdd = [];
2963
3003
  if (!isDeclared(pkg, PACKAGE_NAME)) {
@@ -2977,7 +3017,7 @@ async function onboard(site, opts = {}) {
2977
3017
  return { kind: "apply", plan: { pkg, toAdd } };
2978
3018
  },
2979
3019
  apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
2980
- const pkgPath = join18(cwd, "package.json");
3020
+ const pkgPath = join19(cwd, "package.json");
2981
3021
  let next = pkg;
2982
3022
  for (const dep of toAdd) {
2983
3023
  next = bumpDep(next, dep.name, dep.version);
@@ -3046,7 +3086,7 @@ import { resolve as resolve8 } from "path";
3046
3086
 
3047
3087
  // src/recipes/svelte-codemods.ts
3048
3088
  import { writeFile as writeFile8 } from "fs/promises";
3049
- import { join as join19 } from "path";
3089
+ import { join as join20 } from "path";
3050
3090
  async function svelteCodemods(site) {
3051
3091
  return withRecipe({
3052
3092
  name: "svelte-codemods",
@@ -3060,7 +3100,7 @@ async function svelteCodemods(site) {
3060
3100
  },
3061
3101
  apply: async (changes, { commit: commit2, cwd }) => {
3062
3102
  for (const c of changes) {
3063
- await writeFile8(join19(cwd, c.rel), c.after, "utf-8");
3103
+ await writeFile8(join20(cwd, c.rel), c.after, "utf-8");
3064
3104
  }
3065
3105
  await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
3066
3106
  return { kind: "ok" };
@@ -3284,10 +3324,10 @@ async function runSingleSiteDraft(slug, opts) {
3284
3324
 
3285
3325
  // src/cli/version.ts
3286
3326
  import { readFileSync as readFileSync2 } from "fs";
3287
- import { join as join21 } from "path";
3327
+ import { join as join22 } from "path";
3288
3328
  function resolvePackageVersion(fromDir) {
3289
3329
  try {
3290
- const raw = readFileSync2(join21(fromDir, "..", "..", "package.json"), "utf-8");
3330
+ const raw = readFileSync2(join22(fromDir, "..", "..", "package.json"), "utf-8");
3291
3331
  const pkg = JSON.parse(raw);
3292
3332
  return pkg.version ?? "unknown";
3293
3333
  } catch {