@reddoorla/maintenance 0.55.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/airtable-2BJX4ATJ.js +9 -0
- package/dist/airtable-2BJX4ATJ.js.map +1 -0
- package/dist/announce-JEVCDDKL.js +171 -0
- package/dist/announce-JEVCDDKL.js.map +1 -0
- package/dist/bump-deps-O3SOLQYY.js +57 -0
- package/dist/bump-deps-O3SOLQYY.js.map +1 -0
- package/dist/chunk-2X322ZES.js +99 -0
- package/dist/chunk-2X322ZES.js.map +1 -0
- package/dist/chunk-4NAOR44V.js +153 -0
- package/dist/chunk-4NAOR44V.js.map +1 -0
- package/dist/chunk-4PY4I5OR.js +1167 -0
- package/dist/chunk-4PY4I5OR.js.map +1 -0
- package/dist/chunk-5L2QXUTU.js +279 -0
- package/dist/chunk-5L2QXUTU.js.map +1 -0
- package/dist/chunk-5WL3ET24.js +203 -0
- package/dist/chunk-5WL3ET24.js.map +1 -0
- package/dist/chunk-6LIWDIXU.js +38 -0
- package/dist/chunk-6LIWDIXU.js.map +1 -0
- package/dist/chunk-6VAL7XJT.js +52 -0
- package/dist/chunk-6VAL7XJT.js.map +1 -0
- package/dist/chunk-BWLUNY7L.js +585 -0
- package/dist/chunk-BWLUNY7L.js.map +1 -0
- package/dist/chunk-CASCWQQX.js +161 -0
- package/dist/chunk-CASCWQQX.js.map +1 -0
- package/dist/chunk-CLVTVDOU.js +274 -0
- package/dist/chunk-CLVTVDOU.js.map +1 -0
- package/dist/chunk-D3DQFC32.js +79 -0
- package/dist/chunk-D3DQFC32.js.map +1 -0
- package/dist/chunk-EYODPUHV.js +37 -0
- package/dist/chunk-EYODPUHV.js.map +1 -0
- package/dist/chunk-FDIFTIXC.js +70 -0
- package/dist/chunk-FDIFTIXC.js.map +1 -0
- package/dist/chunk-FUD4I2ZW.js +35 -0
- package/dist/chunk-FUD4I2ZW.js.map +1 -0
- package/dist/chunk-HG5DTFNF.js +62 -0
- package/dist/chunk-HG5DTFNF.js.map +1 -0
- package/dist/chunk-HN6I2XWI.js +65 -0
- package/dist/chunk-HN6I2XWI.js.map +1 -0
- package/dist/chunk-HVOOCK6L.js +84 -0
- package/dist/chunk-HVOOCK6L.js.map +1 -0
- package/dist/chunk-IMIR7SC2.js +51 -0
- package/dist/chunk-IMIR7SC2.js.map +1 -0
- package/dist/chunk-J3OPWFZE.js +64 -0
- package/dist/chunk-J3OPWFZE.js.map +1 -0
- package/dist/chunk-J4ECCFRT.js +221 -0
- package/dist/chunk-J4ECCFRT.js.map +1 -0
- package/dist/chunk-JNXCDUYB.js +211 -0
- package/dist/chunk-JNXCDUYB.js.map +1 -0
- package/dist/chunk-JQG222CI.js +49 -0
- package/dist/chunk-JQG222CI.js.map +1 -0
- package/dist/chunk-LKGVSM2O.js +43 -0
- package/dist/chunk-LKGVSM2O.js.map +1 -0
- package/dist/chunk-LYCKN7ZR.js +41 -0
- package/dist/chunk-LYCKN7ZR.js.map +1 -0
- package/dist/chunk-MKSZBODM.js +162 -0
- package/dist/chunk-MKSZBODM.js.map +1 -0
- package/dist/chunk-O65I7G7G.js +246 -0
- package/dist/chunk-O65I7G7G.js.map +1 -0
- package/dist/chunk-OCS7SZUZ.js +243 -0
- package/dist/chunk-OCS7SZUZ.js.map +1 -0
- package/dist/chunk-OGGRFBL4.js +14 -0
- package/dist/chunk-OGGRFBL4.js.map +1 -0
- package/dist/chunk-OQPMZ6BM.js +368 -0
- package/dist/chunk-OQPMZ6BM.js.map +1 -0
- package/dist/chunk-OTGB6YQO.js +52 -0
- package/dist/chunk-OTGB6YQO.js.map +1 -0
- package/dist/chunk-PZQ3WED2.js +14 -0
- package/dist/chunk-PZQ3WED2.js.map +1 -0
- package/dist/chunk-QTHKOIFJ.js +13 -0
- package/dist/chunk-QTHKOIFJ.js.map +1 -0
- package/dist/chunk-U5SZ4FOP.js +26 -0
- package/dist/chunk-U5SZ4FOP.js.map +1 -0
- package/dist/chunk-W3G7XFBZ.js +444 -0
- package/dist/chunk-W3G7XFBZ.js.map +1 -0
- package/dist/chunk-WEKL2HYJ.js +31 -0
- package/dist/chunk-WEKL2HYJ.js.map +1 -0
- package/dist/chunk-WVRPOXKH.js +74 -0
- package/dist/chunk-WVRPOXKH.js.map +1 -0
- package/dist/chunk-XKOZUVK2.js +121 -0
- package/dist/chunk-XKOZUVK2.js.map +1 -0
- package/dist/chunk-XO2RUYVM.js +138 -0
- package/dist/chunk-XO2RUYVM.js.map +1 -0
- package/dist/chunk-XTK5VIQB.js +97 -0
- package/dist/chunk-XTK5VIQB.js.map +1 -0
- package/dist/chunk-XXTZBPUY.js +9 -0
- package/dist/chunk-XXTZBPUY.js.map +1 -0
- package/dist/chunk-ZYBOT5PM.js +153 -0
- package/dist/chunk-ZYBOT5PM.js.map +1 -0
- package/dist/cli/bin.js +75 -7785
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/commands/audit.js +37 -2336
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/client-EPVO3FEH.js +10 -0
- package/dist/client-EPVO3FEH.js.map +1 -0
- package/dist/client-JA6QLEDW.js +48 -0
- package/dist/client-JA6QLEDW.js.map +1 -0
- package/dist/configs/lighthouse.js +4 -31
- package/dist/configs/lighthouse.js.map +1 -1
- package/dist/configs/playwright-a11y.js +6 -33
- package/dist/configs/playwright-a11y.js.map +1 -1
- package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
- package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
- package/dist/db-AXKMRTVA.js +23 -0
- package/dist/db-AXKMRTVA.js.map +1 -0
- package/dist/digest-YTO73BEO.js +408 -0
- package/dist/digest-YTO73BEO.js.map +1 -0
- package/dist/forms/index.js +3 -8
- package/dist/forms/index.js.map +1 -1
- package/dist/github-signals-XLQXXH6Q.js +116 -0
- package/dist/github-signals-XLQXXH6Q.js.map +1 -0
- package/dist/index.js +102 -4757
- package/dist/index.js.map +1 -1
- package/dist/init-JITQXFVL.js +85 -0
- package/dist/init-JITQXFVL.js.map +1 -0
- package/dist/launch-XW6PEKUL.js +207 -0
- package/dist/launch-XW6PEKUL.js.map +1 -0
- package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
- package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
- package/dist/migrate-P2LS4WXI.js +7 -0
- package/dist/migrate-P2LS4WXI.js.map +1 -0
- package/dist/onboard-TCQOP2W4.js +69 -0
- package/dist/onboard-TCQOP2W4.js.map +1 -0
- package/dist/orchestrate-LHH3LQNP.js +23 -0
- package/dist/orchestrate-LHH3LQNP.js.map +1 -0
- package/dist/recipes/sync-configs.js +9 -505
- package/dist/recipes/sync-configs.js.map +1 -1
- package/dist/renovate-dispatch-YVBTBJJY.js +100 -0
- package/dist/renovate-dispatch-YVBTBJJY.js.map +1 -0
- package/dist/report-WK3SKWJI.js +240 -0
- package/dist/report-WK3SKWJI.js.map +1 -0
- package/dist/resend-G2BJMAQV.js +7 -0
- package/dist/resend-G2BJMAQV.js.map +1 -0
- package/dist/self-updating-BYUBUNO4.js +61 -0
- package/dist/self-updating-BYUBUNO4.js.map +1 -0
- package/dist/svelte-codemods-BYRJAU7I.js +51 -0
- package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
- package/dist/sync-configs-JPRLAW5J.js +109 -0
- package/dist/sync-configs-JPRLAW5J.js.map +1 -0
- package/dist/upgrade-ERA45EVR.js +64 -0
- package/dist/upgrade-ERA45EVR.js.map +1 -0
- package/dist/util/git.js +19 -76
- package/dist/util/git.js.map +1 -1
- package/dist/util/pkg.js +5 -45
- package/dist/util/pkg.js.map +1 -1
- package/dist/websites-X5DLC2PC.js +43 -0
- package/dist/websites-X5DLC2PC.js.map +1 -0
- package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
- package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
- package/package.json +13 -12
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {
|
|
2
|
+
siteSlug
|
|
3
|
+
} from "./chunk-CLVTVDOU.js";
|
|
4
|
+
|
|
5
|
+
// src/audits/lighthouse-airtable.ts
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
var LIGHTHOUSE_CATEGORIES = ["performance", "accessibility", "best-practices", "seo"];
|
|
9
|
+
function hasRealScores(result) {
|
|
10
|
+
if (result.audit !== "lighthouse") return false;
|
|
11
|
+
const details = result.details ?? {};
|
|
12
|
+
const summary = details.summary ?? {};
|
|
13
|
+
return LIGHTHOUSE_CATEGORIES.some(
|
|
14
|
+
(k) => typeof summary[k] === "number" && !Number.isNaN(summary[k])
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
function lighthouseScoresFromResult(result) {
|
|
18
|
+
if (result.audit !== "lighthouse") {
|
|
19
|
+
throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);
|
|
20
|
+
}
|
|
21
|
+
const details = result.details ?? {};
|
|
22
|
+
const summary = details.summary ?? {};
|
|
23
|
+
const toPct = (n) => typeof n === "number" && !Number.isNaN(n) ? Math.round(n * 100) : 0;
|
|
24
|
+
return {
|
|
25
|
+
performance: toPct(summary["performance"]),
|
|
26
|
+
accessibility: toPct(summary["accessibility"]),
|
|
27
|
+
bestPractices: toPct(summary["best-practices"]),
|
|
28
|
+
seo: toPct(summary["seo"])
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function resolveSlugFromCwd(cwd) {
|
|
32
|
+
try {
|
|
33
|
+
const pkgPath = join(cwd, "package.json");
|
|
34
|
+
const raw = await readFile(pkgPath, "utf-8");
|
|
35
|
+
const pkg = JSON.parse(raw);
|
|
36
|
+
if (!pkg.name) throw new Error("package.json has no 'name' field");
|
|
37
|
+
return siteSlug(pkg.name);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Could not derive site slug from ${cwd}/package.json: ${e.message}. Pass --write-airtable=<slug> explicitly.`,
|
|
41
|
+
{ cause: e }
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
hasRealScores,
|
|
48
|
+
lighthouseScoresFromResult,
|
|
49
|
+
resolveSlugFromCwd
|
|
50
|
+
};
|
|
51
|
+
//# sourceMappingURL=chunk-IMIR7SC2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/audits/lighthouse-airtable.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport type { AuditResult } from \"../types.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\nimport { siteSlug } from \"../reports/airtable/websites.js\";\n\nconst LIGHTHOUSE_CATEGORIES = [\"performance\", \"accessibility\", \"best-practices\", \"seo\"] as const;\n\n/**\n * True when the result carries real lighthouse scores worth persisting.\n *\n * The `audit --write-airtable` policy used to refuse on any `status: \"fail\"`,\n * but that conflates two very different failure modes:\n * 1. Infrastructure failure (no lhr-*.json written, spawn timeout, etc.)\n * → `details.summary` empty → all-zeros would corrupt the dashboard\n * 2. Assertion failure (scores below threshold, e.g. best-practices < 0.9)\n * → `details.summary` has real numbers → tracking these IS the point\n *\n * The dashboard exists to surface drift over time. Refusing to write the\n * very numbers the dashboard wants to plot — just because one assertion\n * tripped — defeats the purpose. Write whenever real scores exist.\n */\nexport function hasRealScores(result: AuditResult): boolean {\n if (result.audit !== \"lighthouse\") return false;\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n return LIGHTHOUSE_CATEGORIES.some(\n (k) => typeof summary[k] === \"number\" && !Number.isNaN(summary[k]),\n );\n}\n\n/**\n * Extract the four Lighthouse scores (as integer percentages) from a\n * `lighthouse` AuditResult. LHCI manifest summaries are floats in [0,1];\n * multiply + round.\n */\nexport function lighthouseScoresFromResult(result: AuditResult): LighthouseScores {\n if (result.audit !== \"lighthouse\") {\n throw new Error(`Expected a 'lighthouse' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as { summary?: Record<string, number> };\n const summary = details.summary ?? {};\n const toPct = (n: number | undefined) =>\n typeof n === \"number\" && !Number.isNaN(n) ? Math.round(n * 100) : 0;\n return {\n performance: toPct(summary[\"performance\"]),\n accessibility: toPct(summary[\"accessibility\"]),\n bestPractices: toPct(summary[\"best-practices\"]),\n seo: toPct(summary[\"seo\"]),\n };\n}\n\n/**\n * Derive a site slug from the cwd's package.json#name. Used by\n * `audit lighthouse --write-airtable` when the operator doesn't pass an\n * explicit slug — the cwd is the site checkout, so package.json#name is\n * usually the canonical site name.\n */\nexport async function resolveSlugFromCwd(cwd: string): Promise<string> {\n try {\n const pkgPath = join(cwd, \"package.json\");\n const raw = await readFile(pkgPath, \"utf-8\");\n const pkg = JSON.parse(raw) as { name?: string };\n if (!pkg.name) throw new Error(\"package.json has no 'name' field\");\n return siteSlug(pkg.name);\n } catch (e) {\n throw new Error(\n `Could not derive site slug from ${cwd}/package.json: ${(e as Error).message}. ` +\n `Pass --write-airtable=<slug> explicitly.`,\n { cause: e },\n );\n }\n}\n"],"mappings":";;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY;AAKrB,IAAM,wBAAwB,CAAC,eAAe,iBAAiB,kBAAkB,KAAK;AAgB/E,SAAS,cAAc,QAA8B;AAC1D,MAAI,OAAO,UAAU,aAAc,QAAO;AAC1C,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,SAAO,sBAAsB;AAAA,IAC3B,CAAC,MAAM,OAAO,QAAQ,CAAC,MAAM,YAAY,CAAC,OAAO,MAAM,QAAQ,CAAC,CAAC;AAAA,EACnE;AACF;AAOO,SAAS,2BAA2B,QAAuC;AAChF,MAAI,OAAO,UAAU,cAAc;AACjC,UAAM,IAAI,MAAM,6CAA6C,OAAO,KAAK,GAAG;AAAA,EAC9E;AACA,QAAM,UAAW,OAAO,WAAW,CAAC;AACpC,QAAM,UAAU,QAAQ,WAAW,CAAC;AACpC,QAAM,QAAQ,CAAC,MACb,OAAO,MAAM,YAAY,CAAC,OAAO,MAAM,CAAC,IAAI,KAAK,MAAM,IAAI,GAAG,IAAI;AACpE,SAAO;AAAA,IACL,aAAa,MAAM,QAAQ,aAAa,CAAC;AAAA,IACzC,eAAe,MAAM,QAAQ,eAAe,CAAC;AAAA,IAC7C,eAAe,MAAM,QAAQ,gBAAgB,CAAC;AAAA,IAC9C,KAAK,MAAM,QAAQ,KAAK,CAAC;AAAA,EAC3B;AACF;AAQA,eAAsB,mBAAmB,KAA8B;AACrE,MAAI;AACF,UAAM,UAAU,KAAK,KAAK,cAAc;AACxC,UAAM,MAAM,MAAM,SAAS,SAAS,OAAO;AAC3C,UAAM,MAAM,KAAK,MAAM,GAAG;AAC1B,QAAI,CAAC,IAAI,KAAM,OAAM,IAAI,MAAM,kCAAkC;AACjE,WAAO,SAAS,IAAI,IAAI;AAAA,EAC1B,SAAS,GAAG;AACV,UAAM,IAAI;AAAA,MACR,mCAAmC,GAAG,kBAAmB,EAAY,OAAO;AAAA,MAE5E,EAAE,OAAO,EAAE;AAAA,IACb;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromJsonFile,
|
|
3
|
+
localPath
|
|
4
|
+
} from "./chunk-FDIFTIXC.js";
|
|
5
|
+
import {
|
|
6
|
+
isHttpUrl
|
|
7
|
+
} from "./chunk-U5SZ4FOP.js";
|
|
8
|
+
|
|
9
|
+
// src/cli/fleet/resolve-sites.ts
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
import { resolve, extname } from "path";
|
|
12
|
+
function sanitizeDynamicSites(sites) {
|
|
13
|
+
return sites.map((s) => {
|
|
14
|
+
if (s.deployedUrl !== void 0 && !isHttpUrl(s.deployedUrl)) {
|
|
15
|
+
console.warn(
|
|
16
|
+
`[inventory] dynamic inventory: ignoring deployedUrl that is not http(s) for ${s.name ?? s.path}: ${JSON.stringify(s.deployedUrl)}`
|
|
17
|
+
);
|
|
18
|
+
const copy = { ...s };
|
|
19
|
+
delete copy.deployedUrl;
|
|
20
|
+
return copy;
|
|
21
|
+
}
|
|
22
|
+
return s;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async function resolveSites(input) {
|
|
26
|
+
if (input.site && input.fleet) {
|
|
27
|
+
throw Object.assign(new Error("cannot combine a positional [site] with --fleet"), {
|
|
28
|
+
exitCode: 2
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (input.fleet === "airtable") {
|
|
32
|
+
const { openBase, readAirtableConfig } = await import("./client-EPVO3FEH.js");
|
|
33
|
+
const { fromAirtableBase } = await import("./airtable-2BJX4ATJ.js");
|
|
34
|
+
const base = openBase(readAirtableConfig());
|
|
35
|
+
const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});
|
|
36
|
+
return provider();
|
|
37
|
+
}
|
|
38
|
+
if (input.fleet) {
|
|
39
|
+
const fleetPath = resolve(input.cwd, input.fleet);
|
|
40
|
+
const ext = extname(fleetPath).toLowerCase();
|
|
41
|
+
if (ext === ".json") {
|
|
42
|
+
return fromJsonFile(fleetPath)();
|
|
43
|
+
}
|
|
44
|
+
if (ext === ".js" || ext === ".mjs" || ext === ".cjs") {
|
|
45
|
+
const mod = await import(pathToFileURL(fleetPath).href);
|
|
46
|
+
if (!mod.default || typeof mod.default !== "function") {
|
|
47
|
+
throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {
|
|
48
|
+
exitCode: 2
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
return sanitizeDynamicSites(await mod.default());
|
|
52
|
+
}
|
|
53
|
+
throw Object.assign(
|
|
54
|
+
new Error(`--fleet ${input.fleet}: unsupported extension ${ext || "(none)"}`),
|
|
55
|
+
{ exitCode: 2 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return localPath(resolve(input.cwd, input.site ?? input.cwd))();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
resolveSites
|
|
63
|
+
};
|
|
64
|
+
//# sourceMappingURL=chunk-J3OPWFZE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cli/fleet/resolve-sites.ts"],"sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { resolve, extname } from \"node:path\";\nimport type { InventoryProvider, Site } from \"../../types.js\";\nimport { localPath } from \"../../inventory/local.js\";\nimport { fromJsonFile } from \"../../inventory/json.js\";\nimport { isHttpUrl } from \"../../util/url.js\";\n\n/** A dynamic .js/.mjs/.cjs inventory returns arbitrary Site objects; apply the\n * same `deployedUrl` scheme-allowlist the JSON + Airtable providers enforce, so\n * a module returning `file:///…`/`gopher://…` can't reach Chrome/lhci. Drop the\n * offending value (and warn) rather than throw — matching the JSON provider. */\nexport function sanitizeDynamicSites(sites: Site[]): Site[] {\n return sites.map((s) => {\n if (s.deployedUrl !== undefined && !isHttpUrl(s.deployedUrl)) {\n console.warn(\n `[inventory] dynamic inventory: ignoring deployedUrl that is not http(s) for ` +\n `${s.name ?? s.path}: ${JSON.stringify(s.deployedUrl)}`,\n );\n const copy = { ...s };\n delete copy.deployedUrl;\n return copy;\n }\n return s;\n });\n}\n\nexport type ResolveSitesInput = {\n site?: string;\n fleet?: string;\n /** Optional workdir for the `--fleet airtable` keyword path (computes site.path as {workdir}/{slug}). */\n workdir?: string;\n cwd: string;\n};\n\nexport async function resolveSites(input: ResolveSitesInput): Promise<Site[]> {\n if (input.site && input.fleet) {\n throw Object.assign(new Error(\"cannot combine a positional [site] with --fleet\"), {\n exitCode: 2,\n });\n }\n\n if (input.fleet === \"airtable\") {\n const { openBase, readAirtableConfig } = await import(\"../../reports/airtable/client.js\");\n const { fromAirtableBase } = await import(\"../../inventory/airtable.js\");\n const base = openBase(readAirtableConfig());\n const provider = fromAirtableBase(base, input.workdir ? { workdir: input.workdir } : {});\n return provider();\n }\n\n if (input.fleet) {\n const fleetPath = resolve(input.cwd, input.fleet);\n const ext = extname(fleetPath).toLowerCase();\n if (ext === \".json\") {\n // fromJsonFile already scheme-allowlists deployedUrl internally.\n return fromJsonFile(fleetPath)();\n }\n if (ext === \".js\" || ext === \".mjs\" || ext === \".cjs\") {\n const mod = (await import(pathToFileURL(fleetPath).href)) as {\n default?: InventoryProvider;\n };\n if (!mod.default || typeof mod.default !== \"function\") {\n throw Object.assign(new Error(`--fleet ${input.fleet}: default export is not a function`), {\n exitCode: 2,\n });\n }\n // A dynamic module's deployedUrl is unvalidated — sanitize before it can\n // reach a deployed audit (the JSON/Airtable providers already do this).\n return sanitizeDynamicSites(await mod.default());\n }\n throw Object.assign(\n new Error(`--fleet ${input.fleet}: unsupported extension ${ext || \"(none)\"}`),\n { exitCode: 2 },\n );\n }\n\n return localPath(resolve(input.cwd, input.site ?? input.cwd))();\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,eAAe;AAU1B,SAAS,qBAAqB,OAAuB;AAC1D,SAAO,MAAM,IAAI,CAAC,MAAM;AACtB,QAAI,EAAE,gBAAgB,UAAa,CAAC,UAAU,EAAE,WAAW,GAAG;AAC5D,cAAQ;AAAA,QACN,+EACK,EAAE,QAAQ,EAAE,IAAI,KAAK,KAAK,UAAU,EAAE,WAAW,CAAC;AAAA,MACzD;AACA,YAAM,OAAO,EAAE,GAAG,EAAE;AACpB,aAAO,KAAK;AACZ,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AACH;AAUA,eAAsB,aAAa,OAA2C;AAC5E,MAAI,MAAM,QAAQ,MAAM,OAAO;AAC7B,UAAM,OAAO,OAAO,IAAI,MAAM,iDAAiD,GAAG;AAAA,MAChF,UAAU;AAAA,IACZ,CAAC;AAAA,EACH;AAEA,MAAI,MAAM,UAAU,YAAY;AAC9B,UAAM,EAAE,UAAU,mBAAmB,IAAI,MAAM,OAAO,sBAAkC;AACxF,UAAM,EAAE,iBAAiB,IAAI,MAAM,OAAO,wBAA6B;AACvE,UAAM,OAAO,SAAS,mBAAmB,CAAC;AAC1C,UAAM,WAAW,iBAAiB,MAAM,MAAM,UAAU,EAAE,SAAS,MAAM,QAAQ,IAAI,CAAC,CAAC;AACvF,WAAO,SAAS;AAAA,EAClB;AAEA,MAAI,MAAM,OAAO;AACf,UAAM,YAAY,QAAQ,MAAM,KAAK,MAAM,KAAK;AAChD,UAAM,MAAM,QAAQ,SAAS,EAAE,YAAY;AAC3C,QAAI,QAAQ,SAAS;AAEnB,aAAO,aAAa,SAAS,EAAE;AAAA,IACjC;AACA,QAAI,QAAQ,SAAS,QAAQ,UAAU,QAAQ,QAAQ;AACrD,YAAM,MAAO,MAAM,OAAO,cAAc,SAAS,EAAE;AAGnD,UAAI,CAAC,IAAI,WAAW,OAAO,IAAI,YAAY,YAAY;AACrD,cAAM,OAAO,OAAO,IAAI,MAAM,WAAW,MAAM,KAAK,oCAAoC,GAAG;AAAA,UACzF,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AAGA,aAAO,qBAAqB,MAAM,IAAI,QAAQ,CAAC;AAAA,IACjD;AACA,UAAM,OAAO;AAAA,MACX,IAAI,MAAM,WAAW,MAAM,KAAK,2BAA2B,OAAO,QAAQ,EAAE;AAAA,MAC5E,EAAE,UAAU,EAAE;AAAA,IAChB;AAAA,EACF;AAEA,SAAO,UAAU,QAAQ,MAAM,KAAK,MAAM,QAAQ,MAAM,GAAG,CAAC,EAAE;AAChE;","names":[]}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultSpawn
|
|
3
|
+
} from "./chunk-HVOOCK6L.js";
|
|
4
|
+
|
|
5
|
+
// src/github/gh.ts
|
|
6
|
+
function assertUrlSegment(kind, value) {
|
|
7
|
+
const structural = /[\s?#%\\]|\.\./;
|
|
8
|
+
if (value.length === 0 || value.startsWith("/") || structural.test(value)) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`unsafe ${kind} for gh api path (illegal characters or traversal): ${JSON.stringify(value)}`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function mapRollupState(state) {
|
|
15
|
+
switch (state) {
|
|
16
|
+
case "SUCCESS":
|
|
17
|
+
return "passing";
|
|
18
|
+
case "FAILURE":
|
|
19
|
+
case "ERROR":
|
|
20
|
+
return "failing";
|
|
21
|
+
case "PENDING":
|
|
22
|
+
case "EXPECTED":
|
|
23
|
+
return "pending";
|
|
24
|
+
default:
|
|
25
|
+
return "none";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function mapMergeable(state) {
|
|
29
|
+
return state === "MERGEABLE" || state === "CONFLICTING" ? state : "UNKNOWN";
|
|
30
|
+
}
|
|
31
|
+
function makeGitHub(deps) {
|
|
32
|
+
const spawn = deps.spawn ?? defaultSpawn;
|
|
33
|
+
const env = { ...process.env, GH_TOKEN: deps.token };
|
|
34
|
+
async function gh(args) {
|
|
35
|
+
const r = await spawn("gh", args, { env, timeoutMs: 6e4 });
|
|
36
|
+
if (r.code !== 0) throw new Error(`gh ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);
|
|
37
|
+
return r.stdout;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
async openPullRequest(repo, pr) {
|
|
41
|
+
const out = await gh([
|
|
42
|
+
"pr",
|
|
43
|
+
"create",
|
|
44
|
+
"--repo",
|
|
45
|
+
repo,
|
|
46
|
+
"--head",
|
|
47
|
+
pr.head,
|
|
48
|
+
"--base",
|
|
49
|
+
pr.base,
|
|
50
|
+
"--title",
|
|
51
|
+
pr.title,
|
|
52
|
+
"--body",
|
|
53
|
+
pr.body
|
|
54
|
+
]);
|
|
55
|
+
return { url: out.trim() };
|
|
56
|
+
},
|
|
57
|
+
async enableRepoAutoMerge(repo) {
|
|
58
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-F", "allow_auto_merge=true"]);
|
|
59
|
+
},
|
|
60
|
+
async protectBranch(repo, branch, requiredChecks) {
|
|
61
|
+
assertUrlSegment("branch", branch);
|
|
62
|
+
const args = [
|
|
63
|
+
"api",
|
|
64
|
+
"-X",
|
|
65
|
+
"PUT",
|
|
66
|
+
`repos/${repo}/branches/${branch}/protection`,
|
|
67
|
+
"-H",
|
|
68
|
+
"Accept: application/vnd.github+json",
|
|
69
|
+
"-F",
|
|
70
|
+
"required_status_checks[strict]=true",
|
|
71
|
+
...requiredChecks.flatMap((c) => ["-f", `required_status_checks[contexts][]=${c}`]),
|
|
72
|
+
"-F",
|
|
73
|
+
"enforce_admins=true",
|
|
74
|
+
"-F",
|
|
75
|
+
"required_pull_request_reviews=null",
|
|
76
|
+
"-F",
|
|
77
|
+
"restrictions=null"
|
|
78
|
+
];
|
|
79
|
+
await gh(args);
|
|
80
|
+
},
|
|
81
|
+
async setRepoSecret(repo, name, value) {
|
|
82
|
+
await gh(["secret", "set", name, "--repo", repo, "--body", value]);
|
|
83
|
+
},
|
|
84
|
+
async repoExists(repo) {
|
|
85
|
+
const r = await spawn("gh", ["api", `repos/${repo}`], { env, timeoutMs: 6e4 });
|
|
86
|
+
return r.code === 0;
|
|
87
|
+
},
|
|
88
|
+
async defaultBranch(repo) {
|
|
89
|
+
const out = await gh(["api", `repos/${repo}`, "--jq", ".default_branch"]);
|
|
90
|
+
return out.trim();
|
|
91
|
+
},
|
|
92
|
+
// filesOnBranch and branchProtectionContexts call `spawn` directly (not the
|
|
93
|
+
// throwing `gh()` helper) because a 404 is an expected, meaningful answer —
|
|
94
|
+
// "file/protection absent" — not an error. The remaining readers use `gh()`
|
|
95
|
+
// since a non-200 there is a genuine failure (e.g. missing token scope).
|
|
96
|
+
async filesOnBranch(repo, branch, paths) {
|
|
97
|
+
assertUrlSegment("branch", branch);
|
|
98
|
+
const present = [];
|
|
99
|
+
for (const p of paths) {
|
|
100
|
+
assertUrlSegment("path", p);
|
|
101
|
+
const r = await spawn("gh", [`api`, `repos/${repo}/contents/${p}?ref=${branch}`], {
|
|
102
|
+
env,
|
|
103
|
+
timeoutMs: 6e4
|
|
104
|
+
});
|
|
105
|
+
if (r.code === 0) present.push(p);
|
|
106
|
+
}
|
|
107
|
+
return present;
|
|
108
|
+
},
|
|
109
|
+
async branchProtectionContexts(repo, branch) {
|
|
110
|
+
assertUrlSegment("branch", branch);
|
|
111
|
+
const r = await spawn(
|
|
112
|
+
"gh",
|
|
113
|
+
[
|
|
114
|
+
"api",
|
|
115
|
+
`repos/${repo}/branches/${branch}/protection`,
|
|
116
|
+
"--jq",
|
|
117
|
+
".required_status_checks.contexts[]?"
|
|
118
|
+
],
|
|
119
|
+
{ env, timeoutMs: 6e4 }
|
|
120
|
+
);
|
|
121
|
+
if (r.code !== 0) return [];
|
|
122
|
+
return r.stdout.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
123
|
+
},
|
|
124
|
+
async secretExists(repo, name) {
|
|
125
|
+
const out = await gh([
|
|
126
|
+
"api",
|
|
127
|
+
`repos/${repo}/actions/secrets?per_page=100`,
|
|
128
|
+
"--jq",
|
|
129
|
+
".secrets[].name"
|
|
130
|
+
]);
|
|
131
|
+
return out.split("\n").map((l) => l.trim()).includes(name);
|
|
132
|
+
},
|
|
133
|
+
async autoMergeEnabled(repo) {
|
|
134
|
+
const out = await gh(["api", `repos/${repo}`, "--jq", ".allow_auto_merge"]);
|
|
135
|
+
return out.trim() === "true";
|
|
136
|
+
},
|
|
137
|
+
async findOpenSelfUpdatingPR(repo) {
|
|
138
|
+
const out = await gh([
|
|
139
|
+
"api",
|
|
140
|
+
`repos/${repo}/pulls?state=open&per_page=100`,
|
|
141
|
+
"--jq",
|
|
142
|
+
'.[] | select(.head.ref | startswith("maint/self-updating-")) | .html_url'
|
|
143
|
+
]);
|
|
144
|
+
const first = out.split("\n").map((l) => l.trim()).find((l) => l.length > 0);
|
|
145
|
+
return first ?? null;
|
|
146
|
+
},
|
|
147
|
+
async openPullRequests(repo) {
|
|
148
|
+
const [owner, name, ...rest] = repo.split("/");
|
|
149
|
+
if (!owner || !name || rest.length > 0) {
|
|
150
|
+
throw new Error(`openPullRequests: expected "owner/repo", got "${repo}"`);
|
|
151
|
+
}
|
|
152
|
+
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName mergeable commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}";
|
|
153
|
+
const out = await gh([
|
|
154
|
+
"api",
|
|
155
|
+
"graphql",
|
|
156
|
+
"-f",
|
|
157
|
+
`query=${query}`,
|
|
158
|
+
"-F",
|
|
159
|
+
`owner=${owner}`,
|
|
160
|
+
"-F",
|
|
161
|
+
`name=${name}`
|
|
162
|
+
]);
|
|
163
|
+
const parsed = JSON.parse(out);
|
|
164
|
+
const nodes = parsed.data?.repository?.pullRequests?.nodes ?? [];
|
|
165
|
+
return nodes.map((n) => ({
|
|
166
|
+
number: n.number,
|
|
167
|
+
title: n.title,
|
|
168
|
+
url: n.url,
|
|
169
|
+
headRef: n.headRefName,
|
|
170
|
+
ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state),
|
|
171
|
+
mergeable: mapMergeable(n.mergeable)
|
|
172
|
+
}));
|
|
173
|
+
},
|
|
174
|
+
async defaultBranchStatus(repo) {
|
|
175
|
+
const [owner, name, ...rest] = repo.split("/");
|
|
176
|
+
if (!owner || !name || rest.length > 0) {
|
|
177
|
+
throw new Error(`defaultBranchStatus: expected "owner/repo", got "${repo}"`);
|
|
178
|
+
}
|
|
179
|
+
const query = "query($owner:String!,$name:String!){repository(owner:$owner,name:$name){defaultBranchRef{target{... on Commit{committedDate statusCheckRollup{state}}}}}}";
|
|
180
|
+
const out = await gh([
|
|
181
|
+
"api",
|
|
182
|
+
"graphql",
|
|
183
|
+
"-f",
|
|
184
|
+
`query=${query}`,
|
|
185
|
+
"-F",
|
|
186
|
+
`owner=${owner}`,
|
|
187
|
+
"-F",
|
|
188
|
+
`name=${name}`
|
|
189
|
+
]);
|
|
190
|
+
const parsed = JSON.parse(out);
|
|
191
|
+
const target = parsed.data?.repository?.defaultBranchRef?.target;
|
|
192
|
+
return {
|
|
193
|
+
ciState: mapRollupState(target?.statusCheckRollup?.state),
|
|
194
|
+
lastCommitAt: target?.committedDate ?? null
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
async dispatchWorkflow(repo, workflow, ref) {
|
|
198
|
+
const [owner, name, ...rest] = repo.split("/");
|
|
199
|
+
if (!owner || !name || rest.length > 0) {
|
|
200
|
+
throw new Error(`dispatchWorkflow: expected "owner/repo", got "${repo}"`);
|
|
201
|
+
}
|
|
202
|
+
assertUrlSegment("path", owner);
|
|
203
|
+
assertUrlSegment("path", name);
|
|
204
|
+
assertUrlSegment("path", workflow);
|
|
205
|
+
assertUrlSegment("branch", ref);
|
|
206
|
+
await gh([
|
|
207
|
+
"api",
|
|
208
|
+
"-X",
|
|
209
|
+
"POST",
|
|
210
|
+
`repos/${owner}/${name}/actions/workflows/${workflow}/dispatches`,
|
|
211
|
+
"-f",
|
|
212
|
+
`ref=${ref}`
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export {
|
|
219
|
+
makeGitHub
|
|
220
|
+
};
|
|
221
|
+
//# sourceMappingURL=chunk-J4ECCFRT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/github/gh.ts"],"sourcesContent":["import { defaultSpawn, type SpawnFn } from \"../audits/util/spawn.js\";\n\n/** Aggregate CI state of a PR's head commit, normalized from GitHub's rollup. */\nexport type CiState = \"passing\" | \"failing\" | \"pending\" | \"none\";\n\n/** GitHub's computed mergeability of a PR. `UNKNOWN` is transient — GitHub is\n * still computing it (e.g. right after a push) — so it should be read as \"not\n * known to conflict\", never as conflicting. */\nexport type PrMergeable = \"MERGEABLE\" | \"CONFLICTING\" | \"UNKNOWN\";\n\n/** A minimal open-PR summary with its head-commit CI rollup state + mergeability. */\nexport type PullRequestSummary = {\n number: number;\n title: string;\n url: string;\n headRef: string;\n ciState: CiState;\n mergeable: PrMergeable;\n};\n\n/**\n * Reject a value before it's interpolated into a `gh api` URL path. The\n * `owner/repo` split methods already validate shape; this is the defense-in-depth\n * guard for the `branch` and file-`path` segments. An unexpected value (`..`, a\n * leading `/`, whitespace, or a URL-structural char like `?#%` or a backslash)\n * could otherwise retarget the endpoint (escape the intended path, smuggle a\n * query string, or traverse). Conservative by design — legit branch names like\n * `maint/self-updating-x` and paths like `.github/workflows/ci.yml` pass.\n *\n * Both branch refs (`maint/self-updating-x`) and file paths\n * (`.github/workflows/ci.yml`) legitimately contain `/`, so a single slash is\n * allowed; what's rejected is `..`, a leading `/`, whitespace, or a\n * URL-structural char (`?`, `#`, `%`, backslash) that could escape or retarget\n * the endpoint.\n */\nfunction assertUrlSegment(kind: \"branch\" | \"path\", value: string): void {\n const structural = /[\\s?#%\\\\]|\\.\\./;\n if (value.length === 0 || value.startsWith(\"/\") || structural.test(value)) {\n throw new Error(\n `unsafe ${kind} for gh api path (illegal characters or traversal): ${JSON.stringify(value)}`,\n );\n }\n}\n\n/** Map GitHub's `statusCheckRollup.state` enum to our normalized CiState. */\nfunction mapRollupState(state: string | null | undefined): CiState {\n switch (state) {\n case \"SUCCESS\":\n return \"passing\";\n case \"FAILURE\":\n case \"ERROR\":\n return \"failing\";\n case \"PENDING\":\n case \"EXPECTED\":\n return \"pending\";\n default:\n return \"none\"; // null/undefined = no checks reported\n }\n}\n\n/** Coerce GitHub's `mergeable` enum to our PrMergeable. Anything unexpected\n * (including the literal `UNKNOWN` GitHub returns while still computing) maps to\n * `UNKNOWN` — i.e. \"not known to conflict\". Only an explicit `CONFLICTING` is. */\nfunction mapMergeable(state: string | null | undefined): PrMergeable {\n return state === \"MERGEABLE\" || state === \"CONFLICTING\" ? state : \"UNKNOWN\";\n}\n\nexport type GitHub = {\n openPullRequest: (\n repo: string,\n pr: { head: string; base: string; title: string; body: string },\n ) => Promise<{ url: string }>;\n enableRepoAutoMerge: (repo: string) => Promise<void>;\n protectBranch: (repo: string, branch: string, requiredChecks: string[]) => Promise<void>;\n setRepoSecret: (repo: string, name: string, value: string) => Promise<void>;\n repoExists: (repo: string) => Promise<boolean>;\n defaultBranch: (repo: string) => Promise<string>;\n filesOnBranch: (repo: string, branch: string, paths: string[]) => Promise<string[]>;\n branchProtectionContexts: (repo: string, branch: string) => Promise<string[]>;\n secretExists: (repo: string, name: string) => Promise<boolean>;\n autoMergeEnabled: (repo: string) => Promise<boolean>;\n findOpenSelfUpdatingPR: (repo: string) => Promise<string | null>;\n /** All open PRs on a repo with each head commit's normalized CI rollup state. */\n openPullRequests: (repo: string) => Promise<PullRequestSummary[]>;\n /** The default branch's latest-commit date + normalized CI rollup, one query. */\n defaultBranchStatus: (repo: string) => Promise<{ ciState: CiState; lastCommitAt: string | null }>;\n /** Fire a `workflow_dispatch` for `<workflow>` (a filename like `renovate.yml`)\n * on `ref`. Requires the token's `actions:write` scope; a 404 (no such\n * workflow) or 403 (missing scope) surfaces as a thrown error. */\n dispatchWorkflow: (repo: string, workflow: string, ref: string) => Promise<void>;\n};\n\nexport function makeGitHub(deps: { token: string; spawn?: SpawnFn }): GitHub {\n const spawn = deps.spawn ?? defaultSpawn;\n const env = { ...process.env, GH_TOKEN: deps.token };\n\n async function gh(args: string[]): Promise<string> {\n const r = await spawn(\"gh\", args, { env, timeoutMs: 60_000 });\n if (r.code !== 0) throw new Error(`gh ${args[0]} failed (code ${r.code}): ${r.stderr.trim()}`);\n return r.stdout;\n }\n\n return {\n async openPullRequest(repo, pr) {\n const out = await gh([\n \"pr\",\n \"create\",\n \"--repo\",\n repo,\n \"--head\",\n pr.head,\n \"--base\",\n pr.base,\n \"--title\",\n pr.title,\n \"--body\",\n pr.body,\n ]);\n return { url: out.trim() };\n },\n async enableRepoAutoMerge(repo) {\n await gh([\"api\", \"-X\", \"PATCH\", `repos/${repo}`, \"-F\", \"allow_auto_merge=true\"]);\n },\n async protectBranch(repo, branch, requiredChecks) {\n assertUrlSegment(\"branch\", branch);\n const args = [\n \"api\",\n \"-X\",\n \"PUT\",\n `repos/${repo}/branches/${branch}/protection`,\n \"-H\",\n \"Accept: application/vnd.github+json\",\n \"-F\",\n \"required_status_checks[strict]=true\",\n ...requiredChecks.flatMap((c) => [\"-f\", `required_status_checks[contexts][]=${c}`]),\n \"-F\",\n \"enforce_admins=true\",\n \"-F\",\n \"required_pull_request_reviews=null\",\n \"-F\",\n \"restrictions=null\",\n ];\n await gh(args);\n },\n async setRepoSecret(repo, name, value) {\n await gh([\"secret\", \"set\", name, \"--repo\", repo, \"--body\", value]);\n },\n async repoExists(repo) {\n const r = await spawn(\"gh\", [\"api\", `repos/${repo}`], { env, timeoutMs: 60_000 });\n return r.code === 0;\n },\n async defaultBranch(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".default_branch\"]);\n return out.trim();\n },\n // filesOnBranch and branchProtectionContexts call `spawn` directly (not the\n // throwing `gh()` helper) because a 404 is an expected, meaningful answer —\n // \"file/protection absent\" — not an error. The remaining readers use `gh()`\n // since a non-200 there is a genuine failure (e.g. missing token scope).\n async filesOnBranch(repo, branch, paths) {\n assertUrlSegment(\"branch\", branch);\n const present: string[] = [];\n for (const p of paths) {\n assertUrlSegment(\"path\", p);\n const r = await spawn(\"gh\", [`api`, `repos/${repo}/contents/${p}?ref=${branch}`], {\n env,\n timeoutMs: 60_000,\n });\n if (r.code === 0) present.push(p);\n }\n return present;\n },\n async branchProtectionContexts(repo, branch) {\n assertUrlSegment(\"branch\", branch);\n const r = await spawn(\n \"gh\",\n [\n \"api\",\n `repos/${repo}/branches/${branch}/protection`,\n \"--jq\",\n \".required_status_checks.contexts[]?\",\n ],\n { env, timeoutMs: 60_000 },\n );\n if (r.code !== 0) return []; // 404 = no protection configured\n return r.stdout\n .split(\"\\n\")\n .map((l) => l.trim())\n .filter((l) => l.length > 0);\n },\n async secretExists(repo, name) {\n // per_page=100: the REST default of 30 would false-negative on a repo with >30 secrets,\n // wrongly reporting an existing secret absent (→ a needless overwrite).\n const out = await gh([\n \"api\",\n `repos/${repo}/actions/secrets?per_page=100`,\n \"--jq\",\n \".secrets[].name\",\n ]);\n return out\n .split(\"\\n\")\n .map((l) => l.trim())\n .includes(name);\n },\n async autoMergeEnabled(repo) {\n const out = await gh([\"api\", `repos/${repo}`, \"--jq\", \".allow_auto_merge\"]);\n return out.trim() === \"true\";\n },\n async findOpenSelfUpdatingPR(repo) {\n // per_page=100: with the REST default of 30, a repo with >30 open PRs (plausible under\n // Renovate) could page past the existing self-updating PR and open a duplicate.\n const out = await gh([\n \"api\",\n `repos/${repo}/pulls?state=open&per_page=100`,\n \"--jq\",\n '.[] | select(.head.ref | startswith(\"maint/self-updating-\")) | .html_url',\n ]);\n const first = out\n .split(\"\\n\")\n .map((l) => l.trim())\n .find((l) => l.length > 0);\n return first ?? null;\n },\n async openPullRequests(repo) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`openPullRequests: expected \"owner/repo\", got \"${repo}\"`);\n }\n const query =\n \"query($owner:String!,$name:String!){repository(owner:$owner,name:$name){\" +\n \"pullRequests(states:OPEN,first:100,orderBy:{field:CREATED_AT,direction:DESC}){nodes{number title url headRefName mergeable \" +\n \"commits(last:1){nodes{commit{statusCheckRollup{state}}}}}}}}\";\n const out = await gh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `name=${name}`,\n ]);\n const parsed = JSON.parse(out) as {\n data?: {\n repository?: {\n pullRequests?: {\n nodes?: Array<{\n number: number;\n title: string;\n url: string;\n headRefName: string;\n mergeable?: string;\n commits?: {\n nodes?: Array<{ commit?: { statusCheckRollup?: { state?: string } } }>;\n };\n }>;\n };\n };\n };\n };\n const nodes = parsed.data?.repository?.pullRequests?.nodes ?? [];\n return nodes.map((n) => ({\n number: n.number,\n title: n.title,\n url: n.url,\n headRef: n.headRefName,\n ciState: mapRollupState(n.commits?.nodes?.[0]?.commit?.statusCheckRollup?.state),\n mergeable: mapMergeable(n.mergeable),\n }));\n },\n async defaultBranchStatus(repo) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`defaultBranchStatus: expected \"owner/repo\", got \"${repo}\"`);\n }\n const query =\n \"query($owner:String!,$name:String!){repository(owner:$owner,name:$name){\" +\n \"defaultBranchRef{target{... on Commit{committedDate statusCheckRollup{state}}}}}}\";\n const out = await gh([\n \"api\",\n \"graphql\",\n \"-f\",\n `query=${query}`,\n \"-F\",\n `owner=${owner}`,\n \"-F\",\n `name=${name}`,\n ]);\n const parsed = JSON.parse(out) as {\n data?: {\n repository?: {\n defaultBranchRef?: {\n target?: { committedDate?: string; statusCheckRollup?: { state?: string } | null };\n } | null;\n };\n };\n };\n const target = parsed.data?.repository?.defaultBranchRef?.target;\n return {\n ciState: mapRollupState(target?.statusCheckRollup?.state),\n lastCommitAt: target?.committedDate ?? null,\n };\n },\n async dispatchWorkflow(repo, workflow, ref) {\n const [owner, name, ...rest] = repo.split(\"/\");\n if (!owner || !name || rest.length > 0) {\n throw new Error(`dispatchWorkflow: expected \"owner/repo\", got \"${repo}\"`);\n }\n // Every segment interpolates into the API path, so guard them all like the\n // other write methods do (defense in depth). `owner`/`name` are the most\n // operator-controlled (typed into Airtable's \"Git repo\"); `workflow` is a\n // constant today; `ref` is repo-sourced. A junk value like `repo?x=1` would\n // otherwise smuggle a query string past the bare two-part shape check.\n assertUrlSegment(\"path\", owner);\n assertUrlSegment(\"path\", name);\n assertUrlSegment(\"path\", workflow);\n assertUrlSegment(\"branch\", ref);\n await gh([\n \"api\",\n \"-X\",\n \"POST\",\n `repos/${owner}/${name}/actions/workflows/${workflow}/dispatches`,\n \"-f\",\n `ref=${ref}`,\n ]);\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ALL_TEMPLATES,
|
|
3
|
+
templatesByName
|
|
4
|
+
} from "./chunk-CASCWQQX.js";
|
|
5
|
+
import {
|
|
6
|
+
withRecipe
|
|
7
|
+
} from "./chunk-XKOZUVK2.js";
|
|
8
|
+
import {
|
|
9
|
+
listTrackedFiles,
|
|
10
|
+
removeFromIndex
|
|
11
|
+
} from "./chunk-XTK5VIQB.js";
|
|
12
|
+
|
|
13
|
+
// src/recipes/sync-configs.ts
|
|
14
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
15
|
+
import { join, dirname } from "path";
|
|
16
|
+
|
|
17
|
+
// src/recipes/sync-configs/gitignore.ts
|
|
18
|
+
var MANAGED_MARKER = "# canonical entries from @reddoorla/maintenance sync-configs";
|
|
19
|
+
var CANONICAL_GITIGNORE_ENTRIES = [
|
|
20
|
+
"node_modules/",
|
|
21
|
+
"build/",
|
|
22
|
+
"dist/",
|
|
23
|
+
".svelte-kit/",
|
|
24
|
+
"coverage/",
|
|
25
|
+
".vitest-cache/",
|
|
26
|
+
"playwright-report/",
|
|
27
|
+
"test-results/",
|
|
28
|
+
".lighthouseci/",
|
|
29
|
+
".tsbuildinfo",
|
|
30
|
+
".env",
|
|
31
|
+
".env.*",
|
|
32
|
+
"!.env.example",
|
|
33
|
+
".DS_Store",
|
|
34
|
+
"*.log",
|
|
35
|
+
".vercel/",
|
|
36
|
+
".netlify/",
|
|
37
|
+
".reddoor-a11y/",
|
|
38
|
+
// The a11y audit's transient spec dir, written inside the checkout and
|
|
39
|
+
// normally cleaned, but a timeout-SIGKILL of the parent orphans it. Ignored
|
|
40
|
+
// fleet-wide so it never dirties a self-updating repo's tree (2026-06-10 M-D).
|
|
41
|
+
".reddoor-a11y-spec-*/"
|
|
42
|
+
];
|
|
43
|
+
function stripLeadingSlash(s) {
|
|
44
|
+
return s.startsWith("/") ? s.slice(1) : s;
|
|
45
|
+
}
|
|
46
|
+
function stripTrailingSlash(s) {
|
|
47
|
+
return s.endsWith("/") ? s.slice(0, -1) : s;
|
|
48
|
+
}
|
|
49
|
+
function normalizePresence(line) {
|
|
50
|
+
return stripTrailingSlash(stripLeadingSlash(line.trim()));
|
|
51
|
+
}
|
|
52
|
+
function presentSet(existing) {
|
|
53
|
+
const set = /* @__PURE__ */ new Set();
|
|
54
|
+
for (const raw of existing.split(/\r?\n/)) {
|
|
55
|
+
const trimmed = raw.trim();
|
|
56
|
+
if (!trimmed) continue;
|
|
57
|
+
if (trimmed.startsWith("#")) continue;
|
|
58
|
+
set.add(normalizePresence(trimmed));
|
|
59
|
+
}
|
|
60
|
+
return set;
|
|
61
|
+
}
|
|
62
|
+
function mergeGitignore(existing, canonical) {
|
|
63
|
+
if (existing === null) {
|
|
64
|
+
const body = [MANAGED_MARKER, ...canonical].join("\n") + "\n";
|
|
65
|
+
return { content: body, added: [...canonical] };
|
|
66
|
+
}
|
|
67
|
+
const present = presentSet(existing);
|
|
68
|
+
const added = [];
|
|
69
|
+
for (const entry of canonical) {
|
|
70
|
+
const norm = normalizePresence(entry);
|
|
71
|
+
if (!present.has(norm)) {
|
|
72
|
+
added.push(entry);
|
|
73
|
+
present.add(norm);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (added.length === 0) {
|
|
77
|
+
return { content: existing, added: [] };
|
|
78
|
+
}
|
|
79
|
+
let base = existing;
|
|
80
|
+
if (!base.endsWith("\n")) base += "\n";
|
|
81
|
+
const block = ["", MANAGED_MARKER, ...added].join("\n") + "\n";
|
|
82
|
+
return { content: base + block, added };
|
|
83
|
+
}
|
|
84
|
+
function findTrackedArtifacts(tracked, canonical) {
|
|
85
|
+
const dirEntries = [];
|
|
86
|
+
for (const raw of canonical) {
|
|
87
|
+
const t = raw.trim();
|
|
88
|
+
if (!t) continue;
|
|
89
|
+
if (t.startsWith("!")) continue;
|
|
90
|
+
if (/[*?[]/.test(t)) continue;
|
|
91
|
+
const noLead = stripLeadingSlash(t);
|
|
92
|
+
if (!noLead.endsWith("/")) continue;
|
|
93
|
+
const name = stripTrailingSlash(noLead);
|
|
94
|
+
if (!name) continue;
|
|
95
|
+
dirEntries.push(name);
|
|
96
|
+
}
|
|
97
|
+
const matched = [];
|
|
98
|
+
for (const path of tracked) {
|
|
99
|
+
for (const dir of dirEntries) {
|
|
100
|
+
if (path === dir || path.startsWith(dir + "/")) {
|
|
101
|
+
matched.push(path);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return matched;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// src/recipes/sync-configs.ts
|
|
110
|
+
var GITIGNORE_CONFIG = "gitignore";
|
|
111
|
+
var SVELTE_CONFIG = "svelte";
|
|
112
|
+
var NETLIFY_CONFIG = "netlify";
|
|
113
|
+
function isSvelteConfigCompliant(contents) {
|
|
114
|
+
return contents.includes("createSvelteConfig") && contents.includes("@sveltejs/adapter-netlify");
|
|
115
|
+
}
|
|
116
|
+
var SECURITY_HEADER_RE = /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;
|
|
117
|
+
function isNetlifyConfigCompliant(contents) {
|
|
118
|
+
return contents.includes("[[headers]]") && SECURITY_HEADER_RE.test(contents);
|
|
119
|
+
}
|
|
120
|
+
var ALL_CONFIG_NAMES = [
|
|
121
|
+
"lighthouse",
|
|
122
|
+
"eslint",
|
|
123
|
+
"prettier",
|
|
124
|
+
"prettier-ignore",
|
|
125
|
+
"playwright-a11y",
|
|
126
|
+
"svelte",
|
|
127
|
+
"gitignore",
|
|
128
|
+
"ci",
|
|
129
|
+
"renovate-action",
|
|
130
|
+
"renovate-config",
|
|
131
|
+
"netlify"
|
|
132
|
+
];
|
|
133
|
+
function isConfigName(value) {
|
|
134
|
+
return ALL_CONFIG_NAMES.includes(value);
|
|
135
|
+
}
|
|
136
|
+
async function readMaybe(path) {
|
|
137
|
+
try {
|
|
138
|
+
return await readFile(path, "utf-8");
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function planTemplateDiffs(cwd, templates) {
|
|
144
|
+
const diffs = [];
|
|
145
|
+
for (const t of templates) {
|
|
146
|
+
const existing = await readMaybe(join(cwd, t.path));
|
|
147
|
+
if (existing === t.contents) continue;
|
|
148
|
+
if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
diffs.push(t);
|
|
155
|
+
}
|
|
156
|
+
return diffs;
|
|
157
|
+
}
|
|
158
|
+
async function planGitignore(cwd) {
|
|
159
|
+
const existing = await readMaybe(join(cwd, ".gitignore"));
|
|
160
|
+
const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);
|
|
161
|
+
const tracked = await listTrackedFiles(cwd);
|
|
162
|
+
const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);
|
|
163
|
+
if (merge.added.length === 0 && toUntrack.length === 0) return { kind: "noop" };
|
|
164
|
+
return { kind: "apply", content: merge.content, toUntrack, added: merge.added };
|
|
165
|
+
}
|
|
166
|
+
async function applyGitignore(cwd, plan) {
|
|
167
|
+
await writeFile(join(cwd, ".gitignore"), plan.content, "utf-8");
|
|
168
|
+
if (plan.toUntrack.length > 0) {
|
|
169
|
+
await removeFromIndex(cwd, plan.toUntrack);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
async function syncConfigs(site, opts = {}) {
|
|
173
|
+
const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);
|
|
174
|
+
const templateNames = requested.filter((c) => c !== GITIGNORE_CONFIG);
|
|
175
|
+
const templates = templatesByName(templateNames);
|
|
176
|
+
const includeGitignore = requested.includes(GITIGNORE_CONFIG);
|
|
177
|
+
return withRecipe({
|
|
178
|
+
name: "sync-configs",
|
|
179
|
+
site,
|
|
180
|
+
plan: async () => {
|
|
181
|
+
const templateDiffs = await planTemplateDiffs(site.path, templates);
|
|
182
|
+
const gitignorePlan = includeGitignore ? await planGitignore(site.path) : { kind: "noop" };
|
|
183
|
+
if (templateDiffs.length === 0 && gitignorePlan.kind === "noop") {
|
|
184
|
+
return { kind: "noop", notes: "all targeted configs already match" };
|
|
185
|
+
}
|
|
186
|
+
return { kind: "apply", plan: { templateDiffs, gitignorePlan } };
|
|
187
|
+
},
|
|
188
|
+
apply: async ({ templateDiffs, gitignorePlan }, { commit }) => {
|
|
189
|
+
for (const t of templateDiffs) {
|
|
190
|
+
const dest = join(site.path, t.path);
|
|
191
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
192
|
+
await writeFile(dest, t.contents, "utf-8");
|
|
193
|
+
await commit(`chore: sync ${t.config} config from @reddoorla/maintenance`);
|
|
194
|
+
}
|
|
195
|
+
if (gitignorePlan.kind === "apply") {
|
|
196
|
+
await applyGitignore(site.path, gitignorePlan);
|
|
197
|
+
await commit(`chore: sync gitignore from @reddoorla/maintenance`);
|
|
198
|
+
}
|
|
199
|
+
return { kind: "ok" };
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export {
|
|
205
|
+
CANONICAL_GITIGNORE_ENTRIES,
|
|
206
|
+
mergeGitignore,
|
|
207
|
+
ALL_CONFIG_NAMES,
|
|
208
|
+
isConfigName,
|
|
209
|
+
syncConfigs
|
|
210
|
+
};
|
|
211
|
+
//# sourceMappingURL=chunk-JNXCDUYB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/recipes/sync-configs.ts","../src/recipes/sync-configs/gitignore.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { join, dirname } from \"node:path\";\nimport type { RecipeResult, Site, ConfigName } from \"../types.js\";\nimport { ALL_TEMPLATES, templatesByName, type ConfigTemplate } from \"./sync-configs/templates.js\";\nimport {\n CANONICAL_GITIGNORE_ENTRIES,\n mergeGitignore,\n findTrackedArtifacts,\n} from \"./sync-configs/gitignore.js\";\nimport { listTrackedFiles, removeFromIndex } from \"../util/git.js\";\nimport { withRecipe } from \"./_with-recipe.js\";\n\nexport type SyncConfigsOptions = {\n which?: ConfigName[];\n};\n\nconst GITIGNORE_CONFIG: ConfigName = \"gitignore\";\nconst SVELTE_CONFIG: ConfigName = \"svelte\";\nconst NETLIFY_CONFIG: ConfigName = \"netlify\";\n\n/** A site's `svelte.config.js` is \"compliant\" — and left untouched by sync —\n * once it builds on the canonical helpers (createSvelteConfig + adapter-netlify).\n *\n * Unlike the other exact-match templates, svelte.config legitimately carries\n * site-specific `kit.alias` and `compilerOptions`; an exact overwrite would\n * clobber those on every sync (it silently dropped MSOT's $utils alias,\n * 2026-06-04). So once a config is on the canonical pattern we preserve it as-is\n * and only rewrite a genuinely off-pattern (or missing) config. `createSvelteConfig`\n * now provides the canonical `$lib` aliases itself, and a site's own `kit.alias`\n * overrides per key (and may add more), so a site's additive customization is safe\n * to preserve. */\nfunction isSvelteConfigCompliant(contents: string): boolean {\n return contents.includes(\"createSvelteConfig\") && contents.includes(\"@sveltejs/adapter-netlify\");\n}\n\n/** Any of the baseline security headers — the marker that a netlify.toml is\n * deliberately hardened (vs. e.g. a cache-control-only `[[headers]]` block). */\nconst SECURITY_HEADER_RE =\n /Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options|Referrer-Policy|Permissions-Policy|Cross-Origin-Opener-Policy/i;\n\n/** A site's `netlify.toml` is \"compliant\" — and left untouched by sync — once it\n * carries a `[[headers]]` block AND a security header (HSTS/CSP/X-Frame-Options/…).\n *\n * Like svelte.config, netlify.toml legitimately holds site-specific config\n * (custom CSP, redirects, per-route headers). The canonical template ships the\n * baseline security headers, but an exact overwrite would CLOBBER a site's own\n * hardening — that bug stripped gallerysonder's headers on a routine sync\n * (2026-06-10). So a genuinely-hardened file is left alone, while a missing,\n * header-less (previously-stripped), OR merely cache-header file is non-compliant\n * and gets the canonical template, which backfills the security baseline. */\nfunction isNetlifyConfigCompliant(contents: string): boolean {\n return contents.includes(\"[[headers]]\") && SECURITY_HEADER_RE.test(contents);\n}\n\n/** Runtime enumeration of every `ConfigName`. Mirror of the union in\n * `src/types.ts`. Used by CLI `--only` validation; a missing entry would\n * silently accept typos. The type-test in `tests/types.test.ts` guards\n * against drift between this array and the union. */\nexport const ALL_CONFIG_NAMES: ConfigName[] = [\n \"lighthouse\",\n \"eslint\",\n \"prettier\",\n \"prettier-ignore\",\n \"playwright-a11y\",\n \"svelte\",\n \"gitignore\",\n \"ci\",\n \"renovate-action\",\n \"renovate-config\",\n \"netlify\",\n];\n\nexport function isConfigName(value: string): value is ConfigName {\n return (ALL_CONFIG_NAMES as string[]).includes(value);\n}\n\nasync function readMaybe(path: string): Promise<string | null> {\n try {\n return await readFile(path, \"utf-8\");\n } catch {\n return null;\n }\n}\n\nasync function planTemplateDiffs(\n cwd: string,\n templates: ConfigTemplate[],\n): Promise<ConfigTemplate[]> {\n const diffs: ConfigTemplate[] = [];\n for (const t of templates) {\n const existing = await readMaybe(join(cwd, t.path));\n if (existing === t.contents) continue;\n // svelte.config is compliance-checked, not exact-matched: an existing config\n // already on the canonical pattern is left alone so its aliases/compilerOptions\n // survive. A missing (null) or off-pattern config still gets the canonical template.\n if (t.config === SVELTE_CONFIG && existing !== null && isSvelteConfigCompliant(existing)) {\n continue;\n }\n // netlify.toml is likewise compliance-checked: a file that already carries\n // `[[headers]]` is hardened and left alone (an exact overwrite would strip\n // its security headers). A header-less / missing file gets the template.\n if (t.config === NETLIFY_CONFIG && existing !== null && isNetlifyConfigCompliant(existing)) {\n continue;\n }\n diffs.push(t);\n }\n return diffs;\n}\n\ntype GitignorePlan =\n | { kind: \"noop\" }\n | { kind: \"apply\"; content: string; toUntrack: string[]; added: string[] };\n\nasync function planGitignore(cwd: string): Promise<GitignorePlan> {\n const existing = await readMaybe(join(cwd, \".gitignore\"));\n const merge = mergeGitignore(existing, CANONICAL_GITIGNORE_ENTRIES);\n const tracked = await listTrackedFiles(cwd);\n const toUntrack = findTrackedArtifacts(tracked, CANONICAL_GITIGNORE_ENTRIES);\n if (merge.added.length === 0 && toUntrack.length === 0) return { kind: \"noop\" };\n return { kind: \"apply\", content: merge.content, toUntrack, added: merge.added };\n}\n\nasync function applyGitignore(\n cwd: string,\n plan: Extract<GitignorePlan, { kind: \"apply\" }>,\n): Promise<void> {\n await writeFile(join(cwd, \".gitignore\"), plan.content, \"utf-8\");\n if (plan.toUntrack.length > 0) {\n await removeFromIndex(cwd, plan.toUntrack);\n }\n}\n\nexport async function syncConfigs(\n site: Site,\n opts: SyncConfigsOptions = {},\n): Promise<RecipeResult> {\n const requested = opts.which ?? ALL_TEMPLATES.map((t) => t.config).concat(GITIGNORE_CONFIG);\n const templateNames = requested.filter((c): c is ConfigName => c !== GITIGNORE_CONFIG);\n const templates = templatesByName(templateNames);\n const includeGitignore = requested.includes(GITIGNORE_CONFIG);\n\n return withRecipe({\n name: \"sync-configs\",\n site,\n plan: async () => {\n const templateDiffs = await planTemplateDiffs(site.path, templates);\n const gitignorePlan: GitignorePlan = includeGitignore\n ? await planGitignore(site.path)\n : { kind: \"noop\" };\n if (templateDiffs.length === 0 && gitignorePlan.kind === \"noop\") {\n return { kind: \"noop\", notes: \"all targeted configs already match\" };\n }\n return { kind: \"apply\", plan: { templateDiffs, gitignorePlan } };\n },\n apply: async ({ templateDiffs, gitignorePlan }, { commit }) => {\n for (const t of templateDiffs) {\n const dest = join(site.path, t.path);\n await mkdir(dirname(dest), { recursive: true });\n await writeFile(dest, t.contents, \"utf-8\");\n await commit(`chore: sync ${t.config} config from @reddoorla/maintenance`);\n }\n if (gitignorePlan.kind === \"apply\") {\n await applyGitignore(site.path, gitignorePlan);\n await commit(`chore: sync gitignore from @reddoorla/maintenance`);\n }\n return { kind: \"ok\" };\n },\n });\n}\n","/**\n * Comment line written above the appended block so future runs (and humans)\n * can recognize the managed section. Presence of this line is incidental —\n * the merge logic is keyed on each entry's normalized form, not on the marker.\n */\nexport const MANAGED_MARKER = \"# canonical entries from @reddoorla/maintenance sync-configs\";\n\n/**\n * Build artifacts, test outputs, deploy caches, and secrets that should never\n * be tracked across the reddoor fleet. Sites may keep additional site-specific\n * entries — they are preserved on merge.\n */\nexport const CANONICAL_GITIGNORE_ENTRIES: readonly string[] = [\n \"node_modules/\",\n \"build/\",\n \"dist/\",\n \".svelte-kit/\",\n \"coverage/\",\n \".vitest-cache/\",\n \"playwright-report/\",\n \"test-results/\",\n \".lighthouseci/\",\n \".tsbuildinfo\",\n \".env\",\n \".env.*\",\n \"!.env.example\",\n \".DS_Store\",\n \"*.log\",\n \".vercel/\",\n \".netlify/\",\n \".reddoor-a11y/\",\n // The a11y audit's transient spec dir, written inside the checkout and\n // normally cleaned, but a timeout-SIGKILL of the parent orphans it. Ignored\n // fleet-wide so it never dirties a self-updating repo's tree (2026-06-10 M-D).\n \".reddoor-a11y-spec-*/\",\n];\n\nexport type MergeResult = { content: string; added: string[] };\n\nfunction stripLeadingSlash(s: string): string {\n return s.startsWith(\"/\") ? s.slice(1) : s;\n}\n\nfunction stripTrailingSlash(s: string): string {\n return s.endsWith(\"/\") ? s.slice(0, -1) : s;\n}\n\n/**\n * Normalize for presence comparison only: strip leading `/`, trailing `/`,\n * and surrounding whitespace. `build`, `/build`, `build/`, and `/build/` all\n * collapse to the same key.\n */\nfunction normalizePresence(line: string): string {\n return stripTrailingSlash(stripLeadingSlash(line.trim()));\n}\n\nfunction presentSet(existing: string): Set<string> {\n const set = new Set<string>();\n for (const raw of existing.split(/\\r?\\n/)) {\n const trimmed = raw.trim();\n if (!trimmed) continue;\n if (trimmed.startsWith(\"#\")) continue;\n set.add(normalizePresence(trimmed));\n }\n return set;\n}\n\n/**\n * Merge `canonical` entries into `existing` .gitignore content.\n *\n * - Missing entries are appended under a managed marker comment.\n * - Existing entries (in any normalized variant — `/build`, `build/`, etc.)\n * are preserved as-is; we never rewrite the site's own lines.\n * - When every canonical entry is already present, returns the original\n * content unchanged with `added: []` — the recipe can treat that as noop.\n */\nexport function mergeGitignore(existing: string | null, canonical: readonly string[]): MergeResult {\n if (existing === null) {\n const body = [MANAGED_MARKER, ...canonical].join(\"\\n\") + \"\\n\";\n return { content: body, added: [...canonical] };\n }\n const present = presentSet(existing);\n const added: string[] = [];\n for (const entry of canonical) {\n const norm = normalizePresence(entry);\n if (!present.has(norm)) {\n added.push(entry);\n present.add(norm);\n }\n }\n if (added.length === 0) {\n return { content: existing, added: [] };\n }\n let base = existing;\n if (!base.endsWith(\"\\n\")) base += \"\\n\";\n const block = [\"\", MANAGED_MARKER, ...added].join(\"\\n\") + \"\\n\";\n return { content: base + block, added };\n}\n\n/**\n * Of the tracked paths, return those that fall under a canonical *directory*\n * entry — i.e., paths that the freshly-synced .gitignore now wants ignored\n * but which git currently has in the index.\n *\n * File-pattern entries (`.env`, `*.log`, `.DS_Store`) are intentionally\n * skipped: they may contain user-meaningful data, and `git rm --cached`\n * cannot scrub secrets from history anyway. Surfaced for manual review\n * instead of auto-removing.\n */\nexport function findTrackedArtifacts(\n tracked: readonly string[],\n canonical: readonly string[],\n): string[] {\n const dirEntries: string[] = [];\n for (const raw of canonical) {\n const t = raw.trim();\n if (!t) continue;\n if (t.startsWith(\"!\")) continue;\n if (/[*?[]/.test(t)) continue;\n const noLead = stripLeadingSlash(t);\n if (!noLead.endsWith(\"/\")) continue;\n const name = stripTrailingSlash(noLead);\n if (!name) continue;\n dirEntries.push(name);\n }\n const matched: string[] = [];\n for (const path of tracked) {\n for (const dir of dirEntries) {\n if (path === dir || path.startsWith(dir + \"/\")) {\n matched.push(path);\n break;\n }\n }\n }\n return matched;\n}\n"],"mappings":"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","names":[]}
|