@reddoorla/maintenance 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/bin.js CHANGED
@@ -1040,6 +1040,17 @@ async function commit(cwd, message) {
1040
1040
 
1041
1041
  // src/recipes/sync-configs.ts
1042
1042
  var GITIGNORE_CONFIG = "gitignore";
1043
+ var ALL_CONFIG_NAMES = [
1044
+ "lighthouse",
1045
+ "eslint",
1046
+ "prettier",
1047
+ "playwright-a11y",
1048
+ "svelte",
1049
+ "gitignore"
1050
+ ];
1051
+ function isConfigName(value) {
1052
+ return ALL_CONFIG_NAMES.includes(value);
1053
+ }
1043
1054
  function siteLabel6(site) {
1044
1055
  return site.name ?? site.path;
1045
1056
  }
@@ -1119,12 +1130,34 @@ async function syncConfigs(site, opts = {}) {
1119
1130
 
1120
1131
  // src/cli/commands/sync-configs.ts
1121
1132
  function parseOnly2(value) {
1122
- return value ? value.split(",").map((s) => s.trim()) : void 0;
1133
+ if (!value) return void 0;
1134
+ const names = value.split(",").map((s) => s.trim());
1135
+ for (const n of names) {
1136
+ if (!isConfigName(n)) {
1137
+ throw Object.assign(
1138
+ new Error(`unknown config in --only: "${n}". Valid: ${ALL_CONFIG_NAMES.join(", ")}`),
1139
+ { exitCode: 2 }
1140
+ );
1141
+ }
1142
+ }
1143
+ return names;
1144
+ }
1145
+ async function dryPlanGitignore(cwd) {
1146
+ let existing;
1147
+ try {
1148
+ existing = await readFile7(join7(cwd, ".gitignore"), "utf-8");
1149
+ } catch {
1150
+ return "would create .gitignore";
1151
+ }
1152
+ const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
1153
+ if (merge.added.length === 0) return null;
1154
+ return `would update .gitignore (${merge.added.length} canonical entries to add)`;
1123
1155
  }
1124
1156
  async function dryPlan(cwd, which) {
1125
- const targets = which ? templatesByName(which) : ALL_TEMPLATES;
1157
+ const includeGitignore = which ? which.includes("gitignore") : true;
1158
+ const templateTargets = which ? templatesByName(which.filter((c) => c !== "gitignore")) : ALL_TEMPLATES;
1126
1159
  const lines = [];
1127
- for (const t of targets) {
1160
+ for (const t of templateTargets) {
1128
1161
  let existing = "";
1129
1162
  try {
1130
1163
  existing = await readFile7(join7(cwd, t.path), "utf-8");
@@ -1132,6 +1165,10 @@ async function dryPlan(cwd, which) {
1132
1165
  }
1133
1166
  if (existing !== t.contents) lines.push(`would update ${t.path} (config: ${t.config})`);
1134
1167
  }
1168
+ if (includeGitignore) {
1169
+ const gi = await dryPlanGitignore(cwd);
1170
+ if (gi) lines.push(gi);
1171
+ }
1135
1172
  return lines.length === 0 ? "no changes needed" : lines.join("\n");
1136
1173
  }
1137
1174
  function formatResult(r) {
@@ -1170,9 +1207,19 @@ async function runSyncConfigsCommand(site, opts) {
1170
1207
  import { resolve as resolve4 } from "path";
1171
1208
 
1172
1209
  // src/recipes/bump-deps.ts
1210
+ import { stat as stat2 } from "fs/promises";
1211
+ import { join as join8 } from "path";
1173
1212
  function siteLabel7(site) {
1174
1213
  return site.name ?? site.path;
1175
1214
  }
1215
+ async function exists(path) {
1216
+ try {
1217
+ await stat2(path);
1218
+ return true;
1219
+ } catch {
1220
+ return false;
1221
+ }
1222
+ }
1176
1223
  function outdatedFlagsForGroup(group) {
1177
1224
  if (group === "major") return ["--latest"];
1178
1225
  if (group === "minor") return [];
@@ -1186,6 +1233,24 @@ async function bumpDeps(site, opts = {}) {
1186
1233
  const label = siteLabel7(site);
1187
1234
  const group = opts.group ?? "minor";
1188
1235
  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
+ };
1249
+ }
1250
+ }
1251
+ if (!await isWorkingTreeClean(site.path)) {
1252
+ throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1253
+ }
1189
1254
  await spawn2("pnpm", ["install"], { cwd: site.path, streaming: true });
1190
1255
  const outdated = await spawn2("pnpm", ["outdated", "--json", ...outdatedFlagsForGroup(group)], {
1191
1256
  cwd: site.path
@@ -1206,9 +1271,6 @@ async function bumpDeps(site, opts = {}) {
1206
1271
  notes: `pnpm outdated reported nothing for group=${group}`
1207
1272
  };
1208
1273
  }
1209
- if (!await isWorkingTreeClean(site.path)) {
1210
- throw new Error(`refusing to run: working tree is not clean at ${site.path}`);
1211
- }
1212
1274
  const branch = branchName("bump-deps");
1213
1275
  await createBranch(site.path, branch);
1214
1276
  await spawn2("pnpm", ["up", ...upFlagsForGroup(group)], {
@@ -1262,7 +1324,7 @@ async function runBumpDepsCommand(site, opts) {
1262
1324
  import { resolve as resolve5 } from "path";
1263
1325
 
1264
1326
  // src/recipes/svelte-5/index.ts
1265
- import { join as join13 } from "path";
1327
+ import { join as join14 } from "path";
1266
1328
 
1267
1329
  // src/util/pkg.ts
1268
1330
  import { readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
@@ -1270,8 +1332,18 @@ async function readPackageJson(path) {
1270
1332
  const raw = await readFile8(path, "utf-8");
1271
1333
  return JSON.parse(raw);
1272
1334
  }
1335
+ function detectIndentFromContent(raw) {
1336
+ const match = raw.match(/\n([ \t]+)"/);
1337
+ return match ? match[1] ?? " " : " ";
1338
+ }
1273
1339
  async function writePackageJson(path, pkg) {
1274
- const content = JSON.stringify(pkg, null, 2) + "\n";
1340
+ let indent = " ";
1341
+ try {
1342
+ const existing = await readFile8(path, "utf-8");
1343
+ indent = detectIndentFromContent(existing);
1344
+ } catch {
1345
+ }
1346
+ const content = JSON.stringify(pkg, null, indent) + "\n";
1275
1347
  await writeFile4(path, content, "utf-8");
1276
1348
  }
1277
1349
  function bumpDep(pkg, name, version2, opts = {}) {
@@ -1301,7 +1373,7 @@ function bumpDep(pkg, name, version2, opts = {}) {
1301
1373
  }
1302
1374
 
1303
1375
  // src/recipes/svelte-5/step-bump-versions.ts
1304
- import { join as join8 } from "path";
1376
+ import { join as join9 } from "path";
1305
1377
  var SVELTE_5_VERSIONS = {
1306
1378
  svelte: "^5.55.5",
1307
1379
  "@sveltejs/kit": "^2.59.0",
@@ -1314,7 +1386,7 @@ var SVELTE_5_VERSIONS = {
1314
1386
  "typescript-svelte-plugin": "^0.3.52"
1315
1387
  };
1316
1388
  async function bumpToSvelte5Versions(cwd) {
1317
- const pkgPath = join8(cwd, "package.json");
1389
+ const pkgPath = join9(cwd, "package.json");
1318
1390
  const pkg = await readPackageJson(pkgPath);
1319
1391
  let next = pkg;
1320
1392
  for (const [name, version2] of Object.entries(SVELTE_5_VERSIONS)) {
@@ -1327,7 +1399,7 @@ async function bumpToSvelte5Versions(cwd) {
1327
1399
 
1328
1400
  // src/recipes/svelte-5/step-svelte-config.ts
1329
1401
  import { readFile as readFile9, writeFile as writeFile5 } from "fs/promises";
1330
- import { join as join9 } from "path";
1402
+ import { join as join10 } from "path";
1331
1403
  var VITE_PLUGIN_PKG = "@sveltejs/vite-plugin-svelte";
1332
1404
  var IMPORT_FROM_VITE_PLUGIN = new RegExp(
1333
1405
  String.raw`^import\s+\{\s*([^}]+?)\s*\}\s+from\s+["']` + VITE_PLUGIN_PKG.replace(/[/]/g, "\\/") + String.raw`["'];?[ \t]*\n`,
@@ -1368,7 +1440,7 @@ function dropPreprocessKey(source) {
1368
1440
  return source.slice(0, m.index) + source.slice(tailIdx).replace(new RegExp(`^${indent}\\n`), "");
1369
1441
  }
1370
1442
  async function migrateSvelteConfig(cwd) {
1371
- const path = join9(cwd, "svelte.config.js");
1443
+ const path = join10(cwd, "svelte.config.js");
1372
1444
  let src;
1373
1445
  try {
1374
1446
  src = await readFile9(path, "utf-8");
@@ -1405,9 +1477,9 @@ async function runSvelteMigrate(cwd, spawn2 = defaultSpawn) {
1405
1477
  }
1406
1478
 
1407
1479
  // src/recipes/svelte-5/step-tailwind-upgrade.ts
1408
- import { join as join10 } from "path";
1480
+ import { join as join11 } from "path";
1409
1481
  async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1410
- const pkg = await readPackageJson(join10(cwd, "package.json"));
1482
+ const pkg = await readPackageJson(join11(cwd, "package.json"));
1411
1483
  const tailwindVersion = pkg.devDependencies?.tailwindcss ?? pkg.dependencies?.tailwindcss;
1412
1484
  if (!tailwindVersion) return { ran: false, reason: "tailwindcss not installed" };
1413
1485
  if (/^\^?4\./.test(tailwindVersion)) return { ran: false, reason: "already on tailwind 4.x" };
@@ -1429,7 +1501,7 @@ async function upgradeTailwind(cwd, spawn2 = defaultSpawn) {
1429
1501
 
1430
1502
  // src/recipes/svelte-5/step-gotchas.ts
1431
1503
  import { readFile as readFile10, writeFile as writeFile6 } from "fs/promises";
1432
- import { join as join11 } from "path";
1504
+ import { join as join12 } from "path";
1433
1505
  import { glob as glob2 } from "tinyglobby";
1434
1506
 
1435
1507
  // src/recipes/svelte-5/codemods/on-event-to-handler.ts
@@ -1690,6 +1762,7 @@ function findStringClose(source, openIdx) {
1690
1762
  }
1691
1763
  return -1;
1692
1764
  }
1765
+ 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.";
1693
1766
  function transformBlocks(body) {
1694
1767
  const out = [];
1695
1768
  let last = 0;
@@ -1705,6 +1778,8 @@ function transformBlocks(body) {
1705
1778
  out.push(body.slice(last, m.index));
1706
1779
  out.push(leadingNewline);
1707
1780
  const blockBody = body.slice(openBraceIdx + 1, closeBraceIdx);
1781
+ out.push(`${indent}${MIGRATION_MARKER}
1782
+ `);
1708
1783
  out.push(`${indent}$effect(() => {${blockBody}});`);
1709
1784
  last = closeBraceIdx + 1;
1710
1785
  BLOCK_REACTIVE_HEAD.lastIndex = last;
@@ -1741,7 +1816,7 @@ async function planGotchaCodemods(cwd) {
1741
1816
  const changes = [];
1742
1817
  const relPaths = await glob2(SVELTE_GLOBS, { cwd, ignore: IGNORE2, absolute: false });
1743
1818
  for (const rel of relPaths) {
1744
- const path = join11(cwd, rel);
1819
+ const path = join12(cwd, rel);
1745
1820
  const before = await readFile10(path, "utf-8");
1746
1821
  const after = CODEMODS.reduce((s, fn) => fn(s), before);
1747
1822
  if (after !== before) changes.push({ rel, after });
@@ -1751,7 +1826,7 @@ async function planGotchaCodemods(cwd) {
1751
1826
  async function applyGotchaCodemods(cwd) {
1752
1827
  const changes = await planGotchaCodemods(cwd);
1753
1828
  for (const c of changes) {
1754
- await writeFile6(join11(cwd, c.rel), c.after, "utf-8");
1829
+ await writeFile6(join12(cwd, c.rel), c.after, "utf-8");
1755
1830
  }
1756
1831
  return { filesChanged: changes.length };
1757
1832
  }
@@ -1775,7 +1850,7 @@ async function verifyMigration(cwd, spawn2 = defaultSpawn) {
1775
1850
 
1776
1851
  // src/recipes/svelte-5/step-summary.ts
1777
1852
  import { writeFile as writeFile7 } from "fs/promises";
1778
- import { join as join12 } from "path";
1853
+ import { join as join13 } from "path";
1779
1854
  async function writeMigrationSummary(input) {
1780
1855
  const lines = [
1781
1856
  `# Svelte 4 \u2192 5 migration summary`,
@@ -1792,7 +1867,7 @@ async function writeMigrationSummary(input) {
1792
1867
  `- Verify Playwright a11y tests still pass.`
1793
1868
  ];
1794
1869
  const content = lines.join("\n") + "\n";
1795
- const path = join12(input.cwd, "MIGRATION_SVELTE_5.md");
1870
+ const path = join13(input.cwd, "MIGRATION_SVELTE_5.md");
1796
1871
  await writeFile7(path, content, "utf-8");
1797
1872
  return path;
1798
1873
  }
@@ -1803,7 +1878,7 @@ function siteLabel8(site) {
1803
1878
  }
1804
1879
  async function alreadyOnSvelte5(cwd) {
1805
1880
  try {
1806
- const pkg = await readPackageJson(join13(cwd, "package.json"));
1881
+ const pkg = await readPackageJson(join14(cwd, "package.json"));
1807
1882
  const v = pkg.devDependencies?.svelte ?? pkg.dependencies?.svelte;
1808
1883
  return !!v && /^\^?5\./.test(v);
1809
1884
  } catch {
@@ -1920,8 +1995,8 @@ async function runUpgradeCommand(upgradeName, site, opts = {}) {
1920
1995
  import { resolve as resolve6 } from "path";
1921
1996
 
1922
1997
  // src/recipes/convert-to-pnpm.ts
1923
- import { rm as rm3, stat as stat2 } from "fs/promises";
1924
- import { join as join14 } from "path";
1998
+ import { rm as rm3, stat as stat3 } from "fs/promises";
1999
+ import { join as join15 } from "path";
1925
2000
 
1926
2001
  // src/recipes/convert-to-pnpm/script-rewrites.ts
1927
2002
  function rewriteScriptForPnpm(script) {
@@ -1943,9 +2018,9 @@ function rewriteScriptsForPnpm(scripts) {
1943
2018
 
1944
2019
  // src/recipes/convert-to-pnpm.ts
1945
2020
  var DEFAULT_PNPM_VERSION = "10.33.1";
1946
- async function exists(path) {
2021
+ async function exists2(path) {
1947
2022
  try {
1948
- await stat2(path);
2023
+ await stat3(path);
1949
2024
  return true;
1950
2025
  } catch {
1951
2026
  return false;
@@ -1958,10 +2033,10 @@ async function convertToPnpm(site, opts = {}) {
1958
2033
  const label = siteLabel9(site);
1959
2034
  const spawn2 = opts.spawn ?? defaultSpawn;
1960
2035
  const pnpmVersion = opts.pnpmVersion ?? DEFAULT_PNPM_VERSION;
1961
- const pnpmLockPath = join14(site.path, "pnpm-lock.yaml");
1962
- const npmLockPath = join14(site.path, "package-lock.json");
1963
- const yarnLockPath = join14(site.path, "yarn.lock");
1964
- if (await exists(pnpmLockPath)) {
2036
+ const pnpmLockPath = join15(site.path, "pnpm-lock.yaml");
2037
+ const npmLockPath = join15(site.path, "package-lock.json");
2038
+ const yarnLockPath = join15(site.path, "yarn.lock");
2039
+ if (await exists2(pnpmLockPath)) {
1965
2040
  return {
1966
2041
  recipe: "convert-to-pnpm",
1967
2042
  site: label,
@@ -1970,8 +2045,8 @@ async function convertToPnpm(site, opts = {}) {
1970
2045
  notes: "site already has pnpm-lock.yaml"
1971
2046
  };
1972
2047
  }
1973
- const hasNpmLock = await exists(npmLockPath);
1974
- const hasYarnLock = await exists(yarnLockPath);
2048
+ const hasNpmLock = await exists2(npmLockPath);
2049
+ const hasYarnLock = await exists2(yarnLockPath);
1975
2050
  if (!hasNpmLock && !hasYarnLock) {
1976
2051
  return {
1977
2052
  recipe: "convert-to-pnpm",
@@ -1992,7 +2067,7 @@ async function convertToPnpm(site, opts = {}) {
1992
2067
  const sourceLock = hasNpmLock ? "package-lock.json" : "yarn.lock";
1993
2068
  const lockSha = await commit(site.path, `chore(pnpm): remove ${sourceLock}`);
1994
2069
  if (lockSha) shas.push(lockSha);
1995
- const pkgPath = join14(site.path, "package.json");
2070
+ const pkgPath = join15(site.path, "package.json");
1996
2071
  const pkg = await readPackageJson(pkgPath);
1997
2072
  const next = { ...pkg, packageManager: `pnpm@${pnpmVersion}` };
1998
2073
  if (pkg.scripts && typeof pkg.scripts === "object") {
@@ -2006,6 +2081,7 @@ async function convertToPnpm(site, opts = {}) {
2006
2081
  await writePackageJson(pkgPath, next);
2007
2082
  const pkgSha = await commit(site.path, "chore(pnpm): pin packageManager + rewrite npm scripts");
2008
2083
  if (pkgSha) shas.push(pkgSha);
2084
+ await rm3(join15(site.path, "node_modules"), { recursive: true, force: true });
2009
2085
  const installResult = await spawn2("pnpm", ["install"], {
2010
2086
  cwd: site.path,
2011
2087
  streaming: true
@@ -2059,17 +2135,17 @@ async function runConvertToPnpmCommand(site, opts) {
2059
2135
  import { resolve as resolve7 } from "path";
2060
2136
 
2061
2137
  // src/recipes/onboard.ts
2062
- import { stat as stat3 } from "fs/promises";
2063
- import { join as join16 } from "path";
2138
+ import { stat as stat4 } from "fs/promises";
2139
+ import { join as join17 } from "path";
2064
2140
 
2065
2141
  // src/util/self-version.ts
2066
2142
  import { readFileSync } from "fs";
2067
2143
  import { fileURLToPath } from "url";
2068
- import { dirname, join as join15 } from "path";
2144
+ import { dirname, join as join16 } from "path";
2069
2145
  function selfPackageVersion(callerImportMetaUrl) {
2070
2146
  try {
2071
2147
  const here2 = dirname(fileURLToPath(callerImportMetaUrl));
2072
- const raw = readFileSync(join15(here2, "..", "..", "package.json"), "utf-8");
2148
+ const raw = readFileSync(join16(here2, "..", "..", "package.json"), "utf-8");
2073
2149
  const pkg = JSON.parse(raw);
2074
2150
  return pkg.version ?? "0.0.0";
2075
2151
  } catch {
@@ -2082,16 +2158,27 @@ function selfCaretRange(callerImportMetaUrl) {
2082
2158
 
2083
2159
  // src/recipes/onboard.ts
2084
2160
  var PACKAGE_NAME = "@reddoorla/maintenance";
2085
- var AUDIT_DEPS = {
2086
- lighthouse: [{ name: "@lhci/cli", version: "^0.15.1" }],
2087
- a11y: [
2088
- { name: "@playwright/test", version: "^1.59.1" },
2089
- { name: "@axe-core/playwright", version: "^4.11.3" }
2090
- ]
2161
+ var AUDIT_DEP_NAMES = {
2162
+ lighthouse: ["@lhci/cli"],
2163
+ a11y: ["@playwright/test", "@axe-core/playwright"]
2091
2164
  };
2092
- async function exists2(path) {
2165
+ var AUDIT_DEPS = Object.fromEntries(
2166
+ Object.entries(AUDIT_DEP_NAMES).map(([audit, names]) => [
2167
+ audit,
2168
+ names.map((name) => {
2169
+ const version2 = baselineVersions[name];
2170
+ if (!version2) {
2171
+ throw new Error(
2172
+ `baseline-versions is missing audit dep "${name}" \u2014 add it to src/configs/baseline-versions.ts`
2173
+ );
2174
+ }
2175
+ return { name, version: version2 };
2176
+ })
2177
+ ])
2178
+ );
2179
+ async function exists3(path) {
2093
2180
  try {
2094
- await stat3(path);
2181
+ await stat4(path);
2095
2182
  return true;
2096
2183
  } catch {
2097
2184
  return false;
@@ -2108,7 +2195,7 @@ async function onboard(site, opts = {}) {
2108
2195
  const spawn2 = opts.spawn ?? defaultSpawn;
2109
2196
  const audits = opts.audits ?? ["lighthouse", "a11y"];
2110
2197
  const packageVersion = opts.packageVersion ?? selfCaretRange(import.meta.url);
2111
- if (!await exists2(join16(site.path, "pnpm-lock.yaml"))) {
2198
+ if (!await exists3(join17(site.path, "pnpm-lock.yaml"))) {
2112
2199
  return {
2113
2200
  recipe: "onboard",
2114
2201
  site: label,
@@ -2117,7 +2204,7 @@ async function onboard(site, opts = {}) {
2117
2204
  notes: "no pnpm-lock.yaml at site root \u2014 run convert-to-pnpm first"
2118
2205
  };
2119
2206
  }
2120
- const pkgPath = join16(site.path, "package.json");
2207
+ const pkgPath = join17(site.path, "package.json");
2121
2208
  const pkg = await readPackageJson(pkgPath);
2122
2209
  const toAdd = [];
2123
2210
  if (!isDeclared(pkg, PACKAGE_NAME)) {
@@ -2221,7 +2308,7 @@ import { resolve as resolve8 } from "path";
2221
2308
 
2222
2309
  // src/recipes/svelte-codemods.ts
2223
2310
  import { writeFile as writeFile8 } from "fs/promises";
2224
- import { join as join17 } from "path";
2311
+ import { join as join18 } from "path";
2225
2312
  function siteLabel11(site) {
2226
2313
  return site.name ?? site.path;
2227
2314
  }
@@ -2243,7 +2330,7 @@ async function svelteCodemods(site) {
2243
2330
  const branch = branchName("svelte-codemods");
2244
2331
  await createBranch(site.path, branch);
2245
2332
  for (const c of changes) {
2246
- await writeFile8(join17(site.path, c.rel), c.after, "utf-8");
2333
+ await writeFile8(join18(site.path, c.rel), c.after, "utf-8");
2247
2334
  }
2248
2335
  const sha = await commit(
2249
2336
  site.path,
@@ -2285,10 +2372,10 @@ async function runSvelteCodemodsCommand(site, opts) {
2285
2372
 
2286
2373
  // src/cli/version.ts
2287
2374
  import { readFileSync as readFileSync2 } from "fs";
2288
- import { join as join18 } from "path";
2375
+ import { join as join19 } from "path";
2289
2376
  function resolvePackageVersion(fromDir) {
2290
2377
  try {
2291
- const raw = readFileSync2(join18(fromDir, "..", "..", "package.json"), "utf-8");
2378
+ const raw = readFileSync2(join19(fromDir, "..", "..", "package.json"), "utf-8");
2292
2379
  const pkg = JSON.parse(raw);
2293
2380
  return pkg.version ?? "unknown";
2294
2381
  } catch {