@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.
Files changed (150) hide show
  1. package/README.md +3 -1
  2. package/dist/airtable-2BJX4ATJ.js +9 -0
  3. package/dist/airtable-2BJX4ATJ.js.map +1 -0
  4. package/dist/announce-JEVCDDKL.js +171 -0
  5. package/dist/announce-JEVCDDKL.js.map +1 -0
  6. package/dist/bump-deps-O3SOLQYY.js +57 -0
  7. package/dist/bump-deps-O3SOLQYY.js.map +1 -0
  8. package/dist/chunk-2X322ZES.js +99 -0
  9. package/dist/chunk-2X322ZES.js.map +1 -0
  10. package/dist/chunk-4NAOR44V.js +153 -0
  11. package/dist/chunk-4NAOR44V.js.map +1 -0
  12. package/dist/chunk-4PY4I5OR.js +1167 -0
  13. package/dist/chunk-4PY4I5OR.js.map +1 -0
  14. package/dist/chunk-5L2QXUTU.js +279 -0
  15. package/dist/chunk-5L2QXUTU.js.map +1 -0
  16. package/dist/chunk-5WL3ET24.js +203 -0
  17. package/dist/chunk-5WL3ET24.js.map +1 -0
  18. package/dist/chunk-6LIWDIXU.js +38 -0
  19. package/dist/chunk-6LIWDIXU.js.map +1 -0
  20. package/dist/chunk-6VAL7XJT.js +52 -0
  21. package/dist/chunk-6VAL7XJT.js.map +1 -0
  22. package/dist/chunk-BWLUNY7L.js +585 -0
  23. package/dist/chunk-BWLUNY7L.js.map +1 -0
  24. package/dist/chunk-CASCWQQX.js +161 -0
  25. package/dist/chunk-CASCWQQX.js.map +1 -0
  26. package/dist/chunk-CLVTVDOU.js +274 -0
  27. package/dist/chunk-CLVTVDOU.js.map +1 -0
  28. package/dist/chunk-D3DQFC32.js +79 -0
  29. package/dist/chunk-D3DQFC32.js.map +1 -0
  30. package/dist/chunk-EYODPUHV.js +37 -0
  31. package/dist/chunk-EYODPUHV.js.map +1 -0
  32. package/dist/chunk-FDIFTIXC.js +70 -0
  33. package/dist/chunk-FDIFTIXC.js.map +1 -0
  34. package/dist/chunk-FUD4I2ZW.js +35 -0
  35. package/dist/chunk-FUD4I2ZW.js.map +1 -0
  36. package/dist/chunk-HG5DTFNF.js +62 -0
  37. package/dist/chunk-HG5DTFNF.js.map +1 -0
  38. package/dist/chunk-HN6I2XWI.js +65 -0
  39. package/dist/chunk-HN6I2XWI.js.map +1 -0
  40. package/dist/chunk-HVOOCK6L.js +84 -0
  41. package/dist/chunk-HVOOCK6L.js.map +1 -0
  42. package/dist/chunk-IMIR7SC2.js +51 -0
  43. package/dist/chunk-IMIR7SC2.js.map +1 -0
  44. package/dist/chunk-J3OPWFZE.js +64 -0
  45. package/dist/chunk-J3OPWFZE.js.map +1 -0
  46. package/dist/chunk-J4ECCFRT.js +221 -0
  47. package/dist/chunk-J4ECCFRT.js.map +1 -0
  48. package/dist/chunk-JNXCDUYB.js +211 -0
  49. package/dist/chunk-JNXCDUYB.js.map +1 -0
  50. package/dist/chunk-JQG222CI.js +49 -0
  51. package/dist/chunk-JQG222CI.js.map +1 -0
  52. package/dist/chunk-LKGVSM2O.js +43 -0
  53. package/dist/chunk-LKGVSM2O.js.map +1 -0
  54. package/dist/chunk-LYCKN7ZR.js +41 -0
  55. package/dist/chunk-LYCKN7ZR.js.map +1 -0
  56. package/dist/chunk-MKSZBODM.js +162 -0
  57. package/dist/chunk-MKSZBODM.js.map +1 -0
  58. package/dist/chunk-O65I7G7G.js +246 -0
  59. package/dist/chunk-O65I7G7G.js.map +1 -0
  60. package/dist/chunk-OCS7SZUZ.js +243 -0
  61. package/dist/chunk-OCS7SZUZ.js.map +1 -0
  62. package/dist/chunk-OGGRFBL4.js +14 -0
  63. package/dist/chunk-OGGRFBL4.js.map +1 -0
  64. package/dist/chunk-OQPMZ6BM.js +368 -0
  65. package/dist/chunk-OQPMZ6BM.js.map +1 -0
  66. package/dist/chunk-OTGB6YQO.js +52 -0
  67. package/dist/chunk-OTGB6YQO.js.map +1 -0
  68. package/dist/chunk-PZQ3WED2.js +14 -0
  69. package/dist/chunk-PZQ3WED2.js.map +1 -0
  70. package/dist/chunk-QTHKOIFJ.js +13 -0
  71. package/dist/chunk-QTHKOIFJ.js.map +1 -0
  72. package/dist/chunk-U5SZ4FOP.js +26 -0
  73. package/dist/chunk-U5SZ4FOP.js.map +1 -0
  74. package/dist/chunk-W3G7XFBZ.js +444 -0
  75. package/dist/chunk-W3G7XFBZ.js.map +1 -0
  76. package/dist/chunk-WEKL2HYJ.js +31 -0
  77. package/dist/chunk-WEKL2HYJ.js.map +1 -0
  78. package/dist/chunk-WVRPOXKH.js +74 -0
  79. package/dist/chunk-WVRPOXKH.js.map +1 -0
  80. package/dist/chunk-XKOZUVK2.js +121 -0
  81. package/dist/chunk-XKOZUVK2.js.map +1 -0
  82. package/dist/chunk-XO2RUYVM.js +138 -0
  83. package/dist/chunk-XO2RUYVM.js.map +1 -0
  84. package/dist/chunk-XTK5VIQB.js +97 -0
  85. package/dist/chunk-XTK5VIQB.js.map +1 -0
  86. package/dist/chunk-XXTZBPUY.js +9 -0
  87. package/dist/chunk-XXTZBPUY.js.map +1 -0
  88. package/dist/chunk-ZYBOT5PM.js +153 -0
  89. package/dist/chunk-ZYBOT5PM.js.map +1 -0
  90. package/dist/cli/bin.js +75 -7785
  91. package/dist/cli/bin.js.map +1 -1
  92. package/dist/cli/commands/audit.js +37 -2336
  93. package/dist/cli/commands/audit.js.map +1 -1
  94. package/dist/client-EPVO3FEH.js +10 -0
  95. package/dist/client-EPVO3FEH.js.map +1 -0
  96. package/dist/client-JA6QLEDW.js +48 -0
  97. package/dist/client-JA6QLEDW.js.map +1 -0
  98. package/dist/configs/lighthouse.js +4 -31
  99. package/dist/configs/lighthouse.js.map +1 -1
  100. package/dist/configs/playwright-a11y.js +6 -33
  101. package/dist/configs/playwright-a11y.js.map +1 -1
  102. package/dist/convert-to-pnpm-UR64X3OX.js +51 -0
  103. package/dist/convert-to-pnpm-UR64X3OX.js.map +1 -0
  104. package/dist/db-AXKMRTVA.js +23 -0
  105. package/dist/db-AXKMRTVA.js.map +1 -0
  106. package/dist/digest-YTO73BEO.js +408 -0
  107. package/dist/digest-YTO73BEO.js.map +1 -0
  108. package/dist/forms/index.js +3 -8
  109. package/dist/forms/index.js.map +1 -1
  110. package/dist/github-signals-XLQXXH6Q.js +116 -0
  111. package/dist/github-signals-XLQXXH6Q.js.map +1 -0
  112. package/dist/index.js +102 -4757
  113. package/dist/index.js.map +1 -1
  114. package/dist/init-JITQXFVL.js +85 -0
  115. package/dist/init-JITQXFVL.js.map +1 -0
  116. package/dist/launch-XW6PEKUL.js +207 -0
  117. package/dist/launch-XW6PEKUL.js.map +1 -0
  118. package/dist/lighthouse-airtable-4FKYKFH6.js +12 -0
  119. package/dist/lighthouse-airtable-4FKYKFH6.js.map +1 -0
  120. package/dist/migrate-P2LS4WXI.js +7 -0
  121. package/dist/migrate-P2LS4WXI.js.map +1 -0
  122. package/dist/onboard-TCQOP2W4.js +69 -0
  123. package/dist/onboard-TCQOP2W4.js.map +1 -0
  124. package/dist/orchestrate-LHH3LQNP.js +23 -0
  125. package/dist/orchestrate-LHH3LQNP.js.map +1 -0
  126. package/dist/recipes/sync-configs.js +9 -505
  127. package/dist/recipes/sync-configs.js.map +1 -1
  128. package/dist/renovate-dispatch-YVBTBJJY.js +100 -0
  129. package/dist/renovate-dispatch-YVBTBJJY.js.map +1 -0
  130. package/dist/report-WK3SKWJI.js +240 -0
  131. package/dist/report-WK3SKWJI.js.map +1 -0
  132. package/dist/resend-G2BJMAQV.js +7 -0
  133. package/dist/resend-G2BJMAQV.js.map +1 -0
  134. package/dist/self-updating-BYUBUNO4.js +61 -0
  135. package/dist/self-updating-BYUBUNO4.js.map +1 -0
  136. package/dist/svelte-codemods-BYRJAU7I.js +51 -0
  137. package/dist/svelte-codemods-BYRJAU7I.js.map +1 -0
  138. package/dist/sync-configs-JPRLAW5J.js +109 -0
  139. package/dist/sync-configs-JPRLAW5J.js.map +1 -0
  140. package/dist/upgrade-ERA45EVR.js +64 -0
  141. package/dist/upgrade-ERA45EVR.js.map +1 -0
  142. package/dist/util/git.js +19 -76
  143. package/dist/util/git.js.map +1 -1
  144. package/dist/util/pkg.js +5 -45
  145. package/dist/util/pkg.js.map +1 -1
  146. package/dist/websites-X5DLC2PC.js +43 -0
  147. package/dist/websites-X5DLC2PC.js.map +1 -0
  148. package/dist/write-audits-to-airtable-CE4POBAF.js +13 -0
  149. package/dist/write-audits-to-airtable-CE4POBAF.js.map +1 -0
  150. package/package.json +13 -12
@@ -0,0 +1,203 @@
1
+ import {
2
+ hasRealScores,
3
+ lighthouseScoresFromResult
4
+ } from "./chunk-IMIR7SC2.js";
5
+ import {
6
+ normalizeSecurityAdvisory,
7
+ siteSlug,
8
+ updateAuditFields
9
+ } from "./chunk-CLVTVDOU.js";
10
+
11
+ // src/audits/a11y-airtable.ts
12
+ function hasA11yCounts(result) {
13
+ if (result.audit !== "a11y") return false;
14
+ const details = result.details;
15
+ return typeof details?.totalViolations === "number";
16
+ }
17
+ function a11yCountsFromResult(result) {
18
+ if (result.audit !== "a11y") {
19
+ throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);
20
+ }
21
+ const details = result.details;
22
+ return { violations: details?.totalViolations ?? 0 };
23
+ }
24
+
25
+ // src/audits/deps-airtable.ts
26
+ function hasDepsCounts(result) {
27
+ if (result.audit !== "deps") return false;
28
+ const d = result.details;
29
+ return Array.isArray(d?.entries);
30
+ }
31
+ function depsCountsFromResult(result) {
32
+ if (result.audit !== "deps") {
33
+ throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);
34
+ }
35
+ const details = result.details ?? {};
36
+ const entries = details.entries ?? [];
37
+ const drifted = entries.filter((e) => e.drift !== "same").length;
38
+ const majorBehind = entries.filter((e) => e.drift === "major").length;
39
+ const outdated = details.outdated?.outdated ?? null;
40
+ return { drifted, majorBehind, outdated };
41
+ }
42
+
43
+ // src/audits/security-airtable.ts
44
+ function hasSecurityCounts(result) {
45
+ if (result.audit !== "security") return false;
46
+ const details = result.details;
47
+ return !!details && typeof details.counts === "object";
48
+ }
49
+ function securityCountsFromResult(result) {
50
+ if (result.audit !== "security") {
51
+ throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
52
+ }
53
+ const details = result.details;
54
+ const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };
55
+ return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };
56
+ }
57
+ function advisoriesFromResult(result) {
58
+ if (result.audit !== "security") {
59
+ throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
60
+ }
61
+ const details = result.details;
62
+ const raw = details?.advisories;
63
+ if (!Array.isArray(raw)) return [];
64
+ return raw.map(normalizeSecurityAdvisory).filter((a) => a !== null);
65
+ }
66
+
67
+ // src/audits/domain-airtable.ts
68
+ function hasDomainResult(result) {
69
+ if (result.audit !== "domain") return false;
70
+ const d = result.details;
71
+ return !!d && typeof d.checkedAt === "string";
72
+ }
73
+ function domainResultFromAudit(result) {
74
+ if (result.audit !== "domain") {
75
+ throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);
76
+ }
77
+ const d = result.details;
78
+ return {
79
+ certDaysRemaining: typeof d?.certDaysRemaining === "number" ? d.certDaysRemaining : null,
80
+ checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
81
+ };
82
+ }
83
+
84
+ // src/audits/browser-airtable.ts
85
+ function hasBrowserResult(result) {
86
+ if (result.audit !== "browser") return false;
87
+ const d = result.details;
88
+ return !!d && typeof d.checkedAt === "string";
89
+ }
90
+ function browserFieldsFromAudit(result) {
91
+ if (result.audit !== "browser") {
92
+ throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);
93
+ }
94
+ const d = result.details;
95
+ return {
96
+ desktopOk: d?.desktopOk === true,
97
+ mobileOk: d?.mobileOk === true,
98
+ linksOk: d?.linksOk === true,
99
+ brokenLinks: typeof d?.brokenLinks === "number" ? d.brokenLinks : 0,
100
+ checkedAt: typeof d?.checkedAt === "string" ? d.checkedAt : (/* @__PURE__ */ new Date()).toISOString()
101
+ };
102
+ }
103
+
104
+ // src/audits/write-audits-to-airtable.ts
105
+ async function writeAuditsToAirtable(args) {
106
+ const { base, websites, slug, results } = args;
107
+ const lhResult = results.find((r) => r.audit === "lighthouse");
108
+ const target = websites.find((w) => siteSlug(w.name) === slug);
109
+ if (!target) {
110
+ throw Object.assign(new Error(`No Websites row matched slug "${slug}"`), { exitCode: 2 });
111
+ }
112
+ const writes = [];
113
+ const audits = {};
114
+ const lhHasScores = lhResult ? hasRealScores(lhResult) : false;
115
+ if (lhResult && lhHasScores) {
116
+ const scores = lighthouseScoresFromResult(lhResult);
117
+ audits.scores = scores;
118
+ writes.push({ audit: "lighthouse", counts: scores });
119
+ }
120
+ const a11y = results.find((r) => r.audit === "a11y");
121
+ if (a11y && hasA11yCounts(a11y)) {
122
+ const counts = a11yCountsFromResult(a11y);
123
+ audits.a11y = counts;
124
+ writes.push({ audit: "a11y", counts });
125
+ }
126
+ const deps = results.find((r) => r.audit === "deps");
127
+ if (deps && hasDepsCounts(deps)) {
128
+ const counts = depsCountsFromResult(deps);
129
+ audits.deps = counts;
130
+ writes.push({ audit: "deps", counts });
131
+ }
132
+ const sec = results.find((r) => r.audit === "security");
133
+ if (sec && hasSecurityCounts(sec)) {
134
+ const counts = securityCountsFromResult(sec);
135
+ audits.security = counts;
136
+ audits.securityAdvisories = advisoriesFromResult(sec);
137
+ writes.push({ audit: "security", counts });
138
+ }
139
+ const dom = results.find((r) => r.audit === "domain");
140
+ if (dom && hasDomainResult(dom)) {
141
+ const result = domainResultFromAudit(dom);
142
+ audits.domain = result;
143
+ writes.push({ audit: "domain", counts: result });
144
+ }
145
+ const browser = results.find((r) => r.audit === "browser");
146
+ if (browser && hasBrowserResult(browser)) {
147
+ const fields = browserFieldsFromAudit(browser);
148
+ audits.browser = fields;
149
+ writes.push({ audit: "browser", counts: fields });
150
+ }
151
+ if (Object.keys(audits).length > 0) {
152
+ await updateAuditFields(base, target.id, audits);
153
+ }
154
+ if (lhResult && !lhHasScores) {
155
+ const persisted = writes.map((w) => w.audit);
156
+ throw Object.assign(
157
+ new Error(
158
+ `Lighthouse audit produced no scores; ${persisted.length ? `wrote ${persisted.join("/")} but refused Lighthouse` : "wrote nothing"}. Summary: ${lhResult.summary}`
159
+ ),
160
+ { exitCode: 1 }
161
+ );
162
+ }
163
+ return { siteName: target.name, writes };
164
+ }
165
+ function formatFleetWriteSummary(result) {
166
+ const wrote = result.written.length;
167
+ const failed = result.failed.length;
168
+ const total = wrote + failed;
169
+ let out = `\u2192 wrote ${wrote} site(s) to Airtable`;
170
+ if (failed > 0) {
171
+ out += `
172
+ \u26A0 ${failed} site(s) not written: ${result.failed.map((f) => `${f.slug} (${f.error})`).join("; ")}`;
173
+ }
174
+ out += `
175
+ FLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;
176
+ return out;
177
+ }
178
+ async function writeFleetAuditsToAirtable(args) {
179
+ const { base, websites, results } = args;
180
+ const bySlug = /* @__PURE__ */ new Map();
181
+ for (const r of results) {
182
+ const arr = bySlug.get(r.site) ?? [];
183
+ arr.push(r);
184
+ bySlug.set(r.site, arr);
185
+ }
186
+ const written = [];
187
+ const failed = [];
188
+ for (const [slug, siteResults] of bySlug) {
189
+ try {
190
+ written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));
191
+ } catch (e) {
192
+ failed.push({ slug, error: e.message });
193
+ }
194
+ }
195
+ return { written, failed };
196
+ }
197
+
198
+ export {
199
+ writeAuditsToAirtable,
200
+ formatFleetWriteSummary,
201
+ writeFleetAuditsToAirtable
202
+ };
203
+ //# sourceMappingURL=chunk-5WL3ET24.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/audits/a11y-airtable.ts","../src/audits/deps-airtable.ts","../src/audits/security-airtable.ts","../src/audits/domain-airtable.ts","../src/audits/browser-airtable.ts","../src/audits/write-audits-to-airtable.ts"],"sourcesContent":["import type { AuditResult } from \"../types.js\";\n\ntype A11yDetails = {\n totalViolations: number;\n byImpact: Partial<Record<\"minor\" | \"moderate\" | \"serious\" | \"critical\", number>>;\n};\n\n/** True when an a11y AuditResult carries real counts worth persisting.\n * Mirrors the `hasRealScores` policy on lighthouse: write whenever real\n * data exists, regardless of status (a \"warn\" or \"fail\" with concrete\n * violation counts is exactly what the dashboard needs to track). */\nexport function hasA11yCounts(result: AuditResult): boolean {\n if (result.audit !== \"a11y\") return false;\n const details = result.details as A11yDetails | undefined;\n return typeof details?.totalViolations === \"number\";\n}\n\nexport function a11yCountsFromResult(result: AuditResult): { violations: number } {\n if (result.audit !== \"a11y\") {\n throw new Error(`Expected an 'a11y' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as A11yDetails | undefined;\n return { violations: details?.totalViolations ?? 0 };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DepsDetails, DepsDriftEntry } from \"./deps.js\";\n\n/** True when a deps AuditResult carries the structured details (an entries\n * array, per {@link DepsDetails}). */\nexport function hasDepsCounts(result: AuditResult): boolean {\n if (result.audit !== \"deps\") return false;\n const d = result.details as Partial<DepsDetails> | undefined;\n return Array.isArray(d?.entries);\n}\n\nexport function depsCountsFromResult(result: AuditResult): {\n drifted: number;\n majorBehind: number;\n outdated: number | null;\n} {\n if (result.audit !== \"deps\") {\n throw new Error(`Expected a 'deps' AuditResult, got '${result.audit}'`);\n }\n const details = (result.details ?? {}) as Partial<DepsDetails>;\n const entries = (details.entries ?? []) as DepsDriftEntry[];\n // \"drifted\" = drift !== \"same\", which intentionally includes \"newer\" (ahead of\n // baseline) — parity with the deps audit summary text.\n const drifted = entries.filter((e) => e.drift !== \"same\").length;\n const majorBehind = entries.filter((e) => e.drift === \"major\").length;\n // The real installed-version drift, distinct from declared-range drift. Null\n // when the audit couldn't determine it (no/stale lockfile) — kept null (not 0)\n // so the dashboard shows \"—\" rather than a misleading \"clean\".\n const outdated = details.outdated?.outdated ?? null;\n return { drifted, majorBehind, outdated };\n}\n","import type { AuditResult } from \"../types.js\";\nimport { normalizeSecurityAdvisory, type SecurityAdvisory } from \"../reports/airtable/websites.js\";\n\ntype SecurityDetails = {\n counts: { low: number; moderate: number; high: number; critical: number };\n advisories?: unknown;\n};\n\n/** True when a security AuditResult carries a counts object. Skipped runs\n * (no pnpm + no npm) have no details and return false. */\nexport function hasSecurityCounts(result: AuditResult): boolean {\n if (result.audit !== \"security\") return false;\n const details = result.details as SecurityDetails | undefined;\n return !!details && typeof details.counts === \"object\";\n}\n\nexport function securityCountsFromResult(result: AuditResult): {\n critical: number;\n high: number;\n moderate: number;\n low: number;\n} {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };\n return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };\n}\n\n/** The known advisories behind the counts, normalized for persistence. Empty when the\n * run was clean or carried no advisory detail (npm/pnpm sometimes omit it). Malformed\n * entries are dropped via the shared `normalizeSecurityAdvisory`. */\nexport function advisoriesFromResult(result: AuditResult): SecurityAdvisory[] {\n if (result.audit !== \"security\") {\n throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);\n }\n const details = result.details as SecurityDetails | undefined;\n const raw = details?.advisories;\n if (!Array.isArray(raw)) return [];\n return raw.map(normalizeSecurityAdvisory).filter((a): a is SecurityAdvisory => a !== null);\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { DomainResult } from \"../reports/airtable/websites.js\";\n\ntype DomainDetails = { resolved: boolean; certDaysRemaining: number | null; checkedAt: string };\n\n/** True when an AuditResult is a `domain` audit carrying a usable details payload (i.e. it\n * actually ran — a \"skip\" for a site with no deployed URL has no details). */\nexport function hasDomainResult(result: AuditResult): boolean {\n if (result.audit !== \"domain\") return false;\n const d = result.details as DomainDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable domain result (cert days + checked-at) from a `domain` audit. */\nexport function domainResultFromAudit(result: AuditResult): DomainResult {\n if (result.audit !== \"domain\") {\n throw new Error(`Expected a 'domain' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as DomainDetails | undefined;\n return {\n certDaysRemaining: typeof d?.certDaysRemaining === \"number\" ? d.certDaysRemaining : null,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { BrowserAuditFields } from \"../reports/airtable/websites.js\";\n\ntype BrowserDetails = {\n desktopOk: boolean;\n mobileOk: boolean;\n linksOk: boolean;\n brokenLinks: number;\n checkedAt: string;\n};\n\n/** True when an AuditResult is a `browser` audit that actually ran (has a details payload — a\n * \"skip\" for a site with no deployed URL has none). */\nexport function hasBrowserResult(result: AuditResult): boolean {\n if (result.audit !== \"browser\") return false;\n const d = result.details as BrowserDetails | undefined;\n return !!d && typeof d.checkedAt === \"string\";\n}\n\n/** Extract the Airtable-writable browser verdicts from a `browser` audit. */\nexport function browserFieldsFromAudit(result: AuditResult): BrowserAuditFields {\n if (result.audit !== \"browser\") {\n throw new Error(`Expected a 'browser' AuditResult, got '${result.audit}'`);\n }\n const d = result.details as BrowserDetails | undefined;\n return {\n desktopOk: d?.desktopOk === true,\n mobileOk: d?.mobileOk === true,\n linksOk: d?.linksOk === true,\n brokenLinks: typeof d?.brokenLinks === \"number\" ? d.brokenLinks : 0,\n checkedAt: typeof d?.checkedAt === \"string\" ? d.checkedAt : new Date().toISOString(),\n };\n}\n","import type { AuditResult } from \"../types.js\";\nimport type { AirtableBase } from \"../reports/airtable/client.js\";\nimport { type WebsiteRow, siteSlug, updateAuditFields } from \"../reports/airtable/websites.js\";\nimport type {\n A11yCounts,\n DepsCounts,\n SecurityCounts,\n SecurityAdvisory,\n DomainResult,\n BrowserAuditFields,\n} from \"../reports/airtable/websites.js\";\nimport type { LighthouseScores } from \"../reports/types.js\";\nimport { hasRealScores, lighthouseScoresFromResult } from \"./lighthouse-airtable.js\";\nimport { hasA11yCounts, a11yCountsFromResult } from \"./a11y-airtable.js\";\nimport { hasDepsCounts, depsCountsFromResult } from \"./deps-airtable.js\";\nimport {\n hasSecurityCounts,\n securityCountsFromResult,\n advisoriesFromResult,\n} from \"./security-airtable.js\";\nimport { hasDomainResult, domainResultFromAudit } from \"./domain-airtable.js\";\nimport { hasBrowserResult, browserFieldsFromAudit } from \"./browser-airtable.js\";\n\ntype WriteSummary = {\n siteName: string;\n writes: Array<{\n audit: \"lighthouse\" | \"a11y\" | \"deps\" | \"security\" | \"github-signals\" | \"domain\" | \"browser\";\n counts: object;\n }>;\n};\n\n/** Orchestrates the per-audit Airtable writes for `audit --write-airtable`.\n * Extracted from the CLI command so it can be unit-tested with a fake base\n * and so adding new audit types is a one-line addition here rather than\n * growing the CLI handler.\n *\n * Throws (with .exitCode set) on the failure modes the CLI surfaces today:\n * - 2: --only ran without lighthouse, or no Websites row matched the slug\n * - 1: lighthouse ran but produced no real scores (infrastructure failure).\n * The a11y/deps/security writes still complete FIRST — a Lighthouse\n * miss flags the site without discarding its other audit data.\n *\n * Precedence note: the Websites-row lookup (exitCode 2) is checked BEFORE the\n * no-scores gate (exitCode 1), so the rare no-row + no-scores combo surfaces\n * as exitCode 2. */\nexport async function writeAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n slug: string;\n results: AuditResult[];\n}): Promise<WriteSummary> {\n const { base, websites, slug, results } = args;\n\n // Lighthouse is OPTIONAL. The checkout-free `--only lighthouse,domain,browser` nightly includes\n // it, but a standalone `--only security` (checkout-ful) sweep legitimately has none. We write\n // whatever audits produced values; only the lighthouse-MISS flag below is lighthouse-specific\n // (so a real Lighthouse infra failure still reds the run without discarding other audit data).\n const lhResult = results.find((r) => r.audit === \"lighthouse\");\n const target = websites.find((w) => siteSlug(w.name) === slug);\n if (!target) {\n throw Object.assign(new Error(`No Websites row matched slug \"${slug}\"`), { exitCode: 2 });\n }\n\n const writes: WriteSummary[\"writes\"] = [];\n const audits: {\n scores?: LighthouseScores;\n a11y?: A11yCounts;\n deps?: DepsCounts;\n security?: SecurityCounts;\n securityAdvisories?: SecurityAdvisory[];\n domain?: DomainResult;\n browser?: BrowserAuditFields;\n } = {};\n\n // Collect every audit that produced real values into ONE merged input, then do a\n // SINGLE atomic Airtable update (was: up to four sequential updates on the same\n // row — a mid-sequence failure left it half-written yet reported fully failed, at\n // 4× the request volume). Lighthouse is the most timeout-prone audit: a Lighthouse\n // miss must NOT discard the site's valid a11y/deps/security results (morning-brief\n // 2026-06-10 MEDIUM-E). So include Lighthouse scores only when real, include the\n // other audits whenever present, write all of them in one update, then — if\n // Lighthouse missed — throw AFTER that atomic write so the site is still flagged\n // (exitCode 1 / collected in FleetWriteResult.failed) without losing its other data.\n const lhHasScores = lhResult ? hasRealScores(lhResult) : false;\n if (lhResult && lhHasScores) {\n const scores = lighthouseScoresFromResult(lhResult);\n audits.scores = scores;\n writes.push({ audit: \"lighthouse\", counts: scores });\n }\n\n const a11y = results.find((r) => r.audit === \"a11y\");\n if (a11y && hasA11yCounts(a11y)) {\n const counts = a11yCountsFromResult(a11y);\n audits.a11y = counts;\n writes.push({ audit: \"a11y\", counts });\n }\n\n const deps = results.find((r) => r.audit === \"deps\");\n if (deps && hasDepsCounts(deps)) {\n const counts = depsCountsFromResult(deps);\n audits.deps = counts;\n writes.push({ audit: \"deps\", counts });\n }\n\n const sec = results.find((r) => r.audit === \"security\");\n if (sec && hasSecurityCounts(sec)) {\n const counts = securityCountsFromResult(sec);\n audits.security = counts;\n // Persist the advisory list alongside the counts so the dashboard can show which packages\n // are vulnerable. An empty array (clean run) clears any stale list.\n audits.securityAdvisories = advisoriesFromResult(sec);\n writes.push({ audit: \"security\", counts });\n }\n\n const dom = results.find((r) => r.audit === \"domain\");\n if (dom && hasDomainResult(dom)) {\n const result = domainResultFromAudit(dom);\n audits.domain = result;\n writes.push({ audit: \"domain\", counts: result });\n }\n\n const browser = results.find((r) => r.audit === \"browser\");\n if (browser && hasBrowserResult(browser)) {\n const fields = browserFieldsFromAudit(browser);\n audits.browser = fields;\n writes.push({ audit: \"browser\", counts: fields });\n }\n\n // One atomic write of everything that ran. Skip the call only if there is nothing\n // to write at all (no real scores AND no other audit produced values) — an empty\n // update is a wasted request.\n if (Object.keys(audits).length > 0) {\n await updateAuditFields(base, target.id, audits);\n }\n\n // Lighthouse-miss flag: only when lighthouse WAS requested (in results) but produced no scores —\n // an infra failure worth reding the run, AFTER persisting the other audits. A sweep that never\n // ran lighthouse (e.g. `--only security`) skips this entirely.\n if (lhResult && !lhHasScores) {\n // Enumerate what WAS persisted so the failure (surfaced to the single-site\n // CLI operator via console.error) reads as a partial write, not a total one.\n const persisted = writes.map((w) => w.audit);\n throw Object.assign(\n new Error(\n `Lighthouse audit produced no scores; ${\n persisted.length ? `wrote ${persisted.join(\"/\")} but refused Lighthouse` : \"wrote nothing\"\n }. Summary: ${lhResult.summary}`,\n ),\n { exitCode: 1 },\n );\n }\n\n return { siteName: target.name, writes };\n}\n\nexport type FleetWriteResult = {\n written: WriteSummary[];\n failed: Array<{ slug: string; error: string }>;\n};\n\n/** Render the fleet write-back outcome for the CLI/CI. Beyond the human-readable\n * lines, it emits a single deterministic, machine-parseable line —\n * `FLEET_WRITE_SUMMARY wrote=N failed=M total=T` — that the nightly workflow\n * greps to decide pass/fail. Keying CI on this line (not the prose, and not a\n * \"wrote ≥ 1\" heuristic) lets the gate tolerate a single known flake while\n * still reding on a total or mass write-back failure. */\nexport function formatFleetWriteSummary(result: FleetWriteResult): string {\n const wrote = result.written.length;\n const failed = result.failed.length;\n const total = wrote + failed;\n let out = `→ wrote ${wrote} site(s) to Airtable`;\n if (failed > 0) {\n out += `\\n⚠ ${failed} site(s) not written: ${result.failed\n .map((f) => `${f.slug} (${f.error})`)\n .join(\"; \")}`;\n }\n out += `\\nFLEET_WRITE_SUMMARY wrote=${wrote} failed=${failed} total=${total}`;\n return out;\n}\n\n/** Write each site's pooled audit results back to its own Websites row,\n * best-effort. Results are grouped by `result.site` (the slug the fleet\n * inventory stamped as Site.name). A per-site failure (no scores, no matching\n * row) is collected — not thrown — so one bad site never aborts the batch. */\nexport async function writeFleetAuditsToAirtable(args: {\n base: AirtableBase;\n websites: WebsiteRow[];\n results: AuditResult[];\n}): Promise<FleetWriteResult> {\n const { base, websites, results } = args;\n\n const bySlug = new Map<string, AuditResult[]>();\n for (const r of results) {\n const arr = bySlug.get(r.site) ?? [];\n arr.push(r);\n bySlug.set(r.site, arr);\n }\n\n const written: WriteSummary[] = [];\n const failed: FleetWriteResult[\"failed\"] = [];\n // Serial on purpose: even at one (now atomic) update call per site, Airtable's\n // ~5 req/sec limit means a Promise.all fan-out across the fleet would burst and\n // trip 429s (silently filed as failures). Below a few dozen sites, serial trades\n // wall-clock for safety. (morning-brief 2026-06-09 MEDIUM-3.) Add a bounded pool\n // when the fleet grows.\n for (const [slug, siteResults] of bySlug) {\n try {\n written.push(await writeAuditsToAirtable({ base, websites, slug, results: siteResults }));\n } catch (e) {\n failed.push({ slug, error: (e as Error).message });\n }\n }\n return { written, failed };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,38 @@
1
+ import {
2
+ isPendingApproval,
3
+ listReportsForSite,
4
+ setDraftReady
5
+ } from "./chunk-OCS7SZUZ.js";
6
+
7
+ // src/reports/queue.ts
8
+ var REPORT_TIER = {
9
+ Maintenance: 1,
10
+ Testing: 2,
11
+ Announcement: 3,
12
+ Launch: 3
13
+ };
14
+ function reportTier(type) {
15
+ return REPORT_TIER[type];
16
+ }
17
+ async function queueDraft(base, report) {
18
+ const newTier = reportTier(report.reportType);
19
+ const others = (await listReportsForSite(base, report.siteId)).filter(isPendingApproval).filter((r) => r.id !== report.id);
20
+ const blocker = others.find((r) => reportTier(r.reportType) >= newTier);
21
+ if (blocker) {
22
+ await setDraftReady(base, report.id, false);
23
+ return { queued: false, blockedBy: blocker.reportType, supersededIds: [] };
24
+ }
25
+ const supersededIds = [];
26
+ for (const r of others) {
27
+ await setDraftReady(base, r.id, false);
28
+ supersededIds.push(r.id);
29
+ }
30
+ await setDraftReady(base, report.id, true);
31
+ return { queued: true, supersededIds };
32
+ }
33
+
34
+ export {
35
+ reportTier,
36
+ queueDraft
37
+ };
38
+ //# sourceMappingURL=chunk-6LIWDIXU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reports/queue.ts"],"sourcesContent":["import type { ReportType } from \"./types.js\";\nimport type { AirtableBase } from \"./airtable/client.js\";\nimport { listReportsForSite, isPendingApproval, setDraftReady } from \"./airtable/reports.js\";\n\n/**\n * Report tiers — a higher tier is a SUPERSET of the lower ones, so only the highest-tier\n * un-sent draft is worth keeping in a site's approve queue. Maintenance ⊂ Testing ⊂\n * {Announcement, Launch}. Announcement and Launch share tier 3 (neither supersets the other).\n */\nexport const REPORT_TIER: Record<ReportType, number> = {\n Maintenance: 1,\n Testing: 2,\n Announcement: 3,\n Launch: 3,\n};\n\nexport function reportTier(type: ReportType): number {\n return REPORT_TIER[type];\n}\n\nexport type QueueOutcome = {\n /** Whether `report` was placed in (kept in) the approve queue. */\n queued: boolean;\n /** When `queued` is false, the queued report type that subsumes this one. */\n blockedBy?: ReportType;\n /** Ids of lower-tier queued reports that this one superseded (un-queued). */\n supersededIds: string[];\n};\n\n/**\n * Enforce \"one report queued per site, highest tier wins\" at draft time. Given a freshly\n * created/reused report, look at the OTHER reports already pending approval for the same site\n * (Draft ready ∧ ¬Approved ∧ ¬Sent) and:\n *\n * - If any is of an EQUAL-OR-HIGHER tier → the new report is subsumed: leave it un-queued\n * (Draft ready = false) and report `blockedBy`. (Equal-tier, e.g. a queued Launch when an\n * Announcement is drafted, keeps the existing one rather than silently replacing it.)\n * - Otherwise the new report is strictly the highest → un-queue every (strictly lower) pending\n * report for the site (superseded, not deleted — the row is kept) and queue the new one.\n *\n * Returns what happened so the caller can surface it. PURE side effects are all `setDraftReady`.\n */\nexport async function queueDraft(\n base: AirtableBase,\n report: { id: string; siteId: string; reportType: ReportType },\n): Promise<QueueOutcome> {\n const newTier = reportTier(report.reportType);\n const others = (await listReportsForSite(base, report.siteId))\n .filter(isPendingApproval)\n .filter((r) => r.id !== report.id);\n\n const blocker = others.find((r) => reportTier(r.reportType) >= newTier);\n if (blocker) {\n // A queued report already covers this one. Make sure the new draft is NOT queued (the reuse\n // path may hand us a row that was Draft-ready from a prior run) and stand down.\n await setDraftReady(base, report.id, false);\n return { queued: false, blockedBy: blocker.reportType, supersededIds: [] };\n }\n\n // The new report is strictly the highest-tier pending — supersede the rest (un-queue), keep it.\n const supersededIds: string[] = [];\n for (const r of others) {\n await setDraftReady(base, r.id, false);\n supersededIds.push(r.id);\n }\n await setDraftReady(base, report.id, true);\n return { queued: true, supersededIds };\n}\n"],"mappings":";;;;;;;AASO,IAAM,cAA0C;AAAA,EACrD,aAAa;AAAA,EACb,SAAS;AAAA,EACT,cAAc;AAAA,EACd,QAAQ;AACV;AAEO,SAAS,WAAW,MAA0B;AACnD,SAAO,YAAY,IAAI;AACzB;AAwBA,eAAsB,WACpB,MACA,QACuB;AACvB,QAAM,UAAU,WAAW,OAAO,UAAU;AAC5C,QAAM,UAAU,MAAM,mBAAmB,MAAM,OAAO,MAAM,GACzD,OAAO,iBAAiB,EACxB,OAAO,CAAC,MAAM,EAAE,OAAO,OAAO,EAAE;AAEnC,QAAM,UAAU,OAAO,KAAK,CAAC,MAAM,WAAW,EAAE,UAAU,KAAK,OAAO;AACtE,MAAI,SAAS;AAGX,UAAM,cAAc,MAAM,OAAO,IAAI,KAAK;AAC1C,WAAO,EAAE,QAAQ,OAAO,WAAW,QAAQ,YAAY,eAAe,CAAC,EAAE;AAAA,EAC3E;AAGA,QAAM,gBAA0B,CAAC;AACjC,aAAW,KAAK,QAAQ;AACtB,UAAM,cAAc,MAAM,EAAE,IAAI,KAAK;AACrC,kBAAc,KAAK,EAAE,EAAE;AAAA,EACzB;AACA,QAAM,cAAc,MAAM,OAAO,IAAI,IAAI;AACzC,SAAO,EAAE,QAAQ,MAAM,cAAc;AACvC;","names":[]}
@@ -0,0 +1,52 @@
1
+ // src/util/credentials.ts
2
+ import { readFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { join } from "path";
5
+ function defaultCredentialsPath() {
6
+ const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
7
+ return join(base, "reddoor-maint", "credentials.env");
8
+ }
9
+ function parseEnvFile(contents) {
10
+ const out = {};
11
+ const lines = contents.split(/\r?\n/);
12
+ for (let i = 0; i < lines.length; i++) {
13
+ const trimmed = lines[i].trim();
14
+ if (!trimmed || trimmed.startsWith("#")) continue;
15
+ const line = trimmed.replace(/^export\s+/, "");
16
+ const eq = line.indexOf("=");
17
+ const key = eq > 0 ? line.slice(0, eq).trim() : "";
18
+ if (eq <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
19
+ console.warn(`credentials: skipping unparseable line ${i + 1}: ${trimmed}`);
20
+ continue;
21
+ }
22
+ let value = line.slice(eq + 1).trim();
23
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
24
+ value = value.slice(1, -1);
25
+ }
26
+ out[key] = value;
27
+ }
28
+ return out;
29
+ }
30
+ function loadCredentialsIntoEnv(path = defaultCredentialsPath()) {
31
+ let contents;
32
+ try {
33
+ contents = readFileSync(path, "utf-8");
34
+ } catch {
35
+ return [];
36
+ }
37
+ const parsed = parseEnvFile(contents);
38
+ const applied = [];
39
+ for (const [k, v] of Object.entries(parsed)) {
40
+ if (process.env[k] === void 0) {
41
+ process.env[k] = v;
42
+ applied.push(k);
43
+ }
44
+ }
45
+ return applied;
46
+ }
47
+
48
+ export {
49
+ defaultCredentialsPath,
50
+ loadCredentialsIntoEnv
51
+ };
52
+ //# sourceMappingURL=chunk-6VAL7XJT.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/util/credentials.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\n/** Resolve the canonical credentials file path. Respects $XDG_CONFIG_HOME\n * (Linux/macOS convention) and falls back to ~/.config/reddoor-maint/. */\nexport function defaultCredentialsPath(): string {\n const base = process.env.XDG_CONFIG_HOME ?? join(homedir(), \".config\");\n return join(base, \"reddoor-maint\", \"credentials.env\");\n}\n\n/** Parse a tiny subset of dotenv: `KEY=value` per line, `# comments`,\n * blank lines. A leading `export ` token is stripped (dotenv does this),\n * so a hand-edited `export AIRTABLE_PAT=…` parses instead of being dropped.\n * Quoted values strip the surrounding quotes. A non-blank, non-comment line\n * that still doesn't parse (no `=`, bad key) is skipped with a one-line\n * stderr warning naming the line number — this is a credentials file, so a\n * silent drop turns into a confusing \"missing credential\" downstream. */\nexport function parseEnvFile(contents: string): Record<string, string> {\n const out: Record<string, string> = {};\n const lines = contents.split(/\\r?\\n/);\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i]!.trim();\n if (!trimmed || trimmed.startsWith(\"#\")) continue;\n // Strip a leading `export ` so `export KEY=value` (a common hand-edit)\n // parses the same as `KEY=value`.\n const line = trimmed.replace(/^export\\s+/, \"\");\n const eq = line.indexOf(\"=\");\n const key = eq > 0 ? line.slice(0, eq).trim() : \"\";\n if (eq <= 0 || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {\n console.warn(`credentials: skipping unparseable line ${i + 1}: ${trimmed}`);\n continue;\n }\n let value = line.slice(eq + 1).trim();\n if (\n (value.startsWith('\"') && value.endsWith('\"')) ||\n (value.startsWith(\"'\") && value.endsWith(\"'\"))\n ) {\n value = value.slice(1, -1);\n }\n out[key] = value;\n }\n return out;\n}\n\n/** Load credentials from `path` (default: canonical file) into `process.env`.\n * `process.env` values win — file-defined keys are only applied when the\n * env var is currently undefined. Missing/unreadable file is a silent\n * no-op; commands that need the credentials will fail downstream with\n * their own clear error. Returns the keys actually applied (diagnostics). */\nexport function loadCredentialsIntoEnv(path: string = defaultCredentialsPath()): string[] {\n let contents: string;\n try {\n contents = readFileSync(path, \"utf-8\");\n } catch {\n return [];\n }\n const parsed = parseEnvFile(contents);\n const applied: string[] = [];\n for (const [k, v] of Object.entries(parsed)) {\n if (process.env[k] === undefined) {\n process.env[k] = v;\n applied.push(k);\n }\n }\n return applied;\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAC7B,SAAS,eAAe;AACxB,SAAS,YAAY;AAId,SAAS,yBAAiC;AAC/C,QAAM,OAAO,QAAQ,IAAI,mBAAmB,KAAK,QAAQ,GAAG,SAAS;AACrE,SAAO,KAAK,MAAM,iBAAiB,iBAAiB;AACtD;AASO,SAAS,aAAa,UAA0C;AACrE,QAAM,MAA8B,CAAC;AACrC,QAAM,QAAQ,SAAS,MAAM,OAAO;AACpC,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,UAAU,MAAM,CAAC,EAAG,KAAK;AAC/B,QAAI,CAAC,WAAW,QAAQ,WAAW,GAAG,EAAG;AAGzC,UAAM,OAAO,QAAQ,QAAQ,cAAc,EAAE;AAC7C,UAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,UAAM,MAAM,KAAK,IAAI,KAAK,MAAM,GAAG,EAAE,EAAE,KAAK,IAAI;AAChD,QAAI,MAAM,KAAK,CAAC,2BAA2B,KAAK,GAAG,GAAG;AACpD,cAAQ,KAAK,0CAA0C,IAAI,CAAC,KAAK,OAAO,EAAE;AAC1E;AAAA,IACF;AACA,QAAI,QAAQ,KAAK,MAAM,KAAK,CAAC,EAAE,KAAK;AACpC,QACG,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,KAC3C,MAAM,WAAW,GAAG,KAAK,MAAM,SAAS,GAAG,GAC5C;AACA,cAAQ,MAAM,MAAM,GAAG,EAAE;AAAA,IAC3B;AACA,QAAI,GAAG,IAAI;AAAA,EACb;AACA,SAAO;AACT;AAOO,SAAS,uBAAuB,OAAe,uBAAuB,GAAa;AACxF,MAAI;AACJ,MAAI;AACF,eAAW,aAAa,MAAM,OAAO;AAAA,EACvC,QAAQ;AACN,WAAO,CAAC;AAAA,EACV;AACA,QAAM,SAAS,aAAa,QAAQ;AACpC,QAAM,UAAoB,CAAC;AAC3B,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,GAAG;AAC3C,QAAI,QAAQ,IAAI,CAAC,MAAM,QAAW;AAChC,cAAQ,IAAI,CAAC,IAAI;AACjB,cAAQ,KAAK,CAAC;AAAA,IAChB;AAAA,EACF;AACA,SAAO;AACT;","names":[]}