@reddoorla/maintenance 0.6.6 → 0.6.8

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/cli/bin.js CHANGED
@@ -41,6 +41,11 @@ var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve9, reject) => {
41
41
  import { readFile } from "fs/promises";
42
42
  import { join } from "path";
43
43
 
44
+ // src/util/site.ts
45
+ function siteLabel(site) {
46
+ return site.name ?? site.path;
47
+ }
48
+
44
49
  // src/configs/baseline-versions.ts
45
50
  var baselineVersions = {
46
51
  // SvelteKit core
@@ -81,9 +86,6 @@ var baselineVersions = {
81
86
  };
82
87
 
83
88
  // src/audits/deps.ts
84
- function siteLabel(site) {
85
- return site.name ?? site.path;
86
- }
87
89
  function stripCaret(range) {
88
90
  return range.replace(/^[\^~]/, "");
89
91
  }
@@ -156,9 +158,6 @@ import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "
156
158
  import { glob } from "tinyglobby";
157
159
  var TARGET_GLOBS = ["**/*.{ts,js,svelte}"];
158
160
  var IGNORE = ["node_modules/**", "dist/**", ".svelte-kit/**", "build/**", ".netlify/**"];
159
- function siteLabel2(site) {
160
- return site.name ?? site.path;
161
- }
162
161
  async function listFiles(cwd) {
163
162
  return glob(TARGET_GLOBS, { cwd, ignore: IGNORE, absolute: false });
164
163
  }
@@ -168,7 +167,7 @@ async function lintAudit(ctx) {
168
167
  if (!existsSync(configPath)) {
169
168
  return {
170
169
  audit: "lint",
171
- site: siteLabel2(site),
170
+ site: siteLabel(site),
172
171
  status: "skip",
173
172
  summary: "no eslint config at site root"
174
173
  };
@@ -194,7 +193,7 @@ async function lintAudit(ctx) {
194
193
  const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
195
194
  return {
196
195
  audit: "lint",
197
- site: siteLabel2(site),
196
+ site: siteLabel(site),
198
197
  status,
199
198
  summary,
200
199
  details: {
@@ -207,9 +206,6 @@ async function lintAudit(ctx) {
207
206
  }
208
207
 
209
208
  // src/audits/security.ts
210
- function siteLabel3(site) {
211
- return site.name ?? site.path;
212
- }
213
209
  function classify(v) {
214
210
  if (v.critical > 0 || v.high > 0) return "fail";
215
211
  if (v.moderate > 0 || v.low > 0) return "warn";
@@ -295,7 +291,8 @@ async function runAuditTool(spawn2, cmd, args, cwd) {
295
291
  if (errEnvelope && typeof errEnvelope === "object") {
296
292
  return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
297
293
  }
298
- if (!parsed.metadata?.vulnerabilities) {
294
+ const vulnsMeta = parsed.metadata?.vulnerabilities;
295
+ if (!vulnsMeta || Object.keys(vulnsMeta).length === 0) {
299
296
  return { kind: "error", reason: "no metadata.vulnerabilities in output" };
300
297
  }
301
298
  return { kind: "ok", parsed };
@@ -303,7 +300,7 @@ async function runAuditTool(spawn2, cmd, args, cwd) {
303
300
  async function securityAudit(ctx) {
304
301
  const spawn2 = ctx.spawn ?? defaultSpawn;
305
302
  const site = ctx.site;
306
- const label = siteLabel3(site);
303
+ const label = siteLabel(site);
307
304
  let used = "pnpm audit";
308
305
  let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
309
306
  if (result.kind !== "ok") {
@@ -384,9 +381,6 @@ var lighthouseConfig = {
384
381
  };
385
382
 
386
383
  // src/audits/lighthouse.ts
387
- function siteLabel4(site) {
388
- return site.name ?? site.path;
389
- }
390
384
  async function readJsonMaybe(path) {
391
385
  try {
392
386
  const raw = await readFile3(path, "utf-8");
@@ -424,7 +418,7 @@ function messageForAssertion(a) {
424
418
  async function lighthouseAudit(ctx) {
425
419
  const spawn2 = ctx.spawn ?? defaultSpawn;
426
420
  const site = ctx.site;
427
- const label = siteLabel4(site);
421
+ const label = siteLabel(site);
428
422
  const configDir = await mkdtemp(join3(tmpdir(), "reddoor-lhci-"));
429
423
  const configPath = join3(configDir, "lighthouserc.json");
430
424
  await writeFile(configPath, JSON.stringify(lighthouseConfig), "utf-8");
@@ -522,9 +516,6 @@ var playwrightA11yConfig = defineConfig({
522
516
 
523
517
  // src/audits/a11y.ts
524
518
  var RESULTS_REL = ".reddoor-a11y/results.json";
525
- function siteLabel5(site) {
526
- return site.name ?? site.path;
527
- }
528
519
  async function readJsonMaybe2(path) {
529
520
  try {
530
521
  const raw = await readFile4(path, "utf-8");
@@ -582,7 +573,7 @@ test("a11y across configured routes", async ({ page }) => {
582
573
  async function a11yAudit(ctx) {
583
574
  const spawn2 = ctx.spawn ?? defaultSpawn;
584
575
  const site = ctx.site;
585
- const label = siteLabel5(site);
576
+ const label = siteLabel(site);
586
577
  const specDir = await mkdtemp2(join4(tmpdir2(), "reddoor-a11y-spec-"));
587
578
  const specPath = join4(specDir, "a11y.spec.ts");
588
579
  await writeFile2(specPath, buildSpec(), "utf-8");
@@ -1007,9 +998,8 @@ async function git(cwd, args) {
1007
998
  return exec("git", args, { cwd, env: process.env });
1008
999
  }
1009
1000
  function branchName(recipe, when = /* @__PURE__ */ new Date()) {
1010
- const iso = when.toISOString().replace(/[-:.]/g, "").replace(/Z$/, "Z");
1011
- const trimmed = iso.replace(/(\d{8}T\d{6})\d+(Z)$/, "$1$2");
1012
- return `maint/${recipe}-${trimmed}`;
1001
+ const compact = when.toISOString().replace(/[-:.]/g, "");
1002
+ return `maint/${recipe}-${compact}`;
1013
1003
  }
1014
1004
  async function isWorkingTreeClean(cwd) {
1015
1005
  const { stdout } = await git(cwd, ["status", "--porcelain"]);
@@ -1038,6 +1028,65 @@ async function commit(cwd, message) {
1038
1028
  return sha.trim();
1039
1029
  }
1040
1030
 
1031
+ // src/recipes/_with-recipe.ts
1032
+ async function withRecipe(body) {
1033
+ const label = siteLabel(body.site);
1034
+ if (body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
1035
+ throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
1036
+ }
1037
+ const planned = await body.plan();
1038
+ if (planned.kind === "noop") {
1039
+ return {
1040
+ recipe: body.name,
1041
+ site: label,
1042
+ status: "noop",
1043
+ commits: [],
1044
+ ...planned.notes ? { notes: planned.notes } : {}
1045
+ };
1046
+ }
1047
+ if (planned.kind === "failed") {
1048
+ return {
1049
+ recipe: body.name,
1050
+ site: label,
1051
+ status: "failed",
1052
+ commits: [],
1053
+ notes: planned.notes
1054
+ };
1055
+ }
1056
+ if (!body.checkTreeFirst && !await isWorkingTreeClean(body.site.path)) {
1057
+ throw new Error(`refusing to run: working tree is not clean at ${body.site.path}`);
1058
+ }
1059
+ const branch = branchName(body.name);
1060
+ await createBranch(body.site.path, branch);
1061
+ const shas = [];
1062
+ const result = await body.apply(planned.plan, {
1063
+ cwd: body.site.path,
1064
+ branch,
1065
+ commit: async (msg) => {
1066
+ const sha = await commit(body.site.path, msg);
1067
+ if (sha) shas.push(sha);
1068
+ return sha;
1069
+ }
1070
+ });
1071
+ if (result.kind === "failed") {
1072
+ return {
1073
+ recipe: body.name,
1074
+ site: label,
1075
+ status: "failed",
1076
+ commits: shas,
1077
+ notes: result.notes
1078
+ };
1079
+ }
1080
+ const notes = result.notes ? `${result.notes}; branch: ${branch}` : `branch: ${branch}`;
1081
+ return {
1082
+ recipe: body.name,
1083
+ site: label,
1084
+ status: shas.length > 0 ? "applied" : "noop",
1085
+ commits: shas,
1086
+ notes
1087
+ };
1088
+ }
1089
+
1041
1090
  // src/recipes/sync-configs.ts
1042
1091
  var GITIGNORE_CONFIG = "gitignore";
1043
1092
  var ALL_CONFIG_NAMES = [
@@ -1051,9 +1100,6 @@ var ALL_CONFIG_NAMES = [
1051
1100
  function isConfigName(value) {
1052
1101
  return ALL_CONFIG_NAMES.includes(value);
1053
1102
  }
1054
- function siteLabel6(site) {
1055
- return site.name ?? site.path;
1056
- }
1057
1103
  async function readMaybe(path) {
1058
1104
  try {
1059
1105
  return await readFile6(path, "utf-8");
@@ -1084,48 +1130,33 @@ async function applyGitignore(cwd, plan) {
1084
1130
  }
1085
1131
  }
1086
1132
  async function syncConfigs(site, opts = {}) {
1087
- const label = siteLabel6(site);
1088
1133
  const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);
1089
1134
  const templateNames = requested.filter((c) => c !== GITIGNORE_CONFIG);
1090
1135
  const templates = templatesByName(templateNames);
1091
1136
  const includeGitignore = requested.includes(GITIGNORE_CONFIG);
1092
- const templateDiffs = await planTemplateDiffs(site.path, templates);
1093
- const gitignorePlan = includeGitignore ? await planGitignore(site.path) : { kind: "noop" };
1094
- if (templateDiffs.length === 0 && gitignorePlan.kind === "noop") {
1095
- return {
1096
- recipe: "sync-configs",
1097
- site: label,
1098
- status: "noop",
1099
- commits: [],
1100
- notes: "all targeted configs already match"
1101
- };
1102
- }
1103
- if (!await isWorkingTreeClean(site.path)) {
1104
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1105
- }
1106
- const branch = branchName("sync-configs");
1107
- await createBranch(site.path, branch);
1108
- const shas = [];
1109
- for (const t of templateDiffs) {
1110
- await writeFile3(join6(site.path, t.path), t.contents, "utf-8");
1111
- const sha = await commit(
1112
- site.path,
1113
- `chore: sync ${t.config} config from @reddoorla/maintenance`
1114
- );
1115
- if (sha) shas.push(sha);
1116
- }
1117
- if (gitignorePlan.kind === "apply") {
1118
- await applyGitignore(site.path, gitignorePlan);
1119
- const sha = await commit(site.path, `chore: sync gitignore from @reddoorla/maintenance`);
1120
- if (sha) shas.push(sha);
1121
- }
1122
- return {
1123
- recipe: "sync-configs",
1124
- site: label,
1125
- status: "applied",
1126
- commits: shas,
1127
- notes: `branch: ${branch}`
1128
- };
1137
+ return withRecipe({
1138
+ name: "sync-configs",
1139
+ site,
1140
+ plan: async () => {
1141
+ const templateDiffs = await planTemplateDiffs(site.path, templates);
1142
+ const gitignorePlan = includeGitignore ? await planGitignore(site.path) : { kind: "noop" };
1143
+ if (templateDiffs.length === 0 && gitignorePlan.kind === "noop") {
1144
+ return { kind: "noop", notes: "all targeted configs already match" };
1145
+ }
1146
+ return { kind: "apply", plan: { templateDiffs, gitignorePlan } };
1147
+ },
1148
+ apply: async ({ templateDiffs, gitignorePlan }, { commit: commit2 }) => {
1149
+ for (const t of templateDiffs) {
1150
+ await writeFile3(join6(site.path, t.path), t.contents, "utf-8");
1151
+ await commit2(`chore: sync ${t.config} config from @reddoorla/maintenance`);
1152
+ }
1153
+ if (gitignorePlan.kind === "apply") {
1154
+ await applyGitignore(site.path, gitignorePlan);
1155
+ await commit2(`chore: sync gitignore from @reddoorla/maintenance`);
1156
+ }
1157
+ return { kind: "ok" };
1158
+ }
1159
+ });
1129
1160
  }
1130
1161
 
1131
1162
  // src/cli/commands/sync-configs.ts
@@ -1209,9 +1240,6 @@ import { resolve as resolve4 } from "path";
1209
1240
  // src/recipes/bump-deps.ts
1210
1241
  import { stat as stat2 } from "fs/promises";
1211
1242
  import { join as join8 } from "path";
1212
- function siteLabel7(site) {
1213
- return site.name ?? site.path;
1214
- }
1215
1243
  async function exists(path) {
1216
1244
  try {
1217
1245
  await stat2(path);
@@ -1230,62 +1258,51 @@ function upFlagsForGroup(group) {
1230
1258
  return [];
1231
1259
  }
1232
1260
  async function bumpDeps(site, opts = {}) {
1233
- const label = siteLabel7(site);
1234
1261
  const group = opts.group ?? "minor";
1235
1262
  const spawn2 = opts.spawn ?? defaultSpawn;
1236
- const hasPnpmLock = await exists(join8(site.path, "pnpm-lock.yaml"));
1237
- if (!hasPnpmLock) {
1238
- const hasNpmLock = await exists(join8(site.path, "package-lock.json"));
1239
- const hasYarnLock = await exists(join8(site.path, "yarn.lock"));
1240
- if (hasNpmLock || hasYarnLock) {
1241
- const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
1242
- return {
1243
- recipe: "bump-deps",
1244
- site: label,
1245
- status: "failed",
1246
- commits: [],
1247
- notes: `site has ${competing} but no pnpm-lock.yaml \u2014 run convert-to-pnpm first`
1248
- };
1263
+ return withRecipe({
1264
+ name: "bump-deps",
1265
+ site,
1266
+ // pnpm install (in plan) mutates the lockfile, so the clean-tree check
1267
+ // MUST happen first — otherwise a desynced-lockfile resync would silently
1268
+ // land on top of whatever else was in the tree.
1269
+ checkTreeFirst: true,
1270
+ plan: async () => {
1271
+ const hasPnpmLock = await exists(join8(site.path, "pnpm-lock.yaml"));
1272
+ if (!hasPnpmLock) {
1273
+ const hasNpmLock = await exists(join8(site.path, "package-lock.json"));
1274
+ const hasYarnLock = await exists(join8(site.path, "yarn.lock"));
1275
+ if (hasNpmLock || hasYarnLock) {
1276
+ const competing = hasNpmLock ? "package-lock.json" : "yarn.lock";
1277
+ return {
1278
+ kind: "failed",
1279
+ notes: `site has ${competing} but no pnpm-lock.yaml \u2014 run convert-to-pnpm first`
1280
+ };
1281
+ }
1282
+ }
1283
+ await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
1284
+ const outdated = await spawn2(
1285
+ "pnpm",
1286
+ ["outdated", "--json", ...outdatedFlagsForGroup(group)],
1287
+ { cwd: site.path }
1288
+ );
1289
+ let parsed;
1290
+ try {
1291
+ parsed = JSON.parse(outdated.stdout || "{}");
1292
+ } catch {
1293
+ parsed = {};
1294
+ }
1295
+ if (Object.keys(parsed).length === 0) {
1296
+ return { kind: "noop", notes: `pnpm outdated reported nothing for group=${group}` };
1297
+ }
1298
+ return { kind: "apply", plan: { group } };
1299
+ },
1300
+ apply: async ({ group: g }, { commit: commit2, cwd }) => {
1301
+ await spawn2("pnpm", ["up", ...upFlagsForGroup(g)], { cwd, streaming: true });
1302
+ await commit2(`chore(deps): bump dependencies (${g})`);
1303
+ return { kind: "ok" };
1249
1304
  }
1250
- }
1251
- if (!await isWorkingTreeClean(site.path)) {
1252
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1253
- }
1254
- await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
1255
- const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
1256
- cwd: site.path
1257
1305
  });
1258
- let parsed;
1259
- try {
1260
- parsed = JSON.parse(outdated.stdout || "{}");
1261
- } catch {
1262
- parsed = {};
1263
- }
1264
- const nothingToDo = Object.keys(parsed).length === 0;
1265
- if (nothingToDo) {
1266
- return {
1267
- recipe: "bump-deps",
1268
- site: label,
1269
- status: "noop",
1270
- commits: [],
1271
- notes: `pnpm outdated reported nothing for group=${group}`
1272
- };
1273
- }
1274
- const branch = branchName("bump-deps");
1275
- await createBranch(site.path, branch);
1276
- await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
1277
- cwd: site.path,
1278
- streaming: true
1279
- });
1280
- const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
1281
- const shas = sha ? [sha] : [];
1282
- return {
1283
- recipe: "bump-deps",
1284
- site: label,
1285
- status: shas.length > 0 ? "applied" : "noop",
1286
- commits: shas,
1287
- notes: `branch: ${branch}`
1288
- };
1289
1306
  }
1290
1307
 
1291
1308
  // src/cli/commands/bump-deps.ts
@@ -1507,6 +1524,34 @@ import { glob as glob2 } from "tinyglobby";
1507
1524
  // src/recipes/svelte-5/codemods/on-event-to-handler.ts
1508
1525
  var SCRIPT_BLOCK = /<script\b[^>]*>[\s\S]*?<\/script>/g;
1509
1526
  var SIMPLE_ON_EVENT = /\bon:([a-z]+)(?=\s*=)/g;
1527
+ var MODIFIER_EVENT = /\bon:[a-z]+\|[a-zA-Z]+(?=\s*=)/g;
1528
+ function flagEventModifiers(source) {
1529
+ const insertions = [];
1530
+ let m;
1531
+ MODIFIER_EVENT.lastIndex = 0;
1532
+ while ((m = MODIFIER_EVENT.exec(source)) !== null) {
1533
+ const tagStart = source.lastIndexOf("<", m.index);
1534
+ if (tagStart === -1) continue;
1535
+ const prevLineEnd = tagStart - 1;
1536
+ if (prevLineEnd >= 0) {
1537
+ const prevLineStart = source.lastIndexOf("\n", prevLineEnd - 1) + 1;
1538
+ const prevLine = source.slice(prevLineStart, prevLineEnd + 1);
1539
+ if (/<!--\s*@migration-task/.test(prevLine)) continue;
1540
+ }
1541
+ const lineStart = source.lastIndexOf("\n", tagStart - 1) + 1;
1542
+ const indent = source.slice(lineStart, tagStart);
1543
+ const safeIndent = /^[ \t]*$/.test(indent) ? indent : "";
1544
+ insertions.push({ tagStart, indent: safeIndent, modifier: m[0] });
1545
+ }
1546
+ let out = source;
1547
+ for (let i = insertions.length - 1; i >= 0; i--) {
1548
+ const { tagStart, indent, modifier } = insertions[i];
1549
+ const comment = `<!-- @migration-task: Svelte 5 removed event modifier syntax (\`${modifier}\`). Rewrite inline, e.g. onclick={(e) => { e.preventDefault(); ... }}. -->
1550
+ ${indent}`;
1551
+ out = out.slice(0, tagStart) + comment + out.slice(tagStart);
1552
+ }
1553
+ return out;
1554
+ }
1510
1555
  function onEventToHandler(source) {
1511
1556
  const masked = [];
1512
1557
  const placeholder = (i) => ` SCRIPT_${i} `;
@@ -1514,8 +1559,9 @@ function onEventToHandler(source) {
1514
1559
  masked.push(match);
1515
1560
  return placeholder(masked.length - 1);
1516
1561
  });
1517
- const rewritten = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
1518
- let out = rewritten;
1562
+ let processed = intermediate.replace(SIMPLE_ON_EVENT, (_full, name) => `on${name}`);
1563
+ processed = flagEventModifiers(processed);
1564
+ let out = processed;
1519
1565
  masked.forEach((blk, i) => {
1520
1566
  out = out.replace(placeholder(i), blk);
1521
1567
  });
@@ -1565,6 +1611,22 @@ function exportLetToProps(source) {
1565
1611
  return source.replace(SCRIPT_BLOCK2, (full) => full.replace(inner, body));
1566
1612
  }
1567
1613
 
1614
+ // src/util/svelte-source.ts
1615
+ function findStringEnd(source, openIdx) {
1616
+ const quote = source[openIdx];
1617
+ let i = openIdx + 1;
1618
+ while (i < source.length) {
1619
+ const ch = source[i];
1620
+ if (ch === "\\") {
1621
+ i += 2;
1622
+ continue;
1623
+ }
1624
+ if (ch === quote) return i;
1625
+ i++;
1626
+ }
1627
+ return -1;
1628
+ }
1629
+
1568
1630
  // src/recipes/svelte-5/codemods/dollar-restprops.ts
1569
1631
  function removeInterfaceBlock(source) {
1570
1632
  const re = /^\s*interface\s+\$\$Props\s*\{/m;
@@ -1588,20 +1650,6 @@ function removeInterfaceBlock(source) {
1588
1650
  out = out.slice(0, match.index) + out.slice(endIdx);
1589
1651
  }
1590
1652
  }
1591
- function findStringEnd(source, openIdx) {
1592
- const quote = source[openIdx];
1593
- let i = openIdx + 1;
1594
- while (i < source.length) {
1595
- const ch = source[i];
1596
- if (ch === "\\") {
1597
- i += 2;
1598
- continue;
1599
- }
1600
- if (ch === quote) return i;
1601
- i++;
1602
- }
1603
- return -1;
1604
- }
1605
1653
  function maskStringLiterals(source) {
1606
1654
  const strings = [];
1607
1655
  let out = "";
@@ -1678,7 +1726,8 @@ function stateEffectSyncToDerived(source) {
1678
1726
 
1679
1727
  // src/recipes/svelte-5/codemods/dollar-props-class.ts
1680
1728
  var PROPS_DESTRUCTURE = /let\s*\{([\s\S]*?)\}(\s*:\s*\{([\s\S]*?)\})?\s*=\s*\$props\(\)/;
1681
- var DOLLAR_PROPS_CLASS = /\$\$props\.class\b/g;
1729
+ var HAS_DOLLAR_PROPS_CLASS = /\$\$props\.class\b/;
1730
+ var DOLLAR_PROPS_CLASS_GLOBAL = /\$\$props\.class\b/g;
1682
1731
  var DOLLAR_PROPS_ANY = /\$\$props\b/;
1683
1732
  var SCRIPT_BLOCK4 = /<script\b[^>]*>[\s\S]*?<\/script>/g;
1684
1733
  var MIGRATION_TASK = /^<!--\s*@migration-task[\s\S]*?-->\s*\n?/gm;
@@ -1700,8 +1749,7 @@ function restoreScripts(masked, blocks) {
1700
1749
  }
1701
1750
  function dollarPropsClass(source) {
1702
1751
  const { masked } = maskScripts(source);
1703
- if (!DOLLAR_PROPS_CLASS.test(masked)) return source;
1704
- DOLLAR_PROPS_CLASS.lastIndex = 0;
1752
+ if (!HAS_DOLLAR_PROPS_CLASS.test(masked)) return source;
1705
1753
  if (!PROPS_DESTRUCTURE.test(source)) return source;
1706
1754
  let updated = source.replace(PROPS_DESTRUCTURE, (full, body, typeAnno, typeBody) => {
1707
1755
  if (/\bclass\s*:/.test(body)) return full;
@@ -1715,7 +1763,7 @@ function dollarPropsClass(source) {
1715
1763
  return `let { ${newBody} } = $props()`;
1716
1764
  });
1717
1765
  const reMasked = maskScripts(updated);
1718
- const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS, IDENT);
1766
+ const templateRewritten = reMasked.masked.replace(DOLLAR_PROPS_CLASS_GLOBAL, IDENT);
1719
1767
  updated = restoreScripts(templateRewritten, reMasked.blocks);
1720
1768
  const stripped = updated.replace(MIGRATION_TASK, "");
1721
1769
  if (!DOLLAR_PROPS_ANY.test(stripped)) {
@@ -1734,7 +1782,7 @@ function findMatchingClose(source, openIdx) {
1734
1782
  while (i < source.length) {
1735
1783
  const ch = source[i];
1736
1784
  if (ch === '"' || ch === "'" || ch === "`") {
1737
- const closeStr = findStringClose(source, i);
1785
+ const closeStr = findStringEnd(source, i);
1738
1786
  if (closeStr === -1) return -1;
1739
1787
  i = closeStr + 1;
1740
1788
  continue;
@@ -1748,20 +1796,6 @@ function findMatchingClose(source, openIdx) {
1748
1796
  }
1749
1797
  return -1;
1750
1798
  }
1751
- function findStringClose(source, openIdx) {
1752
- const quote = source[openIdx];
1753
- let i = openIdx + 1;
1754
- while (i < source.length) {
1755
- const ch = source[i];
1756
- if (ch === "\\") {
1757
- i += 2;
1758
- continue;
1759
- }
1760
- if (ch === quote) return i;
1761
- i++;
1762
- }
1763
- return -1;
1764
- }
1765
1799
  var MIGRATION_MARKER = "// @migration-task: $effect won't trigger UI updates on plain `let` bindings \u2014 refine mutated locals to $state or split into per-variable $derived.";
1766
1800
  function transformBlocks(body) {
1767
1801
  const out = [];
@@ -1873,9 +1907,6 @@ async function writeMigrationSummary(input) {
1873
1907
  }
1874
1908
 
1875
1909
  // src/recipes/svelte-5/index.ts
1876
- function siteLabel8(site) {
1877
- return site.name ?? site.path;
1878
- }
1879
1910
  async function alreadyOnSvelte5(cwd) {
1880
1911
  try {
1881
1912
  const pkg = await readPackageJson(join14(cwd, "package.json"));
@@ -1886,72 +1917,49 @@ async function alreadyOnSvelte5(cwd) {
1886
1917
  }
1887
1918
  }
1888
1919
  async function upgradeSvelte4to5(site, opts = {}) {
1889
- const label = siteLabel8(site);
1890
1920
  const spawn2 = opts.spawn ?? defaultSpawn;
1891
- if (await alreadyOnSvelte5(site.path)) {
1892
- return {
1893
- recipe: "svelte-4-to-5",
1894
- site: label,
1895
- status: "noop",
1896
- commits: [],
1897
- notes: "site already declares svelte ^5.x"
1898
- };
1899
- }
1900
- if (!await isWorkingTreeClean(site.path)) {
1901
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1902
- }
1903
- const branch = branchName("svelte-4-to-5");
1904
- await createBranch(site.path, branch);
1905
- const shas = [];
1906
- const bumped = await bumpToSvelte5Versions(site.path);
1907
- if (bumped) {
1908
- const sha = await commit(site.path, "chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte");
1909
- if (sha) shas.push(sha);
1910
- }
1911
- const configChanged = await migrateSvelteConfig(site.path);
1912
- if (configChanged) {
1913
- const sha = await commit(
1914
- site.path,
1915
- "refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)"
1916
- );
1917
- if (sha) shas.push(sha);
1918
- }
1919
- const migrate = await runSvelteMigrate(site.path, spawn2);
1920
- if (migrate.ran) {
1921
- const sha = await commit(site.path, "refactor(svelte5): run official svelte-migrate codemod");
1922
- if (sha) shas.push(sha);
1923
- }
1924
- const tw = await upgradeTailwind(site.path, spawn2);
1925
- if (tw.ran) {
1926
- const sha = await commit(site.path, "chore(svelte5): tailwindcss 3 \u2192 4 upgrade");
1927
- if (sha) shas.push(sha);
1928
- }
1929
- const codemods = await applyGotchaCodemods(site.path);
1930
- if (codemods.filesChanged > 0) {
1931
- const sha = await commit(
1932
- site.path,
1933
- `refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`
1934
- );
1935
- if (sha) shas.push(sha);
1936
- }
1937
- await verifyMigration(site.path, spawn2);
1938
- const verifySha = await commit(site.path, "chore(svelte5): pnpm install + check");
1939
- if (verifySha) shas.push(verifySha);
1940
- await writeMigrationSummary({
1941
- cwd: site.path,
1942
- filesChangedByCodemods: codemods.filesChanged,
1943
- svelteMigrateRan: migrate.ran,
1944
- tailwindUpgraded: tw.ran
1921
+ return withRecipe({
1922
+ name: "svelte-4-to-5",
1923
+ site,
1924
+ plan: async () => {
1925
+ if (await alreadyOnSvelte5(site.path)) {
1926
+ return { kind: "noop", notes: "site already declares svelte ^5.x" };
1927
+ }
1928
+ return { kind: "apply", plan: true };
1929
+ },
1930
+ apply: async (_plan, { commit: commit2, cwd }) => {
1931
+ const bumped = await bumpToSvelte5Versions(cwd);
1932
+ if (bumped) {
1933
+ await commit2("chore(svelte5): bump svelte/kit/vite/vite-plugin-svelte");
1934
+ }
1935
+ const configChanged = await migrateSvelteConfig(cwd);
1936
+ if (configChanged) {
1937
+ await commit2("refactor(svelte5): migrate svelte.config.js (drop vitePreprocess)");
1938
+ }
1939
+ const migrate = await runSvelteMigrate(cwd, spawn2);
1940
+ if (migrate.ran) {
1941
+ await commit2("refactor(svelte5): run official svelte-migrate codemod");
1942
+ }
1943
+ const tw = await upgradeTailwind(cwd, spawn2);
1944
+ if (tw.ran) {
1945
+ await commit2("chore(svelte5): tailwindcss 3 \u2192 4 upgrade");
1946
+ }
1947
+ const codemods = await applyGotchaCodemods(cwd);
1948
+ if (codemods.filesChanged > 0) {
1949
+ await commit2(`refactor(svelte5): apply gotcha codemods (${codemods.filesChanged} files)`);
1950
+ }
1951
+ await verifyMigration(cwd, spawn2);
1952
+ await commit2("chore(svelte5): pnpm install + check");
1953
+ await writeMigrationSummary({
1954
+ cwd,
1955
+ filesChangedByCodemods: codemods.filesChanged,
1956
+ svelteMigrateRan: migrate.ran,
1957
+ tailwindUpgraded: tw.ran
1958
+ });
1959
+ await commit2("docs(svelte5): add MIGRATION_SVELTE_5.md summary");
1960
+ return { kind: "ok" };
1961
+ }
1945
1962
  });
1946
- const summarySha = await commit(site.path, "docs(svelte5): add MIGRATION_SVELTE_5.md summary");
1947
- if (summarySha) shas.push(summarySha);
1948
- return {
1949
- recipe: "svelte-4-to-5",
1950
- site: label,
1951
- status: shas.length > 0 ? "applied" : "noop",
1952
- commits: shas,
1953
- notes: `branch: ${branch}`
1954
- };
1955
1963
  }
1956
1964
 
1957
1965
  // src/cli/commands/upgrade.ts
@@ -2026,84 +2034,56 @@ async function exists2(path) {
2026
2034
  return false;
2027
2035
  }
2028
2036
  }
2029
- function siteLabel9(site) {
2030
- return site.name ?? site.path;
2031
- }
2032
2037
  async function convertToPnpm(site, opts = {}) {
2033
- const label = siteLabel9(site);
2034
2038
  const spawn2 = opts.spawn ?? defaultSpawn;
2035
2039
  const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
2036
2040
  const pnpmLockPath = join15(site.path, "pnpm-lock.yaml");
2037
2041
  const npmLockPath = join15(site.path, "package-lock.json");
2038
2042
  const yarnLockPath = join15(site.path, "yarn.lock");
2039
- if (await exists2(pnpmLockPath)) {
2040
- return {
2041
- recipe: "convert-to-pnpm",
2042
- site: label,
2043
- status: "noop",
2044
- commits: [],
2045
- notes: "site already has pnpm-lock.yaml"
2046
- };
2047
- }
2048
- const hasNpmLock = await exists2(npmLockPath);
2049
- const hasYarnLock = await exists2(yarnLockPath);
2050
- if (!hasNpmLock && !hasYarnLock) {
2051
- return {
2052
- recipe: "convert-to-pnpm",
2053
- site: label,
2054
- status: "noop",
2055
- commits: [],
2056
- notes: "no convertible lockfile (package-lock.json or yarn.lock) at site root"
2057
- };
2058
- }
2059
- if (!await isWorkingTreeClean(site.path)) {
2060
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
2061
- }
2062
- const branch = branchName("convert-to-pnpm");
2063
- await createBranch(site.path, branch);
2064
- const shas = [];
2065
- if (hasNpmLock) await rm3(npmLockPath, { force: true });
2066
- if (hasYarnLock) await rm3(yarnLockPath, { force: true });
2067
- const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
2068
- const lockSha = await commit(site.path, `chore(pnpm): remove ${sourceLock}`);
2069
- if (lockSha) shas.push(lockSha);
2070
- const pkgPath = join15(site.path, "package.json");
2071
- const pkg = await readPackageJson(pkgPath);
2072
- const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
2073
- if (pkg.scripts && typeof pkg.scripts === "object") {
2074
- const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(
2075
- pkg.scripts
2076
- );
2077
- if (changedCount > 0) {
2078
- next.scripts = rewritten;
2043
+ return withRecipe({
2044
+ name: "convert-to-pnpm",
2045
+ site,
2046
+ plan: async () => {
2047
+ if (await exists2(pnpmLockPath)) {
2048
+ return { kind: "noop", notes: "site already has pnpm-lock.yaml" };
2049
+ }
2050
+ const hasNpmLock = await exists2(npmLockPath);
2051
+ const hasYarnLock = await exists2(yarnLockPath);
2052
+ if (!hasNpmLock && !hasYarnLock) {
2053
+ return {
2054
+ kind: "noop",
2055
+ notes: "no convertible lockfile (package-lock.json or yarn.lock) at site root"
2056
+ };
2057
+ }
2058
+ return { kind: "apply", plan: { hasNpmLock, hasYarnLock } };
2059
+ },
2060
+ apply: async ({ hasNpmLock, hasYarnLock }, { commit: commit2, cwd }) => {
2061
+ if (hasNpmLock) await rm3(npmLockPath, { force: true });
2062
+ if (hasYarnLock) await rm3(yarnLockPath, { force: true });
2063
+ const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
2064
+ await commit2(`chore(pnpm): remove ${sourceLock}`);
2065
+ const pkgPath = join15(cwd, "package.json");
2066
+ const pkg = await readPackageJson(pkgPath);
2067
+ const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
2068
+ if (pkg.scripts && typeof pkg.scripts === "object") {
2069
+ const { scripts: rewritten, changedCount } = rewriteScriptsForPnpm(
2070
+ pkg.scripts
2071
+ );
2072
+ if (changedCount > 0) {
2073
+ next.scripts = rewritten;
2074
+ }
2075
+ }
2076
+ await writePackageJson(pkgPath, next);
2077
+ await commit2("chore(pnpm): pin packageManager + rewrite npm scripts");
2078
+ await rm3(join15(cwd, "node_modules"), { recursive: true, force: true });
2079
+ const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
2080
+ if (installResult.code !== 0) {
2081
+ return { kind: "failed", notes: `pnpm install failed (exit ${installResult.code})` };
2082
+ }
2083
+ await commit2("chore(pnpm): add pnpm-lock.yaml");
2084
+ return { kind: "ok" };
2079
2085
  }
2080
- }
2081
- await writePackageJson(pkgPath, next);
2082
- const pkgSha = await commit(site.path, "chore(pnpm): pin packageManager + rewrite npm scripts");
2083
- if (pkgSha) shas.push(pkgSha);
2084
- await rm3(join15(site.path, "node_modules"), { recursive: true, force: true });
2085
- const installResult = await spawn2("pnpm", ["install"], {
2086
- cwd: site.path,
2087
- streaming: true
2088
2086
  });
2089
- if (installResult.code !== 0) {
2090
- return {
2091
- recipe: "convert-to-pnpm",
2092
- site: label,
2093
- status: "failed",
2094
- commits: shas,
2095
- notes: `pnpm install failed (exit ${installResult.code}). branch ${branch} left for inspection.`
2096
- };
2097
- }
2098
- const installSha = await commit(site.path, "chore(pnpm): add pnpm-lock.yaml");
2099
- if (installSha) shas.push(installSha);
2100
- return {
2101
- recipe: "convert-to-pnpm",
2102
- site: label,
2103
- status: "applied",
2104
- commits: shas,
2105
- notes: `branch: ${branch}`
2106
- };
2107
2087
  }
2108
2088
 
2109
2089
  // src/cli/commands/convert-to-pnpm.ts
@@ -2184,81 +2164,63 @@ async function exists3(path) {
2184
2164
  return false;
2185
2165
  }
2186
2166
  }
2187
- function siteLabel10(site) {
2188
- return site.name ?? site.path;
2189
- }
2190
2167
  function isDeclared(pkg, name) {
2191
2168
  return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
2192
2169
  }
2193
2170
  async function onboard(site, opts = {}) {
2194
- const label = siteLabel10(site);
2195
2171
  const spawn2 = opts.spawn ?? defaultSpawn;
2196
2172
  const audits = opts.audits ?? ["lighthouse", "a11y"];
2197
2173
  const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);
2198
- if (!await exists3(join17(site.path, "pnpm-lock.yaml"))) {
2199
- return {
2200
- recipe: "onboard",
2201
- site: label,
2202
- status: "failed",
2203
- commits: [],
2204
- notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
2205
- };
2206
- }
2207
- const pkgPath = join17(site.path, "package.json");
2208
- const pkg = await readPackageJson(pkgPath);
2209
- const toAdd = [];
2210
- if (!isDeclared(pkg, PACKAGE_NAME)) {
2211
- toAdd.push({ name: PACKAGE_NAME, version: packageVersion });
2212
- }
2213
- for (const audit of audits) {
2214
- for (const dep of AUDIT_DEPS[audit]) {
2215
- if (!isDeclared(pkg, dep.name)) toAdd.push(dep);
2174
+ return withRecipe({
2175
+ name: "onboard",
2176
+ site,
2177
+ plan: async () => {
2178
+ if (!await exists3(join17(site.path, "pnpm-lock.yaml"))) {
2179
+ return {
2180
+ kind: "failed",
2181
+ notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
2182
+ };
2183
+ }
2184
+ const pkgPath = join17(site.path, "package.json");
2185
+ const pkg = await readPackageJson(pkgPath);
2186
+ const toAdd = [];
2187
+ if (!isDeclared(pkg, PACKAGE_NAME)) {
2188
+ toAdd.push({ name: PACKAGE_NAME, version: packageVersion });
2189
+ }
2190
+ for (const audit of audits) {
2191
+ for (const dep of AUDIT_DEPS[audit]) {
2192
+ if (!isDeclared(pkg, dep.name)) toAdd.push(dep);
2193
+ }
2194
+ }
2195
+ if (toAdd.length === 0) {
2196
+ return {
2197
+ kind: "noop",
2198
+ notes: `site already has ${PACKAGE_NAME} and audit deps (${audits.join("+")})`
2199
+ };
2200
+ }
2201
+ return { kind: "apply", plan: { pkg, toAdd } };
2202
+ },
2203
+ apply: async ({ pkg, toAdd }, { commit: commit2, cwd }) => {
2204
+ const pkgPath = join17(cwd, "package.json");
2205
+ let next = pkg;
2206
+ for (const dep of toAdd) {
2207
+ next = bumpDep(next, dep.name, dep.version);
2208
+ }
2209
+ await writePackageJson(pkgPath, next);
2210
+ const installResult = await spawn2("pnpm", ["install"], { cwd, streaming: true });
2211
+ if (installResult.code !== 0) {
2212
+ return {
2213
+ kind: "failed",
2214
+ notes: `pnpm install failed (exit ${installResult.code})`
2215
+ };
2216
+ }
2217
+ await commit2(`chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`);
2218
+ return {
2219
+ kind: "ok",
2220
+ notes: `Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(", ")}`
2221
+ };
2216
2222
  }
2217
- }
2218
- if (toAdd.length === 0) {
2219
- return {
2220
- recipe: "onboard",
2221
- site: label,
2222
- status: "noop",
2223
- commits: [],
2224
- notes: `site already has ${PACKAGE_NAME} and audit deps (${audits.join("+")})`
2225
- };
2226
- }
2227
- if (!await isWorkingTreeClean(site.path)) {
2228
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
2229
- }
2230
- const branch = branchName("onboard");
2231
- await createBranch(site.path, branch);
2232
- let next = pkg;
2233
- for (const dep of toAdd) {
2234
- next = bumpDep(next, dep.name, dep.version);
2235
- }
2236
- await writePackageJson(pkgPath, next);
2237
- const installResult = await spawn2("pnpm", ["install"], {
2238
- cwd: site.path,
2239
- streaming: true
2240
2223
  });
2241
- if (installResult.code !== 0) {
2242
- return {
2243
- recipe: "onboard",
2244
- site: label,
2245
- status: "failed",
2246
- commits: [],
2247
- notes: `pnpm install failed (exit ${installResult.code}). branch ${branch} left for inspection.`
2248
- };
2249
- }
2250
- const sha = await commit(
2251
- site.path,
2252
- `chore(reddoor): onboard with ${PACKAGE_NAME} ${packageVersion}`
2253
- );
2254
- const shas = sha ? [sha] : [];
2255
- return {
2256
- recipe: "onboard",
2257
- site: label,
2258
- status: "applied",
2259
- commits: shas,
2260
- notes: `branch: ${branch}. Added ${toAdd.length} dep(s): ${toAdd.map((d) => d.name).join(", ")}`
2261
- };
2262
2224
  }
2263
2225
 
2264
2226
  // src/cli/commands/onboard.ts
@@ -2309,40 +2271,25 @@ import { resolve as resolve8 } from "path";
2309
2271
  // src/recipes/svelte-codemods.ts
2310
2272
  import { writeFile as writeFile8 } from "fs/promises";
2311
2273
  import { join as join18 } from "path";
2312
- function siteLabel11(site) {
2313
- return site.name ?? site.path;
2314
- }
2315
2274
  async function svelteCodemods(site) {
2316
- const label = siteLabel11(site);
2317
- const changes = await planGotchaCodemods(site.path);
2318
- if (changes.length === 0) {
2319
- return {
2320
- recipe: "svelte-codemods",
2321
- site: label,
2322
- status: "noop",
2323
- commits: [],
2324
- notes: "no codemod targets matched"
2325
- };
2326
- }
2327
- if (!await isWorkingTreeClean(site.path)) {
2328
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
2329
- }
2330
- const branch = branchName("svelte-codemods");
2331
- await createBranch(site.path, branch);
2332
- for (const c of changes) {
2333
- await writeFile8(join18(site.path, c.rel), c.after, "utf-8");
2334
- }
2335
- const sha = await commit(
2336
- site.path,
2337
- `refactor(svelte5): apply codemods (${changes.length} files)`
2338
- );
2339
- return {
2340
- recipe: "svelte-codemods",
2341
- site: label,
2342
- status: "applied",
2343
- commits: sha ? [sha] : [],
2344
- notes: `branch: ${branch}`
2345
- };
2275
+ return withRecipe({
2276
+ name: "svelte-codemods",
2277
+ site,
2278
+ plan: async () => {
2279
+ const changes = await planGotchaCodemods(site.path);
2280
+ if (changes.length === 0) {
2281
+ return { kind: "noop", notes: "no codemod targets matched" };
2282
+ }
2283
+ return { kind: "apply", plan: changes };
2284
+ },
2285
+ apply: async (changes, { commit: commit2, cwd }) => {
2286
+ for (const c of changes) {
2287
+ await writeFile8(join18(cwd, c.rel), c.after, "utf-8");
2288
+ }
2289
+ await commit2(`refactor(svelte5): apply codemods (${changes.length} files)`);
2290
+ return { kind: "ok" };
2291
+ }
2292
+ });
2346
2293
  }
2347
2294
 
2348
2295
  // src/cli/commands/svelte-codemods.ts
@@ -2401,6 +2348,17 @@ var RECIPE_DESCRIPTIONS = {
2401
2348
  "convert-to-pnpm": "Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts).",
2402
2349
  onboard: "Install @reddoorla/maintenance + audit deps on a site (preferred first step)."
2403
2350
  };
2351
+ async function runOrExit(fn, opts) {
2352
+ try {
2353
+ const { output, code } = await fn();
2354
+ console.log(output);
2355
+ process.exit(code);
2356
+ } catch (err) {
2357
+ const e = err;
2358
+ console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2359
+ process.exit(e.exitCode ?? 1);
2360
+ }
2361
+ }
2404
2362
  var cli = cac("reddoor-maint");
2405
2363
  cli.option("--cwd <path>", "Override working directory (default: process.cwd())");
2406
2364
  cli.option("--verbose", "Verbose output (full stack on errors)");
@@ -2415,101 +2373,31 @@ cli.command("list-recipes", "Print the available recipes.").action(() => {
2415
2373
  }
2416
2374
  });
2417
2375
  cli.command("audit [site]", "Run audits against a site (default: cwd).").option("--only <names>", "Comma-separated audit names (e.g. deps,lighthouse)").option("--json", "Machine-readable JSON output").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js); aggregates across sites").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2418
- async (site, opts) => {
2419
- try {
2420
- const { output, code } = await runAuditCommand(site, opts);
2421
- console.log(output);
2422
- process.exit(code);
2423
- } catch (err) {
2424
- const e = err;
2425
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2426
- process.exit(e.exitCode ?? 1);
2427
- }
2428
- }
2376
+ async (site, opts) => runOrExit(() => runAuditCommand(site, opts), opts)
2429
2377
  );
2430
2378
  cli.command("sync-configs [site]", "Sync canonical configs into a site.").option("--only <names>", "Comma-separated config names (e.g. eslint,prettier)").option("--dry", "Print diff without writing").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2431
- async (site, opts) => {
2432
- try {
2433
- const { output, code } = await runSyncConfigsCommand(site, opts);
2434
- console.log(output);
2435
- process.exit(code);
2436
- } catch (err) {
2437
- const e = err;
2438
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2439
- process.exit(e.exitCode ?? 1);
2440
- }
2441
- }
2379
+ async (site, opts) => runOrExit(() => runSyncConfigsCommand(site, opts), opts)
2442
2380
  );
2443
2381
  cli.command("bump-deps [site]", "Bump dependencies.").option("--group <group>", "patch | minor | major", { default: "minor" }).option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2444
- async (site, opts) => {
2445
- try {
2446
- const { output, code } = await runBumpDepsCommand(site, opts);
2447
- console.log(output);
2448
- process.exit(code);
2449
- } catch (err) {
2450
- const e = err;
2451
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2452
- process.exit(e.exitCode ?? 1);
2453
- }
2454
- }
2382
+ async (site, opts) => runOrExit(() => runBumpDepsCommand(site, opts), opts)
2455
2383
  );
2456
2384
  cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to-5).").example("reddoor-maint upgrade svelte-4-to-5 ./my-site").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2457
- async (upgrade, site, opts) => {
2458
- try {
2459
- const { output, code } = await runUpgradeCommand(upgrade, site, opts);
2460
- console.log(output);
2461
- process.exit(code);
2462
- } catch (err) {
2463
- const e = err;
2464
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2465
- process.exit(e.exitCode ?? 1);
2466
- }
2467
- }
2385
+ async (upgrade, site, opts) => runOrExit(() => runUpgradeCommand(upgrade, site, opts), opts)
2468
2386
  );
2469
2387
  cli.command(
2470
2388
  "convert-to-pnpm [site]",
2471
2389
  "Convert an npm/yarn site to pnpm (lockfile, packageManager, scripts)."
2472
2390
  ).option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2473
- async (site, opts) => {
2474
- try {
2475
- const { output, code } = await runConvertToPnpmCommand(site, opts);
2476
- console.log(output);
2477
- process.exit(code);
2478
- } catch (err) {
2479
- const e = err;
2480
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2481
- process.exit(e.exitCode ?? 1);
2482
- }
2483
- }
2391
+ async (site, opts) => runOrExit(() => runConvertToPnpmCommand(site, opts), opts)
2484
2392
  );
2485
2393
  cli.command("svelte-codemods [site]", "Apply Svelte 5 gotcha codemods to an already-migrated site.").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2486
- async (site, opts) => {
2487
- try {
2488
- const { output, code } = await runSvelteCodemodsCommand(site, opts);
2489
- console.log(output);
2490
- process.exit(code);
2491
- } catch (err) {
2492
- const e = err;
2493
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2494
- process.exit(e.exitCode ?? 1);
2495
- }
2496
- }
2394
+ async (site, opts) => runOrExit(() => runSvelteCodemodsCommand(site, opts), opts)
2497
2395
  );
2498
2396
  cli.command(
2499
2397
  "onboard [site]",
2500
2398
  "Install @reddoorla/maintenance + audit deps on a site (run after convert-to-pnpm)."
2501
2399
  ).option("--audits <names>", "Comma-separated audit subset: lighthouse,a11y (default: both)").option("--fleet <inventory>", "Inventory file (.json or .mjs/.js)").option("--workdir <path>", "Clone target for fleet mode (default ~/.reddoor-maint/sites)").action(
2502
- async (site, opts) => {
2503
- try {
2504
- const { output, code } = await runOnboardCommand(site, opts);
2505
- console.log(output);
2506
- process.exit(code);
2507
- } catch (err) {
2508
- const e = err;
2509
- console.error(opts.verbose ? e.stack ?? e.message : e.message ?? String(err));
2510
- process.exit(e.exitCode ?? 1);
2511
- }
2512
- }
2400
+ async (site, opts) => runOrExit(() => runOnboardCommand(site, opts), opts)
2513
2401
  );
2514
2402
  cli.help();
2515
2403
  cli.version(version);