@reddoorla/maintenance 0.1.1 → 0.1.2

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,29 +233,42 @@ 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
273
  async function tryRun(spawn2, cmd, args, cwd) {
259
274
  try {
@@ -518,6 +533,10 @@ import { dirname } from "node:path";
518
533
  const pages = ${JSON.stringify(a11yRoutes)};
519
534
  const OUTPUT = process.env.REDDOOR_A11Y_OUTPUT;
520
535
 
536
+ // Playwright's default per-test timeout is 30s. We loop through every
537
+ // configured route in a single test, so the budget needs to scale.
538
+ test.setTimeout(5 * 60_000);
539
+
521
540
  test("a11y across configured routes", async ({ page }) => {
522
541
  const violations = [];
523
542
  for (const { path, name } of pages) {
@@ -649,6 +668,7 @@ function localPath(path, opts = {}) {
649
668
 
650
669
  // src/inventory/json.ts
651
670
  import { readFile as readFile5 } from "fs/promises";
671
+ import { isAbsolute } from "path";
652
672
  function validate(raw) {
653
673
  if (!Array.isArray(raw)) {
654
674
  throw new Error("inventory JSON must be an array of sites");
@@ -661,6 +681,11 @@ function validate(raw) {
661
681
  if (typeof e.path !== "string" || e.path.length === 0) {
662
682
  throw new Error(`inventory entry ${i} is missing required field: path`);
663
683
  }
684
+ if (!isAbsolute(e.path)) {
685
+ throw new Error(
686
+ `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.`
687
+ );
688
+ }
664
689
  const site = { path: e.path };
665
690
  if (typeof e.name === "string") site.name = e.name;
666
691
  if (typeof e.repoUrl === "string") site.repoUrl = e.repoUrl;
@@ -711,11 +736,22 @@ async function resolveSites(input) {
711
736
 
712
737
  // src/cli/fleet/clone-if-needed.ts
713
738
  import { stat, readdir, mkdir } from "fs/promises";
714
- import { join as join5 } from "path";
739
+ import { isAbsolute as isAbsolute2, join as join5 } from "path";
715
740
  function deriveNameFromRepoUrl(repoUrl) {
716
741
  const slash = repoUrl.split("/").pop() ?? repoUrl;
717
742
  return slash.replace(/\.git$/, "");
718
743
  }
744
+ function assertSafeName(name) {
745
+ if (isAbsolute2(name)) {
746
+ throw new Error(`unsafe site name (absolute path not allowed): ${name}`);
747
+ }
748
+ if (name.includes("/") || name.includes("\\")) {
749
+ throw new Error(`unsafe site name (path separator not allowed): ${name}`);
750
+ }
751
+ if (name.split(/[\\/]/).some((seg) => seg === "..")) {
752
+ throw new Error(`unsafe site name (traversal segment not allowed): ${name}`);
753
+ }
754
+ }
719
755
  async function isNonEmptyDir(path) {
720
756
  try {
721
757
  const s = await stat(path);
@@ -732,6 +768,7 @@ async function cloneIfNeeded(site, opts) {
732
768
  throw new Error(`site path does not exist (${site.path}) and no repoUrl is set \u2014 cannot clone`);
733
769
  }
734
770
  const name = site.name ?? deriveNameFromRepoUrl(site.repoUrl);
771
+ assertSafeName(name);
735
772
  const target = join5(opts.workdir, name);
736
773
  await mkdir(opts.workdir, { recursive: true });
737
774
  if (await isNonEmptyDir(target)) {
@@ -995,6 +1032,7 @@ async function bumpDeps(site, opts = {}) {
995
1032
  const label = siteLabel7(site);
996
1033
  const group = opts.group ?? "minor";
997
1034
  const spawn2 = opts.spawn ?? defaultSpawn;
1035
+ await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
998
1036
  const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
999
1037
  cwd: site.path
1000
1038
  });
@@ -1019,7 +1057,10 @@ async function bumpDeps(site, opts = {}) {
1019
1057
  }
1020
1058
  const branch = branchName("bump-deps");
1021
1059
  await createBranch(site.path, branch);
1022
- await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], { cwd: site.path });
1060
+ await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
1061
+ cwd: site.path,
1062
+ streaming: true
1063
+ });
1023
1064
  const sha = await commit(site.path, `chore(deps): bump dependencies (${group})`);
1024
1065
  const shas = sha ? [sha] : [];
1025
1066
  return {
@@ -1075,31 +1116,33 @@ async function readPackageJson(path) {
1075
1116
  const raw = await readFile8(path, "utf-8");
1076
1117
  return JSON.parse(raw);
1077
1118
  }
1078
- async function writePackageJson(path, pkg2) {
1079
- const content = JSON.stringify(pkg2, null, 2) + "\n";
1119
+ async function writePackageJson(path, pkg) {
1120
+ const content = JSON.stringify(pkg, null, 2) + "\n";
1080
1121
  await writeFile4(path, content, "utf-8");
1081
1122
  }
1082
- function bumpDep(pkg2, name, version) {
1123
+ function bumpDep(pkg, name, version2, opts = {}) {
1124
+ const mode = opts.mode ?? "ensure";
1083
1125
  const next = {
1084
- ...pkg2
1126
+ ...pkg
1085
1127
  };
1086
- if (pkg2.dependencies) {
1087
- next.dependencies = { ...pkg2.dependencies };
1128
+ if (pkg.dependencies) {
1129
+ next.dependencies = { ...pkg.dependencies };
1088
1130
  }
1089
- if (pkg2.devDependencies) {
1090
- next.devDependencies = { ...pkg2.devDependencies };
1131
+ if (pkg.devDependencies) {
1132
+ next.devDependencies = { ...pkg.devDependencies };
1091
1133
  }
1092
1134
  if (next.dependencies && name in next.dependencies) {
1093
- if (next.dependencies[name] === version) return pkg2;
1094
- next.dependencies[name] = version;
1135
+ if (next.dependencies[name] === version2) return pkg;
1136
+ next.dependencies[name] = version2;
1095
1137
  return next;
1096
1138
  }
1097
1139
  if (next.devDependencies && name in next.devDependencies) {
1098
- if (next.devDependencies[name] === version) return pkg2;
1099
- next.devDependencies[name] = version;
1140
+ if (next.devDependencies[name] === version2) return pkg;
1141
+ next.devDependencies[name] = version2;
1100
1142
  return next;
1101
1143
  }
1102
- next.devDependencies = { ...next.devDependencies ?? {}, [name]: version };
1144
+ if (mode === "bump-only") return pkg;
1145
+ next.devDependencies = { ...next.devDependencies ?? {}, [name]: version2 };
1103
1146
  return next;
1104
1147
  }
1105
1148
 
@@ -1118,12 +1161,12 @@ var SVELTE_5_VERSIONS = {
1118
1161
  };
1119
1162
  async function bumpToSvelte5Versions(cwd) {
1120
1163
  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);
1164
+ const pkg = await readPackageJson(pkgPath);
1165
+ let next = pkg;
1166
+ for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
1167
+ next = bumpDep(next, name, version2, { mode: "bump-only" });
1125
1168
  }
1126
- if (next === pkg2) return false;
1169
+ if (next === pkg) return false;
1127
1170
  await writePackageJson(pkgPath, next);
1128
1171
  return true;
1129
1172
  }
@@ -1131,6 +1174,45 @@ async function bumpToSvelte5Versions(cwd) {
1131
1174
  // src/recipes/svelte-5/step-svelte-config.ts
1132
1175
  import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
1133
1176
  import { join as join9 } from "path";
1177
+ var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
1178
+ var IMPORT_FROM_VITE_PLUGIN = new RegExp(
1179
+ String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
1180
+ "m"
1181
+ );
1182
+ function dropVitePreprocessImport(source) {
1183
+ return source.replace(IMPORT_FROM_VITE_PLUGIN, (full, names) => {
1184
+ const remaining = names.split(",").map((n) => n.trim()).filter((n) => n.length > 0 && n !== "vitePreprocess");
1185
+ if (remaining.length === 0) return "";
1186
+ return `import { ${remaining.join(", ")} } from "${VITE_PLUGIN_PKG}";
1187
+ `;
1188
+ });
1189
+ }
1190
+ function findMatchingParen(source, openIdx) {
1191
+ if (source[openIdx] !== "(") return -1;
1192
+ let depth = 0;
1193
+ for (let i = openIdx; i < source.length; i++) {
1194
+ const ch = source[i];
1195
+ if (ch === "(") depth++;
1196
+ else if (ch === ")") {
1197
+ depth--;
1198
+ if (depth === 0) return i;
1199
+ }
1200
+ }
1201
+ return -1;
1202
+ }
1203
+ function dropPreprocessKey(source) {
1204
+ const startRe = /^(\s*)preprocess:\s*vitePreprocess\(/m;
1205
+ const m = startRe.exec(source);
1206
+ if (!m) return source;
1207
+ const indent = m[1] ?? "";
1208
+ const parenOpenAbs = m.index + m[0].length - 1;
1209
+ const parenCloseAbs = findMatchingParen(source, parenOpenAbs);
1210
+ if (parenCloseAbs < 0) return source;
1211
+ let tailIdx = parenCloseAbs + 1;
1212
+ while (tailIdx < source.length && /[ \t,]/.test(source[tailIdx] ?? "")) tailIdx++;
1213
+ if (source[tailIdx] === "\n") tailIdx++;
1214
+ return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
1215
+ }
1134
1216
  async function migrateSvelteConfig(cwd) {
1135
1217
  const path = join9(cwd, "svelte.config.js");
1136
1218
  let src;
@@ -1140,11 +1222,8 @@ async function migrateSvelteConfig(cwd) {
1140
1222
  return false;
1141
1223
  }
1142
1224
  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, "");
1225
+ next = dropPreprocessKey(next);
1226
+ next = dropVitePreprocessImport(next);
1148
1227
  if (next === src) return false;
1149
1228
  await writeFile5(path, next, "utf-8");
1150
1229
  return true;
@@ -1174,8 +1253,8 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
1174
1253
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
1175
1254
  import { join as join10 } from "path";
1176
1255
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1177
- const pkg2 = await readPackageJson(join10(cwd, "package.json"));
1178
- const tailwindVersion = pkg2.devDependencies?.tailwindcss ?? pkg2.dependencies?.tailwindcss;
1256
+ const pkg = await readPackageJson(join10(cwd, "package.json"));
1257
+ const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
1179
1258
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
1180
1259
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
1181
1260
  try {
@@ -1254,10 +1333,32 @@ function exportLetToProps(source) {
1254
1333
  }
1255
1334
 
1256
1335
  // src/recipes/svelte-5/codemods/dollar-restprops.ts
1336
+ function removeInterfaceBlock(source) {
1337
+ const re = /^\s*interface\s+\$\$Props\s*\{/m;
1338
+ let out = source;
1339
+ while (true) {
1340
+ const match = re.exec(out);
1341
+ if (!match) return out;
1342
+ const openBraceIdx = match.index + match[0].length - 1;
1343
+ let depth = 1;
1344
+ let i = openBraceIdx + 1;
1345
+ while (i < out.length && depth > 0) {
1346
+ const ch = out[i];
1347
+ if (ch === "{") depth++;
1348
+ else if (ch === "}") depth--;
1349
+ i++;
1350
+ }
1351
+ if (depth !== 0) return out;
1352
+ let endIdx = i;
1353
+ while (endIdx < out.length && /[ \t]/.test(out[endIdx] ?? "")) endIdx++;
1354
+ if (out[endIdx] === "\n") endIdx++;
1355
+ out = out.slice(0, match.index) + out.slice(endIdx);
1356
+ }
1357
+ }
1257
1358
  function removeDollarRestProps(source) {
1258
1359
  let next = source;
1259
1360
  next = next.replace(/\$\$restProps/g, "rest");
1260
- next = next.replace(/^\s*interface\s+\$\$Props\s*\{[^}]*\}\s*\n/gm, "");
1361
+ next = removeInterfaceBlock(next);
1261
1362
  return next;
1262
1363
  }
1263
1364
 
@@ -1327,8 +1428,8 @@ function siteLabel8(site) {
1327
1428
  }
1328
1429
  async function alreadyOnSvelte5(cwd) {
1329
1430
  try {
1330
- const pkg2 = await readPackageJson(join13(cwd, "package.json"));
1331
- const v = pkg2.devDependencies?.svelte ?? pkg2.dependencies?.svelte;
1431
+ const pkg = await readPackageJson(join13(cwd, "package.json"));
1432
+ const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
1332
1433
  return !!v && /^\^?5\./.test(v);
1333
1434
  } catch {
1334
1435
  return false;
@@ -1440,9 +1541,22 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
1440
1541
  return { output, code };
1441
1542
  }
1442
1543
 
1544
+ // src/cli/version.ts
1545
+ import { readFileSync } from "fs";
1546
+ import { join as join14 } from "path";
1547
+ function resolvePackageVersion(fromDir) {
1548
+ try {
1549
+ const raw = readFileSync(join14(fromDir, "..", "..", "package.json"), "utf-8");
1550
+ const pkg = JSON.parse(raw);
1551
+ return pkg.version ?? "unknown";
1552
+ } catch {
1553
+ return "unknown";
1554
+ }
1555
+ }
1556
+
1443
1557
  // src/cli/bin.ts
1444
1558
  var here = dirname(fileURLToPath(import.meta.url));
1445
- var pkg = JSON.parse(readFileSync(join14(here, "../../package.json"), "utf-8"));
1559
+ var version = resolvePackageVersion(here);
1446
1560
  var AUDIT_DESCRIPTIONS = {
1447
1561
  deps: "Diff site package.json against the bundled baseline version map.",
1448
1562
  lighthouse: "Run @lhci/cli autorun using the canonical lighthouserc.",
@@ -1521,6 +1635,6 @@ cli.command("upgrade <upgrade> [site]", "Run a named upgrade recipe (svelte-4-to
1521
1635
  }
1522
1636
  );
1523
1637
  cli.help();
1524
- cli.version(pkg.version);
1638
+ cli.version(version);
1525
1639
  cli.parse();
1526
1640
  //# sourceMappingURL=bin.js.map