@reddoorla/maintenance 0.1.1 → 0.1.3

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
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/bin.ts
4
- import { readFileSync } from "fs";
5
- import { dirname, join as join14 } from "path";
4
+ import { dirname } from "path";
6
5
  import { fileURLToPath } from "url";
7
6
  import { cac } from "cac";
8
7
 
@@ -12,15 +11,18 @@ import { resolve as resolve2 } from "path";
12
11
  // src/audits/util/spawn.ts
13
12
  import { spawn } from "child_process";
14
13
  var defaultSpawn = (cmd, args, opts = {}) => new Promise((resolve6, reject) => {
14
+ const streaming = opts.streaming === true;
15
15
  const child = spawn(cmd, [...args], {
16
16
  cwd: opts.cwd,
17
17
  env: opts.env ?? process.env,
18
- stdio: ["ignore", "pipe", "pipe"]
18
+ stdio: streaming ? ["ignore", "inherit", "inherit"] : ["ignore", "pipe", "pipe"]
19
19
  });
20
20
  let stdout = "";
21
21
  let stderr = "";
22
- child.stdout.on("data", (chunk) => stdout += String(chunk));
23
- child.stderr.on("data", (chunk) => stderr += String(chunk));
22
+ if (!streaming) {
23
+ child.stdout?.on("data", (chunk) => stdout += String(chunk));
24
+ child.stderr?.on("data", (chunk) => stderr += String(chunk));
25
+ }
24
26
  const timer = opts.timeoutMs ? setTimeout(() => {
25
27
  child.kill("SIGTERM");
26
28
  reject(new Error(`spawn timeout after ${opts.timeoutMs}ms: ${cmd}`));
@@ -115,10 +117,10 @@ async function depsAudit(ctx) {
115
117
  details: { error: String(err) }
116
118
  };
117
119
  }
118
- const pkg2 = JSON.parse(pkgRaw);
120
+ const pkg = JSON.parse(pkgRaw);
119
121
  const installed = {
120
- ...pkg2.dependencies ?? {},
121
- ...pkg2.devDependencies ?? {}
122
+ ...pkg.dependencies ?? {},
123
+ ...pkg.devDependencies ?? {}
122
124
  };
123
125
  const details = [];
124
126
  for (const [name, baseline] of Object.entries(baselineVersions)) {
@@ -148,7 +150,7 @@ async function depsAudit(ctx) {
148
150
  // src/audits/lint.ts
149
151
  import { existsSync } from "fs";
150
152
  import { readFile as readFile2 } from "fs/promises";
151
- import { join as join2, relative } from "path";
153
+ import { join as join2 } from "path";
152
154
  import { ESLint } from "eslint";
153
155
  import { check as prettierCheck, resolveConfig as prettierResolveConfig } from "prettier";
154
156
  import { glob } from "tinyglobby";
@@ -177,19 +179,19 @@ async function lintAudit(ctx) {
177
179
  errorOnUnmatchedPattern: false
178
180
  });
179
181
  const relFiles = await listFiles(site.path);
180
- const filesToLint = relFiles.map((f) => join2(site.path, f));
181
- const eslintResults = await eslint2.lintFiles(filesToLint);
182
+ const eslintResults = await eslint2.lintFiles(relFiles);
182
183
  const eslintErrors = eslintResults.reduce((n, r) => n + r.errorCount, 0);
183
184
  const eslintWarnings = eslintResults.reduce((n, r) => n + r.warningCount, 0);
184
185
  const prettierUnformatted = [];
185
- for (const file of filesToLint) {
186
- const source = await readFile2(file, "utf-8");
187
- const options = await prettierResolveConfig(file) ?? {};
188
- const ok = await prettierCheck(source, { ...options, filepath: file });
189
- if (!ok) prettierUnformatted.push(relative(site.path, file));
186
+ for (const rel of relFiles) {
187
+ const absForResolve = join2(site.path, rel);
188
+ const source = await readFile2(absForResolve, "utf-8");
189
+ const options = await prettierResolveConfig(absForResolve) ?? {};
190
+ const ok = await prettierCheck(source, { ...options, filepath: absForResolve });
191
+ if (!ok) prettierUnformatted.push(rel);
190
192
  }
191
193
  const status = eslintErrors > 0 || prettierUnformatted.length > 0 ? "fail" : eslintWarnings > 0 ? "warn" : "pass";
192
- const summary = status === "pass" ? `lint clean across ${filesToLint.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
194
+ const summary = status === "pass" ? `lint clean across ${relFiles.length} files` : `${eslintErrors} eslint errors, ${eslintWarnings} warnings, ${prettierUnformatted.length} unformatted`;
193
195
  return {
194
196
  audit: "lint",
195
197
  site: siteLabel2(site),
@@ -199,7 +201,7 @@ async function lintAudit(ctx) {
199
201
  eslintErrors,
200
202
  eslintWarnings,
201
203
  prettierUnformatted,
202
- files: filesToLint.length
204
+ files: relFiles.length
203
205
  }
204
206
  };
205
207
  }
@@ -215,7 +217,7 @@ function classify(v) {
215
217
  }
216
218
  function normalizeSeverity(s) {
217
219
  if (s === "low" || s === "moderate" || s === "high" || s === "critical") return s;
218
- return "moderate";
220
+ return "low";
219
221
  }
220
222
  function extractAdvisoriesFromPnpm(parsed) {
221
223
  const out = [];
@@ -231,83 +233,101 @@ function extractAdvisoriesFromPnpm(parsed) {
231
233
  }
232
234
  return out;
233
235
  }
236
+ function resolveNpmAdvisoryRoot(startName, vulnerabilities) {
237
+ const seen = /* @__PURE__ */ new Set();
238
+ let current = startName;
239
+ while (!seen.has(current)) {
240
+ seen.add(current);
241
+ const entry = vulnerabilities[current];
242
+ if (!entry || !Array.isArray(entry.via)) return { rootName: current };
243
+ const detailed = entry.via.find(
244
+ (e) => typeof e === "object" && e !== null
245
+ );
246
+ if (detailed) return { rootName: current, detail: detailed };
247
+ const next = entry.via.find((e) => typeof e === "string");
248
+ if (!next || next === current) return { rootName: current };
249
+ current = next;
250
+ }
251
+ return { rootName: current };
252
+ }
234
253
  function extractAdvisoriesFromNpm(parsed) {
235
- const out = [];
236
- for (const [name, v] of Object.entries(parsed.vulnerabilities ?? {})) {
254
+ const vulnerabilities = parsed.vulnerabilities ?? {};
255
+ const roots = /* @__PURE__ */ new Map();
256
+ for (const [name, v] of Object.entries(vulnerabilities)) {
237
257
  if (!v) continue;
238
- let title = name;
239
- let url;
240
- if (Array.isArray(v.via)) {
241
- const detailed = v.via.find(
242
- (entry) => typeof entry === "object" && entry !== null
243
- );
244
- if (detailed) {
245
- title = detailed.title ?? name;
246
- url = detailed.url;
247
- }
248
- }
249
- out.push({
250
- module: v.name ?? name,
251
- severity: normalizeSeverity(v.severity),
258
+ const { rootName, detail } = resolveNpmAdvisoryRoot(name, vulnerabilities);
259
+ if (roots.has(rootName)) continue;
260
+ const rootEntry = vulnerabilities[rootName];
261
+ const severity = normalizeSeverity(rootEntry?.severity ?? v.severity);
262
+ const title = detail?.title ?? rootName;
263
+ const url = detail?.url;
264
+ roots.set(rootName, {
265
+ module: rootEntry?.name ?? rootName,
266
+ severity,
252
267
  title,
253
268
  ...url ? { url } : {}
254
269
  });
255
270
  }
256
- return out;
271
+ return [...roots.values()];
257
272
  }
258
- async function tryRun(spawn2, cmd, args, cwd) {
273
+ async function runAuditTool(spawn2, cmd, args, cwd) {
274
+ let raw;
259
275
  try {
260
- return await spawn2(cmd, args, { cwd });
276
+ raw = await spawn2(cmd, args, { cwd });
261
277
  } catch (err) {
262
278
  const e = err;
263
- if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { missing: true };
264
- throw err;
265
- }
266
- }
267
- async function securityAudit(ctx) {
268
- const spawn2 = ctx.spawn ?? defaultSpawn;
269
- const site = ctx.site;
270
- const label = siteLabel3(site);
271
- let used = "pnpm audit";
272
- let raw = await tryRun(
273
- spawn2,
274
- "pnpm",
275
- ["audit", "--json", "--prod"],
276
- site.path
277
- );
278
- if ("missing" in raw) {
279
- used = "npm audit";
280
- raw = await tryRun(spawn2, "npm", ["audit", "--json", "--omit=dev"], site.path);
281
- }
282
- if ("missing" in raw) {
283
- return {
284
- audit: "security",
285
- site: label,
286
- status: "skip",
287
- summary: "neither pnpm nor npm is available on PATH"
288
- };
279
+ if (e.code === "ENOENT" || /ENOENT/.test(String(err))) return { kind: "missing" };
280
+ return { kind: "error", reason: `spawn failed: ${String(err).slice(0, 200)}` };
289
281
  }
290
282
  if (raw.code !== 0 && raw.code !== 1) {
291
283
  return {
292
- audit: "security",
293
- site: label,
294
- status: "skip",
295
- summary: `${used} exited with code ${raw.code}`,
296
- details: { stderr: raw.stderr }
284
+ kind: "error",
285
+ reason: `exit ${raw.code}${raw.stderr ? `: ${raw.stderr.slice(0, 150)}` : ""}`
297
286
  };
298
287
  }
299
288
  let parsed;
300
289
  try {
301
- parsed = JSON.parse(raw.stdout);
290
+ parsed = JSON.parse(raw.stdout || "{}");
302
291
  } catch (err) {
303
- return {
304
- audit: "security",
305
- site: label,
306
- status: "skip",
307
- summary: `${used} produced unparseable JSON`,
308
- details: { error: String(err), stdout: raw.stdout.slice(0, 500) }
309
- };
292
+ return { kind: "error", reason: `unparseable JSON: ${String(err).slice(0, 100)}` };
293
+ }
294
+ const errEnvelope = parsed.error;
295
+ if (errEnvelope && typeof errEnvelope === "object") {
296
+ return { kind: "error", reason: errEnvelope.code ?? "error envelope returned" };
297
+ }
298
+ if (!parsed.metadata?.vulnerabilities) {
299
+ return { kind: "error", reason: "no metadata.vulnerabilities in output" };
300
+ }
301
+ return { kind: "ok", parsed };
302
+ }
303
+ async function securityAudit(ctx) {
304
+ const spawn2 = ctx.spawn ?? defaultSpawn;
305
+ const site = ctx.site;
306
+ const label = siteLabel3(site);
307
+ let used = "pnpm audit";
308
+ let result = await runAuditTool(spawn2, "pnpm", ["audit", "--json", "--prod"], site.path);
309
+ if (result.kind !== "ok") {
310
+ const pnpmReason = result.kind === "missing" ? "not installed" : result.reason;
311
+ const npmResult = await runAuditTool(
312
+ spawn2,
313
+ "npm",
314
+ ["audit", "--json", "--omit=dev"],
315
+ site.path
316
+ );
317
+ if (npmResult.kind === "ok") {
318
+ result = npmResult;
319
+ used = "npm audit";
320
+ } else {
321
+ const npmReason = npmResult.kind === "missing" ? "not installed" : npmResult.reason;
322
+ return {
323
+ audit: "security",
324
+ site: label,
325
+ status: "skip",
326
+ summary: `cannot run audit \u2014 pnpm: ${pnpmReason}; npm: ${npmReason}`
327
+ };
328
+ }
310
329
  }
330
+ const parsed = result.parsed;
311
331
  const counts = {
312
332
  low: parsed.metadata?.vulnerabilities?.low ?? 0,
313
333
  moderate: parsed.metadata?.vulnerabilities?.moderate ?? 0,
@@ -518,6 +538,10 @@ import { dirname } from "node:path";
518
538
  const pages = ${JSON.stringify(a11yRoutes)};
519
539
  const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
520
540
 
541
+ // Playwright's default per-test timeout is 30s. We loop through every
542
+ // configured route in a single test, so the budget needs to scale.
543
+ test.setTimeout(5 * 60_000);
544
+
521
545
  test("a11y across configured routes", async ({ page }) => {
522
546
  const violations = [];
523
547
  for (const { path, name } of pages) {
@@ -649,6 +673,7 @@ function localPath(path, opts = {}) {
649
673
 
650
674
  // src/inventory/json.ts
651
675
  import { readFile as readFile5 } from "fs/promises";
676
+ import { isAbsolute } from "path";
652
677
  function validate(raw) {
653
678
  if (!Array.isArray(raw)) {
654
679
  throw new Error("inventory JSON must be an array of sites");
@@ -661,6 +686,11 @@ function validate(raw) {
661
686
  if (typeof e.path !== "string" || e.path.length === 0) {
662
687
  throw new Error(`inventory entry ${i} is missing required field: path`);
663
688
  }
689
+ if (!isAbsolute(e.path)) {
690
+ throw new Error(
691
+ `inventory entry ${i}: path must be absolute (got "${e.path}"). Relative paths are rejected so cwd at invocation can't change which site is targeted.`
692
+ );
693
+ }
664
694
  const site = { path: e.path };
665
695
  if (typeof e.name === "string") site.name = e.name;
666
696
  if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
@@ -711,11 +741,22 @@ async function resolveSites(input) {
711
741
 
712
742
  // src/cli/fleet/clone-if-needed.ts
713
743
  import { stat, readdir, mkdir } from "fs/promises";
714
- import { join as join5 } from "path";
744
+ import { isAbsolute as isAbsolute2, join as join5 } from "path";
715
745
  function deriveNameFromRepoUrl(repoUrl) {
716
746
  const slash = repoUrl.split("/").pop() ?? repoUrl;
717
747
  return slash.replace(/\.git$/, "");
718
748
  }
749
+ function assertSafeName(name) {
750
+ if (isAbsolute2(name)) {
751
+ throw new Error(`unsafe site name (absolute path not allowed): ${name}`);
752
+ }
753
+ if (name.includes("/") || name.includes("\\")) {
754
+ throw new Error(`unsafe site name (path separator not allowed): ${name}`);
755
+ }
756
+ if (name.split(/[\\/]/).some((seg) => seg === "..")) {
757
+ throw new Error(`unsafe site name (traversal segment not allowed): ${name}`);
758
+ }
759
+ }
719
760
  async function isNonEmptyDir(path) {
720
761
  try {
721
762
  const s = await stat(path);
@@ -732,6 +773,7 @@ async function cloneIfNeeded(site, opts) {
732
773
  throw new Error(`site path does not exist (${site.path}) and no repoUrl is set \u2014 cannot clone`);
733
774
  }
734
775
  const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
776
+ assertSafeName(name);
735
777
  const target = join5(opts.workdir, name);
736
778
  await mkdir(opts.workdir, { recursive: true });
737
779
  if (await isNonEmptyDir(target)) {
@@ -995,6 +1037,7 @@ async function bumpDeps(site, opts = {}) {
995
1037
  const label = siteLabel7(site);
996
1038
  const group = opts.group ?? "minor";
997
1039
  const spawn2 = opts.spawn ?? defaultSpawn;
1040
+ await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
998
1041
  const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
999
1042
  cwd: site.path
1000
1043
  });
@@ -1019,7 +1062,10 @@ async function bumpDeps(site, opts = {}) {
1019
1062
  }
1020
1063
  const branch = branchName("bump-deps");
1021
1064
  await createBranch(site.path, branch);
1022
- await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], { cwd: site.path });
1065
+ await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
1066
+ cwd: site.path,
1067
+ streaming: true
1068
+ });
1023
1069
  const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
1024
1070
  const shas = sha ? [sha] : [];
1025
1071
  return {
@@ -1075,31 +1121,33 @@ async function readPackageJson(path) {
1075
1121
  const raw = await readFile8(path, "utf-8");
1076
1122
  return JSON.parse(raw);
1077
1123
  }
1078
- async function writePackageJson(path, pkg2) {
1079
- const content = JSON.stringify(pkg2, null, 2) + "\n";
1124
+ async function writePackageJson(path, pkg) {
1125
+ const content = JSON.stringify(pkg, null, 2) + "\n";
1080
1126
  await writeFile4(path, content, "utf-8");
1081
1127
  }
1082
- function bumpDep(pkg2, name, version) {
1128
+ function bumpDep(pkg, name, version2, opts = {}) {
1129
+ const mode = opts.mode ?? "ensure";
1083
1130
  const next = {
1084
- ...pkg2
1131
+ ...pkg
1085
1132
  };
1086
- if (pkg2.dependencies) {
1087
- next.dependencies = { ...pkg2.dependencies };
1133
+ if (pkg.dependencies) {
1134
+ next.dependencies = { ...pkg.dependencies };
1088
1135
  }
1089
- if (pkg2.devDependencies) {
1090
- next.devDependencies = { ...pkg2.devDependencies };
1136
+ if (pkg.devDependencies) {
1137
+ next.devDependencies = { ...pkg.devDependencies };
1091
1138
  }
1092
1139
  if (next.dependencies && name in next.dependencies) {
1093
- if (next.dependencies[name] === version) return pkg2;
1094
- next.dependencies[name] = version;
1140
+ if (next.dependencies[name] === version2) return pkg;
1141
+ next.dependencies[name] = version2;
1095
1142
  return next;
1096
1143
  }
1097
1144
  if (next.devDependencies && name in next.devDependencies) {
1098
- if (next.devDependencies[name] === version) return pkg2;
1099
- next.devDependencies[name] = version;
1145
+ if (next.devDependencies[name] === version2) return pkg;
1146
+ next.devDependencies[name] = version2;
1100
1147
  return next;
1101
1148
  }
1102
- next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
1149
+ if (mode === "bump-only") return pkg;
1150
+ next.devDependencies = { ...next.devDependencies ?? {}, [name]: version2 };
1103
1151
  return next;
1104
1152
  }
1105
1153
 
@@ -1118,12 +1166,12 @@ var SVELTE_5_VERSIONS = {
1118
1166
  };
1119
1167
  async function bumpToSvelte5Versions(cwd) {
1120
1168
  const pkgPath = join8(cwd, "package.json");
1121
- const pkg2 = await readPackageJson(pkgPath);
1122
- let next = pkg2;
1123
- for (const [name, version] of Object.entries(SVELTE_5_VERSIONS)) {
1124
- next = bumpDep(next, name, version);
1169
+ const pkg = await readPackageJson(pkgPath);
1170
+ let next = pkg;
1171
+ for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
1172
+ next = bumpDep(next, name, version2, { mode: "bump-only" });
1125
1173
  }
1126
- if (next === pkg2) return false;
1174
+ if (next === pkg) return false;
1127
1175
  await writePackageJson(pkgPath, next);
1128
1176
  return true;
1129
1177
  }
@@ -1131,6 +1179,45 @@ async function bumpToSvelte5Versions(cwd) {
1131
1179
  // src/recipes/svelte-5/step-svelte-config.ts
1132
1180
  import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
1133
1181
  import { join as join9 } from "path";
1182
+ var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
1183
+ var IMPORT_FROM_VITE_PLUGIN = new RegExp(
1184
+ String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
1185
+ "m"
1186
+ );
1187
+ function dropVitePreprocessImport(source) {
1188
+ return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names) => {
1189
+ const remaining = names.split(",").map((n) => n.trim()).filter((n) => n.length > 0 && n !== "vitePreprocess");
1190
+ if (remaining.length === 0) return "";
1191
+ return `import { ${remaining.join(", ")} } from "${VITE_PLUGIN_PKG}";
1192
+ `;
1193
+ });
1194
+ }
1195
+ function findMatchingParen(source, openIdx) {
1196
+ if (source[openIdx] !== "(") return -1;
1197
+ let depth = 0;
1198
+ for (let i = openIdx; i < source.length; i++) {
1199
+ const ch = source[i];
1200
+ if (ch === "(") depth++;
1201
+ else if (ch === ")") {
1202
+ depth--;
1203
+ if (depth === 0) return i;
1204
+ }
1205
+ }
1206
+ return -1;
1207
+ }
1208
+ function dropPreprocessKey(source) {
1209
+ const startRe = /^(\s*)preprocess:\s*vitePreprocess\(/m;
1210
+ const m = startRe.exec(source);
1211
+ if (!m) return source;
1212
+ const indent = m[1] ?? "";
1213
+ const parenOpenAbs = m.index + m[0].length - 1;
1214
+ const parenCloseAbs = findMatchingParen(source, parenOpenAbs);
1215
+ if (parenCloseAbs < 0) return source;
1216
+ let tailIdx = parenCloseAbs + 1;
1217
+ while (tailIdx < source.length && /[ \t,]/.test(source[tailIdx] ?? "")) tailIdx++;
1218
+ if (source[tailIdx] === "\n") tailIdx++;
1219
+ return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
1220
+ }
1134
1221
  async function migrateSvelteConfig(cwd) {
1135
1222
  const path = join9(cwd, "svelte.config.js");
1136
1223
  let src;
@@ -1140,11 +1227,8 @@ async function migrateSvelteConfig(cwd) {
1140
1227
  return false;
1141
1228
  }
1142
1229
  let next = src;
1143
- next = next.replace(
1144
- /^import\s+\{\s*vitePreprocess\s*\}\s+from\s+["']@sveltejs\/vite-plugin-svelte["'];\n/m,
1145
- ""
1146
- );
1147
- next = next.replace(/^\s*preprocess:\s*vitePreprocess\(\)\s*,?\s*\n/m, "");
1230
+ next = dropPreprocessKey(next);
1231
+ next = dropVitePreprocessImport(next);
1148
1232
  if (next === src) return false;
1149
1233
  await writeFile5(path, next, "utf-8");
1150
1234
  return true;
@@ -1174,8 +1258,8 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
1174
1258
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
1175
1259
  import { join as join10 } from "path";
1176
1260
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1177
- const pkg2 = await readPackageJson(join10(cwd, "package.json"));
1178
- const tailwindVersion = pkg2.devDependencies?.tailwindcss ?? pkg2.dependencies?.tailwindcss;
1261
+ const pkg = await readPackageJson(join10(cwd, "package.json"));
1262
+ const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
1179
1263
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
1180
1264
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
1181
1265
  try {
@@ -1254,10 +1338,32 @@ function exportLetToProps(source) {
1254
1338
  }
1255
1339
 
1256
1340
  // src/recipes/svelte-5/codemods/dollar-restprops.ts
1341
+ function removeInterfaceBlock(source) {
1342
+ const re = /^\s*interface\s+\$\$Props\s*\{/m;
1343
+ let out = source;
1344
+ while (true) {
1345
+ const match = re.exec(out);
1346
+ if (!match) return out;
1347
+ const openBraceIdx = match.index + match[0].length - 1;
1348
+ let depth = 1;
1349
+ let i = openBraceIdx + 1;
1350
+ while (i < out.length && depth > 0) {
1351
+ const ch = out[i];
1352
+ if (ch === "{") depth++;
1353
+ else if (ch === "}") depth--;
1354
+ i++;
1355
+ }
1356
+ if (depth !== 0) return out;
1357
+ let endIdx = i;
1358
+ while (endIdx < out.length && /[ \t]/.test(out[endIdx] ?? "")) endIdx++;
1359
+ if (out[endIdx] === "\n") endIdx++;
1360
+ out = out.slice(0, match.index) + out.slice(endIdx);
1361
+ }
1362
+ }
1257
1363
  function removeDollarRestProps(source) {
1258
1364
  let next = source;
1259
1365
  next = next.replace(/\$\$restProps/g, "rest");
1260
- next = next.replace(/^\s*interface\s+\$\$Props\s*\{[^}]*\}\s*\n/gm, "");
1366
+ next = removeInterfaceBlock(next);
1261
1367
  return next;
1262
1368
  }
1263
1369
 
@@ -1327,8 +1433,8 @@ function siteLabel8(site) {
1327
1433
  }
1328
1434
  async function alreadyOnSvelte5(cwd) {
1329
1435
  try {
1330
- const pkg2 = await readPackageJson(join13(cwd, "package.json"));
1331
- const v = pkg2.devDependencies?.svelte ?? pkg2.dependencies?.svelte;
1436
+ const pkg = await readPackageJson(join13(cwd, "package.json"));
1437
+ const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
1332
1438
  return !!v && /^\^?5\./.test(v);
1333
1439
  } catch {
1334
1440
  return false;
@@ -1440,9 +1546,22 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
1440
1546
  return { output, code };
1441
1547
  }
1442
1548
 
1549
+ // src/cli/version.ts
1550
+ import { readFileSync } from "fs";
1551
+ import { join as join14 } from "path";
1552
+ function resolvePackageVersion(fromDir) {
1553
+ try {
1554
+ const raw = readFileSync(join14(fromDir, "..", "..", "package.json"), "utf-8");
1555
+ const pkg = JSON.parse(raw);
1556
+ return pkg.version ?? "unknown";
1557
+ } catch {
1558
+ return "unknown";
1559
+ }
1560
+ }
1561
+
1443
1562
  // src/cli/bin.ts
1444
1563
  var here = dirname(fileURLToPath(import.meta.url));
1445
- var pkg = JSON.parse(readFileSync(join14(here, "../../package.json"), "utf-8"));
1564
+ var version = resolvePackageVersion(here);
1446
1565
  var AUDIT_DESCRIPTIONS = {
1447
1566
  deps: "Diff site package.json against the bundled baseline version map.",
1448
1567
  lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
@@ -1521,6 +1640,6 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
1521
1640
  }
1522
1641
  );
1523
1642
  cli.help();
1524
- cli.version(pkg.version);
1643
+ cli.version(version);
1525
1644
  cli.parse();
1526
1645
  //# sourceMappingURL=bin.js.map