youiyoux 0.1.10 → 0.1.13

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 (72) hide show
  1. package/dist/chunks/{chunk-HDJ2TOWZ.js → chunk-5I2VHSEN.js} +1 -1
  2. package/dist/chunks/{chunk-T3KNPVQG.js → chunk-DX2NKY2M.js} +4 -4
  3. package/dist/chunks/{chunk-75DBKEAH.js → chunk-EV62TUDV.js} +1119 -619
  4. package/dist/chunks/{chunk-3RP664IP.js → chunk-N5Z5A637.js} +2 -2
  5. package/dist/chunks/{chunk-DWYDHVLZ.js → chunk-NF4OJYKC.js} +106 -66
  6. package/dist/chunks/{chunk-X2YYMHR4.js → chunk-PCLXMWZ2.js} +3 -3
  7. package/dist/chunks/{chunk-OCZJ3JGH.js → chunk-PWGZRP3C.js} +3 -3
  8. package/dist/chunks/{chunk-FLBRX6Y3.js → chunk-XOXF2ILO.js} +1 -1
  9. package/dist/chunks/{chunk-6GITMASZ.js → chunk-ZOQW6LNH.js} +56 -900
  10. package/dist/chunks/chunk-ZWVWENSB.js +899 -0
  11. package/dist/chunks/{src-XTNHB65M.js → src-4QOHR5GC.js} +3 -3
  12. package/dist/chunks/src-624TVB6T.js +55 -0
  13. package/dist/chunks/{src-DPFJ4S2N.js → src-E6ZPFKPR.js} +3 -2
  14. package/dist/chunks/{src-75ODGHE3.js → src-HVYMF4S2.js} +9 -7
  15. package/dist/chunks/{src-BYNWPNRT.js → src-IKDQ2BZO.js} +7 -1
  16. package/dist/chunks/{src-ZG7JU6YJ.js → src-PBM76LFQ.js} +2 -2
  17. package/dist/chunks/{src-6SNQVIZS.js → src-R7JGKOYI.js} +4 -4
  18. package/dist/chunks/{src-PURAHRCR.js → src-YC64NNQ3.js} +2 -2
  19. package/dist/hub/assets/{CandidatePanel-ZzbsyZo3.js → CandidatePanel-DfntneYT.js} +1 -1
  20. package/dist/hub/assets/{CatalogScreen-DDYlvOHy.js → CatalogScreen-kpFYZqX3.js} +1 -1
  21. package/dist/hub/assets/{ChangesScreen-C8_EyHgJ.js → ChangesScreen-Chu-Ui1B.js} +1 -1
  22. package/dist/hub/assets/{CheatSheet-DfcPiXdS.js → CheatSheet-B1dXTYTW.js} +1 -1
  23. package/dist/hub/assets/{CommandPalette-DfFkDTVg.js → CommandPalette-LBZHJ659.js} +1 -1
  24. package/dist/hub/assets/{DiscoverScreen-D6NTgEgk.js → DiscoverScreen-Yd3gVUG4.js} +2 -2
  25. package/dist/hub/assets/DraftEditor-QH9Sa_Uc.js +5 -0
  26. package/dist/hub/assets/{GapsScreen-CqedCj4z.js → GapsScreen-DPYGlNnA.js} +1 -1
  27. package/dist/hub/assets/{HomeScreen-CIj1QTkw.js → HomeScreen-BCPPk7mz.js} +1 -1
  28. package/dist/hub/assets/{InventoryScreen-CyvFXa2_.js → InventoryScreen-SleB733C.js} +1 -1
  29. package/dist/hub/assets/{LibraryScreen-C4WU0f42.js → LibraryScreen-BVXhOVPU.js} +1 -1
  30. package/dist/hub/assets/{Opened-CeRWkA7c.js → Opened-CJyMbxqV.js} +1 -1
  31. package/dist/hub/assets/OrgApp-Dxs_7CxZ.js +1 -0
  32. package/dist/hub/assets/{Preview-CgW-FMB7.js → Preview-DWFP0BIe.js} +1 -1
  33. package/dist/hub/assets/{RuleDetail-BDhZ1gCd.js → RuleDetail-C2fTlLJL.js} +3 -3
  34. package/dist/hub/assets/{RulesScreen-D3rSo_z2.js → RulesScreen-BABeAGJg.js} +3 -3
  35. package/dist/hub/assets/{ScreenImage-DLeA3nRh.js → ScreenImage-CyhKm_nj.js} +1 -1
  36. package/dist/hub/assets/ScreensScreen-8Ih8byGy.js +1 -0
  37. package/dist/hub/assets/{SearchField-DtEgq9-Q.js → SearchField-hF-6-YDM.js} +1 -1
  38. package/dist/hub/assets/{ServiceDown-Bn6AcA3J.js → ServiceDown-S4ya0Cob.js} +1 -1
  39. package/dist/hub/assets/{SpecimenCapture-p6YErF5y.js → SpecimenCapture-e_pgT4Sp.js} +1 -1
  40. package/dist/hub/assets/{Specimens-DuaE0WeO.js → Specimens-D_9hjlJH.js} +2 -2
  41. package/dist/hub/assets/{StackScreen-DHDulDng.js → StackScreen-J2FQilLW.js} +1 -1
  42. package/dist/hub/assets/{StubScreens-CmhVNX_Z.js → StubScreens-DAkMph3i.js} +1 -1
  43. package/dist/hub/assets/StyleScreen-C5WytSZU.js +1 -0
  44. package/dist/hub/assets/{Tabs-Cbh9BCKW.js → Tabs-DU-fFRDV.js} +1 -1
  45. package/dist/hub/assets/{TokenDetail-BICobLK1.js → TokenDetail-CDY1xxb9.js} +1 -1
  46. package/dist/hub/assets/{TokensScreen-CyTHn55q.js → TokensScreen-Sx4aICWk.js} +1 -1
  47. package/dist/hub/assets/{WorkspaceScreen-uPWqfklh.js → WorkspaceScreen-CzZ16fPZ.js} +1 -1
  48. package/dist/hub/assets/{client-BXHiuV_w.js → client-Bdgr12LP.js} +1 -1
  49. package/dist/hub/assets/{discover-CFhyKler.js → discover-va2KFuK_.js} +1 -1
  50. package/dist/hub/assets/{dist-DYNYuTjI.js → dist-C8tf4g8c.js} +1 -1
  51. package/dist/hub/assets/{dist-VmEpVQjP.js → dist-CC_p8DIj.js} +1 -1
  52. package/dist/hub/assets/{dist-C6iTWL0b.js → dist-YTWqg5sS.js} +1 -1
  53. package/dist/hub/assets/{family-tiles-B8_onXUi.js → family-tiles-qaK1-_c8.js} +1 -1
  54. package/dist/hub/assets/main-cvOFSjox.js +2 -0
  55. package/dist/hub/assets/main-pNY4iNRo.css +2 -0
  56. package/dist/hub/assets/{try-live-CX04d2L2.js → try-live-Iys3nCBG.js} +1 -1
  57. package/dist/hub/assets/when-COrv5lkZ.js +1 -0
  58. package/dist/hub/assets/{workflow-DbBdiOmA.js → workflow-C53UBmRZ.js} +1 -1
  59. package/dist/hub/index.html +2 -2
  60. package/dist/hub-previews.js +6 -5
  61. package/dist/hub-server.js +3 -3
  62. package/dist/hub-snapshot.js +1 -1
  63. package/dist/schema/youi.org.schema.json +438 -0
  64. package/dist/schema/youi.record.schema.json +338 -0
  65. package/dist/youi.js +7 -6
  66. package/dist/youiyoux.js +7 -6
  67. package/package.json +4 -4
  68. package/dist/hub/assets/DraftEditor-CUNGYbg0.js +0 -5
  69. package/dist/hub/assets/ScreensScreen-bGuFIyWj.js +0 -1
  70. package/dist/hub/assets/StyleScreen-LwTqfGID.js +0 -1
  71. package/dist/hub/assets/main-7snyIdoi.css +0 -2
  72. package/dist/hub/assets/main-CfbpuT4Y.js +0 -2
@@ -53,7 +53,7 @@ import {
53
53
  vendoredPath,
54
54
  whereUsed,
55
55
  withFile
56
- } from "./chunk-X2YYMHR4.js";
56
+ } from "./chunk-PCLXMWZ2.js";
57
57
  import {
58
58
  escapingLinks,
59
59
  within
@@ -67,31 +67,33 @@ import {
67
67
  libraryOfPackage,
68
68
  packageOf,
69
69
  playwrightCli
70
- } from "./chunk-HDJ2TOWZ.js";
70
+ } from "./chunk-5I2VHSEN.js";
71
71
  import {
72
72
  MissingIndexError,
73
73
  buildGaps,
74
74
  candidateSource,
75
- checkpointSqlite,
76
75
  cleanData,
77
76
  declaredPackages,
78
77
  discoverCacheDir,
79
78
  discoverIndexPath,
80
79
  expandAll,
81
- indexHome,
82
80
  ingestCatalog,
83
- inventoryRun,
84
81
  linkCandidates,
85
- openSqlite,
86
- projectGraph,
87
82
  readCatalog,
88
83
  searchCatalog,
89
84
  selectSources,
90
85
  sharedStore,
91
86
  showFromCatalog,
92
- storeLocation,
93
87
  syncIndex
94
- } from "./chunk-6GITMASZ.js";
88
+ } from "./chunk-ZOQW6LNH.js";
89
+ import {
90
+ checkpointSqlite,
91
+ indexHome,
92
+ inventoryRun,
93
+ openSqlite,
94
+ projectGraph,
95
+ storeLocation
96
+ } from "./chunk-ZWVWENSB.js";
95
97
  import {
96
98
  DL_EXT,
97
99
  buildTokens,
@@ -101,9 +103,10 @@ import {
101
103
  parseCssColor,
102
104
  parseDimension,
103
105
  readRules
104
- } from "./chunk-FLBRX6Y3.js";
106
+ } from "./chunk-XOXF2ILO.js";
105
107
  import {
106
108
  Baseline,
109
+ CiRecord,
107
110
  DESIGN_CONFIG_FILE,
108
111
  DefinitionError,
109
112
  INVENTORY_ROLES,
@@ -114,6 +117,7 @@ import {
114
117
  OutputConfig,
115
118
  PACKAGE_NAME,
116
119
  ProjectConfig,
120
+ RepoName,
117
121
  ScanReport,
118
122
  WORKSPACE_FILE,
119
123
  WorkspaceConfig,
@@ -139,18 +143,19 @@ import {
139
143
  toPosix,
140
144
  unifiedDiff,
141
145
  writeFileAtomic
142
- } from "./chunk-DWYDHVLZ.js";
146
+ } from "./chunk-NF4OJYKC.js";
143
147
  import {
144
148
  __toESM
145
149
  } from "./chunk-WNTS7ZO4.js";
146
150
 
147
151
  // ../cli/src/main.ts
148
152
  var import_yaml4 = __toESM(require_dist(), 1);
149
- import { execFile as execFile5 } from "node:child_process";
153
+ import { execFile as execFile6 } from "node:child_process";
150
154
  import { existsSync as existsSync5 } from "node:fs";
151
- import { appendFile as appendFile2, readFile as readFile10, rm as rm7, rmdir as rmdir3 } from "node:fs/promises";
152
- import { basename as basename5, dirname as dirname13, join as join26, relative as relative11, resolve as resolve15 } from "node:path";
153
- import { parseArgs, promisify as promisify5 } from "node:util";
155
+ import { homedir as homedir4 } from "node:os";
156
+ import { appendFile as appendFile2, readFile as readFile11, rm as rm7, rmdir as rmdir3 } from "node:fs/promises";
157
+ import { basename as basename5, dirname as dirname13, join as join28, relative as relative11, resolve as resolve15 } from "node:path";
158
+ import { parseArgs, promisify as promisify6 } from "node:util";
154
159
 
155
160
  // ../cli/src/check.ts
156
161
  import { dirname as dirname3, join as join7, resolve as resolve2 } from "node:path";
@@ -167,7 +172,7 @@ import { join as join2, relative, resolve } from "node:path";
167
172
 
168
173
  // ../cli/src/command.ts
169
174
  var COMMAND = "youiyoux";
170
- var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "upgrade", "init", "adopt", "status", "system", "sync", "link", "unlink", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "discover", "index", "report", "render", "screens", "requests", "detect", "build", "parity", "schema", "inventory", "gaps", "help"];
175
+ var SUBCOMMANDS = ["show", "where", "bom", "check", "baseline", "rules", "plan", "migrate", "upgrade", "record", "hub", "init", "adopt", "status", "system", "sync", "link", "unlink", "scan", "watch", "hooks", "agents", "mcp", "ui", "library", "discover", "index", "report", "render", "screens", "requests", "detect", "build", "parity", "schema", "inventory", "gaps", "help"];
171
176
 
172
177
  // ../cli/src/agents.ts
173
178
  var import_yaml = __toESM(require_dist(), 1);
@@ -197,8 +202,8 @@ function viaNpx(text, root) {
197
202
  return text.replace(BARE, `${portableYoui(root).run} `);
198
203
  }
199
204
  function diffInstruction(root) {
200
- const { run: run7, setup } = portableYoui(root);
201
- return `- ${setup} After UI edits, run \`${run7} check --diff\` (\`--json\` for data): it lists what your change adds that near-duplicates an existing component, hardcodes a value a token holds, or imports an element from a library other than the project's choice, each with its fix. Apply each fix.`;
205
+ const { run: run8, setup } = portableYoui(root);
206
+ return `- ${setup} After UI edits, run \`${run8} check --diff\` (\`--json\` for data): it lists what your change adds that near-duplicates an existing component, hardcodes a value a token holds, or imports an element from a library other than the project's choice, each with its fix. Apply each fix.`;
202
207
  }
203
208
  function repoAgentsBlock(root) {
204
209
  return viaNpx([BLOCK_BEGIN, REPO_BLOCK_HEADING, "", diffInstruction(root), "- Run `youiyoux init --design <dir>` to connect design-system rules; repository inference works without it.", BLOCK_END].join("\n"), root);
@@ -1063,7 +1068,7 @@ async function runCheck(project2, options = {}) {
1063
1068
  const { report } = scan;
1064
1069
  const checked = new Set(scan.files);
1065
1070
  const unmatched = scope ? [...scope].filter((f) => !checked.has(f)) : [];
1066
- const failed2 = report.coverage.failed.filter((f) => !scope || scope.has(f.file) || !/\.(tsx?|css)$/.test(f.file));
1071
+ const failed3 = report.coverage.failed.filter((f) => !scope || scope.has(f.file) || !/\.(tsx?|css)$/.test(f.file));
1067
1072
  const wantedRule = (id) => !options.rule || id.endsWith(`/rule/${options.rule}`);
1068
1073
  const violations = report.violations.filter((v) => wantedRule(v.rule) && (!options.quiet || v.severity === "error"));
1069
1074
  const notRun = failedChecks(report.checks).filter((c) => wantedRule(c.rule));
@@ -1074,7 +1079,7 @@ async function runCheck(project2, options = {}) {
1074
1079
  const failing = baseline ? [...baseline.comparison.new, ...baseline.comparison.regressed] : violations;
1075
1080
  const errors = failing.filter((v) => v.severity === "error").length + (lock ?? []).filter((i) => i.severity === "error").length;
1076
1081
  const drift = diff ? await analyseChange(project2, scan, diff, { violations }) : null;
1077
- return { identity: scan.identity, scan, scope, unmatched, failed: failed2, notRun, violations, lock, baseline, errors, incomplete: unmatched.length > 0 || failed2.length > 0 || notRun.length > 0 || (drift?.incomplete.length ?? 0) > 0, diff: drift };
1082
+ return { identity: scan.identity, scan, scope, unmatched, failed: failed3, notRun, violations, lock, baseline, errors, incomplete: unmatched.length > 0 || failed3.length > 0 || notRun.length > 0 || (drift?.incomplete.length ?? 0) > 0, diff: drift };
1078
1083
  }
1079
1084
  async function lockIssues(project2) {
1080
1085
  const lock = await verifyLock(project2);
@@ -1288,6 +1293,63 @@ async function installWorkflow(project2, toolRoot, target, force, options = {})
1288
1293
  }
1289
1294
  return write(file, text, before, `authoritative: ${published ? "runs the published youiyoux package; pinned to" : `pinned to ${tool.slug}@${tool.commit.slice(0, 7)} and`} rules ${design.slug}@${design.commit.slice(0, 7)}${diff ? "; pull requests also fail on drift they add (youiyoux check --diff --strict)" : ""}`);
1290
1295
  }
1296
+ var RECORD_WORKFLOW = ".github/workflows/youiyoux-record.yml";
1297
+ var RECORD_HEADER = "# Generated by `youiyoux hooks install --record`.";
1298
+ async function installStep(top) {
1299
+ const has = async (file) => await readTextOrNull(join8(top, file)) !== null;
1300
+ if (await has("pnpm-lock.yaml")) return " - run: corepack enable && pnpm install --frozen-lockfile --ignore-scripts\n";
1301
+ if (await has("yarn.lock")) return " - run: corepack enable && yarn install --immutable --mode=skip-build\n";
1302
+ if (await has("package-lock.json")) return " - run: npm ci --ignore-scripts\n";
1303
+ return null;
1304
+ }
1305
+ function recordWorkflowText(input) {
1306
+ const tool = `youiyoux@${input.version}`;
1307
+ return `${RECORD_HEADER}
1308
+ # Sends where each product here stands with its System to your team's hosted hub
1309
+ # (youiyoux hub serve): youiyoux record, redacted as the shareable report is, with no code.
1310
+ # Set the repository secret YOUIYOUX_HUB_URL (the hub's https address). The record proves this
1311
+ # repository with GitHub's OIDC token (id-token: write), so a hub that takes GitHub's needs no
1312
+ # other secret; a hub on a shared ingest token also needs YOUIYOUX_HUB_TOKEN.
1313
+ # Pinned to youiyoux ${input.version}; change it here. Only GitHub's own actions run here.
1314
+ name: youiyoux record
1315
+ on:
1316
+ push:
1317
+ branches: [${JSON.stringify(input.branch)}]
1318
+ workflow_dispatch:
1319
+ permissions:
1320
+ contents: read
1321
+ id-token: write
1322
+ concurrency:
1323
+ group: youiyoux-record
1324
+ cancel-in-progress: true
1325
+ jobs:
1326
+ record:
1327
+ runs-on: ubuntu-latest
1328
+ timeout-minutes: 10
1329
+ steps:
1330
+ - uses: actions/checkout@v4
1331
+ - uses: actions/setup-node@v4
1332
+ with:
1333
+ node-version: 24
1334
+ ${input.install ?? ""} - name: youiyoux record
1335
+ env:
1336
+ YOUIYOUX_HUB_URL: \${{ secrets.YOUIYOUX_HUB_URL }}
1337
+ YOUIYOUX_HUB_TOKEN: \${{ secrets.YOUIYOUX_HUB_TOKEN }}
1338
+ run: npx -y ${tool} record --send
1339
+ `;
1340
+ }
1341
+ async function installRecordWorkflow(dir, version, force) {
1342
+ if (!version) throw new Error("cannot tell which youiyoux version this is, so the workflow could not be pinned: run npx youiyoux@<version> hooks install --record");
1343
+ const top = await gitOut(dir, ["rev-parse", "--show-toplevel"]).catch(() => {
1344
+ throw new Error(`${dir} is not in a git repository; the record workflow runs in one`);
1345
+ });
1346
+ const remoteHead = await gitOut(top, ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).catch(() => "");
1347
+ const branch = remoteHead.replace(/^origin\//, "") || await gitOut(top, ["symbolic-ref", "--short", "HEAD"]).catch(() => "") || "main";
1348
+ const file = join8(top, RECORD_WORKFLOW);
1349
+ const before = await readTextOrNull(file);
1350
+ if (before !== null && !before.startsWith(RECORD_HEADER) && !force) throw new Error(`${file} exists and was not written by youiyoux; pass --force to replace it`);
1351
+ return write(file, recordWorkflowText({ branch, version, install: await installStep(top) }), before, `sends this repository's record (redacted, no code) to the hub in YOUIYOUX_HUB_URL on each push to ${branch}, proving the repository with GitHub's OIDC token; set YOUIYOUX_HUB_URL as a repository secret (and YOUIYOUX_HUB_TOKEN for a hub on a shared token)`);
1352
+ }
1291
1353
 
1292
1354
  // ../cli/src/check-repo.ts
1293
1355
  async function loadCheckProject(root, options = {}, inferTop = false) {
@@ -1659,7 +1721,7 @@ function githubAnnotations(result, prefix) {
1659
1721
  for (const issue of result.lock ?? []) lines.push(command(issue.severity === "error" ? "error" : "warning", issue.message, { file: `${prefix}${issue.file}`, title: "youi setup" }));
1660
1722
  for (const file of result.unmatched) lines.push(command("error", `${file} was not checked: not in this project's include globs`, { title: "youi incomplete" }));
1661
1723
  for (const failure of result.failed) lines.push(command("error", `could not be fully analysed (${failure.reason}); a clean result here is not a pass`, { file: `${prefix}${failure.file}`, title: "youi incomplete" }));
1662
- for (const run7 of result.notRun) lines.push(command("error", `rule ${ruleSlug(run7.rule)} did not run (${run7.reason ?? run7.state}); its findings are unknown`, { title: "youi incomplete" }));
1724
+ for (const run8 of result.notRun) lines.push(command("error", `rule ${ruleSlug(run8.rule)} did not run (${run8.reason ?? run8.state}); its findings are unknown`, { title: "youi incomplete" }));
1663
1725
  return lines;
1664
1726
  }
1665
1727
  var revision = (state) => state.commit ? `\`${state.commit}\`${state.dirty ? " (dirty)" : ""}` : "not a git checkout";
@@ -1754,8 +1816,8 @@ function findingLines(result, root) {
1754
1816
  const findings2 = result.diff?.findings ?? [];
1755
1817
  const lines = findings2.slice(0, MAX_LISTED).map((f) => `- ${diffFindingLine(f)}`);
1756
1818
  if (findings2.length > MAX_LISTED) {
1757
- const { run: run7, setup } = portableYoui(root);
1758
- lines.push(`- \u2026and ${findings2.length - MAX_LISTED} more. ${setup} Run \`${run7} check --diff\`.`);
1819
+ const { run: run8, setup } = portableYoui(root);
1820
+ lines.push(`- \u2026and ${findings2.length - MAX_LISTED} more. ${setup} Run \`${run8} check --diff\`.`);
1759
1821
  }
1760
1822
  for (const gap of diffGaps(result)) lines.push(`- incomplete: ${gap}; 0 findings there is not a pass`);
1761
1823
  return lines;
@@ -2090,7 +2152,7 @@ import { homedir, userInfo } from "node:os";
2090
2152
  import { join as join12, resolve as resolve6 } from "node:path";
2091
2153
  async function swapImpact(candidate, family) {
2092
2154
  try {
2093
- const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-DPFJ4S2N.js");
2155
+ const { discoverIndexPath: discoverIndexPath2, sharedStore: sharedStore2 } = await import("./src-E6ZPFKPR.js");
2094
2156
  const path = discoverIndexPath2();
2095
2157
  if (!process.env.DATABASE_URL && !existsSync(path)) return null;
2096
2158
  const impact = await (await sharedStore2(path, process.env, { readOnly: true })).swapImpact(candidate, family);
@@ -2101,7 +2163,7 @@ async function swapImpact(candidate, family) {
2101
2163
  }
2102
2164
  async function libraryLicence(id) {
2103
2165
  try {
2104
- const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-DPFJ4S2N.js");
2166
+ const { discoverCacheDir: discoverCacheDir2, findListing } = await import("./src-E6ZPFKPR.js");
2105
2167
  const { entry } = await findListing(discoverCacheDir2(void 0, process.env), id);
2106
2168
  return entry.id === id ? entry.license : null;
2107
2169
  } catch {
@@ -2109,7 +2171,7 @@ async function libraryLicence(id) {
2109
2171
  }
2110
2172
  }
2111
2173
  async function startRuleEditor(options) {
2112
- const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-6SNQVIZS.js");
2174
+ const { FileStore, RuleWorkflow, serveWorkflow } = await import("./src-R7JGKOYI.js");
2113
2175
  const projectRoot = resolve6(options.project);
2114
2176
  const load2 = { ...options.config ? { configPath: resolve6(options.config) } : {}, ...options.design ? { designRoot: resolve6(options.design) } : {}, ...options.root ? { productRoot: resolve6(options.root) } : {} };
2115
2177
  const project2 = await loadProject(projectRoot, load2);
@@ -2786,10 +2848,10 @@ async function upgradeProject(project2, options = {}) {
2786
2848
  if (late.length > 0) return await fail(`the sync conflicted in ${late.map((a) => a.file).join(", ")}`, {});
2787
2849
  const applied = [];
2788
2850
  for (const { spec } of planned) {
2789
- const run7 = await runMigration(project2, spec, { write: true });
2790
- applied.push(run7);
2791
- for (const change of run7.changes) snapshot.remember(change.file, change.before);
2792
- if (run7.status === "rejected") return await fail(`${spec.id} did not hold on the written files: ${gateFailures(run7)}`, { migrations: applied });
2851
+ const run8 = await runMigration(project2, spec, { write: true });
2852
+ applied.push(run8);
2853
+ for (const change of run8.changes) snapshot.remember(change.file, change.before);
2854
+ if (run8.status === "rejected") return await fail(`${spec.id} did not hold on the written files: ${gateFailures(run8)}`, { migrations: applied });
2793
2855
  }
2794
2856
  const residuals = applied.reduce((n, m) => n + m.residuals.length, 0);
2795
2857
  return result("upgraded", { sync: synced.actions.filter((a) => WRITES.has(a.action) || a.action === "kept-local"), migrations: applied }, residuals > 0 ? `fix the ${residuals === 1 ? "site" : `${residuals} sites`} listed for a person, then youiyoux check` : "youiyoux check");
@@ -2825,225 +2887,841 @@ function renderUpgrade(r, showDiff) {
2825
2887
  return lines.join("\n");
2826
2888
  }
2827
2889
 
2828
- // ../cli/src/status.ts
2829
- import { realpath as realpath5 } from "node:fs/promises";
2830
- import { dirname as dirname9, join as join17, relative as relative8, resolve as resolve10 } from "node:path";
2831
- var placeOf = (cwd, root) => {
2832
- const rel = relative8(cwd, root) || ".";
2833
- return rel.split(/[\\/]/).filter((part) => part === "..").length > 1 ? resolve10(root) : rel;
2834
- };
2835
- var WRITES2 = /* @__PURE__ */ new Set(["created", "updated", "merged", "adopted", "conflict", "removed"]);
2836
- function sourceOf(project2) {
2837
- if (project2.linked) return `your checkout ${project2.linked}`;
2838
- const source = project2.config.source;
2839
- if ("package" in source) return `package ${source.package}`;
2840
- if ("git" in source) return `git ${source.git} at ${source.ref}`;
2841
- return resolve10(project2.root) === resolve10(project2.repo.root) ? "this repo" : `path ${source.path}`;
2842
- }
2843
- function nextStep(s, migrations) {
2844
- if (s.sync.state === "never") return "youiyoux sync: this product has never been synced with its System";
2845
- if (s.sync.state === "conflict") return "youiyoux sync: resolve the conflicts it reports, then youiyoux sync --adopt records them";
2846
- if (migrations > 0) return `youiyoux upgrade: ${s.system.name} v${s.system.version} comes with ${plural(migrations, "migration")} for this product's code, which upgrade runs with the sync`;
2847
- if (s.sync.state === "behind") return `youiyoux sync: ${plural(s.sync.changes.length, "file")} would change`;
2848
- if (s.check.incomplete) return "youiyoux check: some files or rules could not be checked, and it says which";
2849
- if (s.check.errors > 0) return `youiyoux check: fix ${plural(s.check.errors, "error")}`;
2850
- if (s.requests.open > 0) return `youiyoux requests: answer ${plural(s.requests.open, "open request")}`;
2851
- if (s.check.warnings > 0 && !s.check.baseline) return `youiyoux check: ${plural(s.check.warnings, "warning")} to fix, or youiyoux baseline records them as accepted debt`;
2852
- return "nothing to do";
2853
- }
2854
- async function projectStatus(project2, cwd = process.cwd()) {
2855
- const lock = await readLock(project2.root);
2856
- const { actions } = await syncProject(project2, { dryRun: true });
2857
- const changes = actions.filter((a) => WRITES2.has(a.action)).map((a) => ({ file: a.file, action: a.action }));
2858
- const baselineFile2 = baselinePath(project2);
2859
- const check = await runCheck(project2, { baseline: await readTextOrNull(baselineFile2) === null ? null : baselineFile2 });
2860
- const comparison = check.baseline?.comparison;
2861
- const counted = comparison ? [...comparison.new, ...comparison.regressed] : check.violations;
2862
- const usages = check.scan.report.usages;
2863
- const { RequestStore } = await import("./src-6SNQVIZS.js");
2864
- const requests = await new RequestStore(project2.root, project2.config.id).list();
2865
- const status = {
2866
- id: project2.config.id,
2867
- root: placeOf(cwd, project2.root),
2868
- system: { id: project2.system.config.id, name: project2.system.config.name, version: project2.system.config.version, source: sourceOf(project2), linked: Boolean(project2.linked), commit: project2.designCommit ?? null },
2869
- lock: lock ? { systemVersion: lock.systemVersion, syncedAt: lock.syncedAt } : null,
2870
- sync: { state: lock === null ? "never" : actions.some((a) => a.action === "conflict") ? "conflict" : changes.length > 0 ? "behind" : "current", changes },
2871
- drift: (await verifyLock(project2) ?? []).map((i) => ({ file: i.file, severity: i.severity, message: i.message })),
2872
- check: {
2873
- errors: check.errors,
2874
- warnings: counted.filter((v) => v.severity === "warn").length,
2875
- baseline: comparison ? { known: comparison.known.length, new: comparison.new.length + comparison.regressed.length, fixed: comparison.fixed.reduce((n, f) => n + f.gone, 0) } : null,
2876
- incomplete: check.incomplete
2877
- },
2878
- components: { system: project2.system.components.length, used: new Set(usages.map((u) => u.element)).size, sites: usages.length },
2879
- requests: { open: requests.requests.filter((r) => r.status === "open").length, addressed: requests.requests.filter((r) => r.status === "addressed").length, unreadable: requests.unreadable.length }
2890
+ // ../cli/src/record.ts
2891
+ import { execFile as execFile5 } from "node:child_process";
2892
+ import { readFileSync as readFileSync2 } from "node:fs";
2893
+ import { join as join20 } from "node:path";
2894
+ import { promisify as promisify5 } from "node:util";
2895
+
2896
+ // ../cli/src/org-hub.ts
2897
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2898
+ import { readFile as readFile6 } from "node:fs/promises";
2899
+ import { createServer } from "node:http";
2900
+ import { extname, join as join17, normalize, sep as sep3 } from "node:path";
2901
+
2902
+ // ../cli/src/github-oidc.ts
2903
+ import { createPublicKey, verify } from "node:crypto";
2904
+ var GITHUB_ISSUER = "https://token.actions.githubusercontent.com";
2905
+ var JWKS_URL = `${GITHUB_ISSUER}/.well-known/jwks`;
2906
+ var LEEWAY_SECONDS = 60;
2907
+ var JWKS_TTL_MS = 60 * 60 * 1e3;
2908
+ var JWKS_RETRY_MS = 60 * 1e3;
2909
+ var audienceOf = (hub) => hub.trim().replace(/\/+$/, "");
2910
+ async function requestGithubToken(audience, env = process.env, fetcher = fetch) {
2911
+ const url = env.ACTIONS_ID_TOKEN_REQUEST_URL;
2912
+ const bearer2 = env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
2913
+ if (!url || !bearer2) return null;
2914
+ const request = new URL(url);
2915
+ request.searchParams.set("audience", audience);
2916
+ const response = await fetcher(request, { headers: { authorization: `bearer ${bearer2}` }, signal: AbortSignal.timeout(15e3) });
2917
+ if (!response.ok) throw new Error(`GitHub Actions did not give an OIDC token (${response.status}): does the workflow have permissions: id-token: write?`);
2918
+ const { value } = await response.json();
2919
+ if (typeof value !== "string") throw new Error("GitHub Actions answered without an OIDC token");
2920
+ return value;
2921
+ }
2922
+ var decode = (part) => JSON.parse(Buffer.from(part, "base64url").toString("utf8"));
2923
+ var DEFAULT_BRANCHES = ["main", "master"];
2924
+ function githubVerifier(options) {
2925
+ const listed = options.owners.map((o) => o.trim().toLowerCase()).filter(Boolean);
2926
+ const ownerIds = new Set(listed.filter((o) => /^\d+$/.test(o)));
2927
+ const ownerNames = new Set(listed.filter((o) => !/^\d+$/.test(o)));
2928
+ const branches = new Set(options.branches && options.branches.length > 0 ? options.branches : DEFAULT_BRANCHES);
2929
+ const issuer = options.issuer ?? GITHUB_ISSUER;
2930
+ const audience = audienceOf(options.audience);
2931
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
2932
+ let cached = null;
2933
+ let lastFetch = 0;
2934
+ const fetchKeys = options.jwks ?? (async () => await (await fetch(JWKS_URL, { signal: AbortSignal.timeout(15e3) })).json());
2935
+ const keyFor = async (kid) => {
2936
+ const find = (jwks) => jwks.keys.find((k) => k.kid === kid && k.kty === "RSA") ?? null;
2937
+ const fresh = cached && Date.now() - cached.at < JWKS_TTL_MS ? find(cached.keys) : null;
2938
+ if (fresh) return fresh;
2939
+ if (cached && Date.now() - lastFetch < JWKS_RETRY_MS) return find(cached.keys);
2940
+ lastFetch = Date.now();
2941
+ cached = { keys: await fetchKeys(), at: Date.now() };
2942
+ return find(cached.keys);
2880
2943
  };
2881
- return { ...status, next: nextStep(status, lock ? pendingMigrations(project2, lock).length : 0) };
2882
- }
2883
- async function statusTargets(cwd, named2) {
2884
- if (named2) return { roots: [resolve10(named2)], workspace: null };
2885
- const product2 = findUp(cwd, PROJECT_FILE);
2886
- if (product2) return { roots: [product2], workspace: null };
2887
- const at = findUp(cwd, WORKSPACE_FILE);
2888
- if (at) {
2889
- const file = join17(at, WORKSPACE_FILE);
2890
- const workspace = await readDefinition(file, WorkspaceConfig);
2891
- return { roots: workspace.projects.map((p) => resolve10(at, p)), workspace: file };
2892
- }
2893
- const top = findUp(cwd, ".git") ?? null;
2894
- if (top === null) return { roots: [], workspace: null };
2895
- const configs = await findFiles(cwd, [`**/${PROJECT_FILE}`]);
2896
- const roots = [];
2897
- for (const config of configs) {
2898
- const dir = resolve10(cwd, dirname9(config));
2899
- if (await realpath5(findUp(dir, ".git") ?? dir) === await realpath5(top)) roots.push(dir);
2900
- }
2901
- return { roots, workspace: null };
2902
- }
2903
- async function workspaceStatus(cwd, named2) {
2904
- const { roots, workspace } = await statusTargets(cwd, named2);
2905
- const projects = [];
2906
- for (const root of roots) {
2944
+ return async (token) => {
2945
+ const parts = token.split(".");
2946
+ if (parts.length !== 3) throw new Error("not a JSON Web Token");
2947
+ const [head, body, signature] = parts;
2948
+ let header;
2949
+ let claims;
2907
2950
  try {
2908
- projects.push(await projectStatus(await loadProject(root), cwd));
2909
- } catch (error) {
2910
- const label = placeOf(cwd, root);
2911
- projects.push({ root: label, error: error instanceof Error ? error.message : String(error), next: await readTextOrNull(join17(root, PROJECT_FILE)) === null ? `youiyoux adopt or youiyoux init --design <dir> in ${label}` : `fix ${join17(label, PROJECT_FILE)}` });
2951
+ header = decode(head);
2952
+ claims = decode(body);
2953
+ } catch {
2954
+ throw new Error("not a JSON Web Token");
2912
2955
  }
2913
- }
2914
- return { schema: "youi.status/v1", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), workspace: workspace ? relative8(cwd, workspace) || WORKSPACE_FILE : null, projects };
2915
- }
2916
- var failed = (p) => "error" in p;
2917
- function projectLines(s) {
2918
- const row = (label, text) => ` ${padEnd(label, 11)} ${text}`;
2919
- const synced = s.lock ? `synced ${s.lock.syncedAt.slice(0, 10)}${s.lock.systemVersion !== s.system.version ? ` at v${s.lock.systemVersion}` : ""}` : "never synced";
2920
- const sync = { current: style.ok("up to date"), behind: style.warn(`behind: ${plural(s.sync.changes.length, "file")} would change`), conflict: style.error(`conflict: ${plural(s.sync.changes.filter((c) => c.action === "conflict").length, "file")}`), never: style.warn("never synced") }[s.sync.state];
2921
- const edited = s.drift.filter((d) => d.severity === "warn");
2922
- const broken = s.drift.filter((d) => d.severity === "error");
2923
- const drift = s.drift.length === 0 ? style.ok("none") : [broken.length > 0 ? style.error(`${plural(broken.length, "file")} wrong (${broken.slice(0, 3).map((d) => d.file.split("/").at(-1)).join(", ")})`) : "", edited.length > 0 ? style.warn(`${plural(edited.length, "owned file")} edited locally, merged on sync (${edited.slice(0, 3).map((d) => d.file.split("/").at(-1)).join(", ")})`) : ""].filter(Boolean).join("; ");
2924
- const { check } = s;
2925
- const counts = `${check.errors > 0 ? style.error(plural(check.errors, "error")) : style.ok("0 errors")} \xB7 ${plural(check.warnings, "warning")}`;
2926
- const baseline = check.baseline ? style.dim(` beyond the baseline (${check.baseline.known} accepted${check.baseline.fixed > 0 ? `, ${check.baseline.fixed} fixed` : ""})`) : "";
2927
- return [
2928
- `${style.bold(s.id)}${s.root === s.id ? "" : ` ${style.dim(`\xB7 ${s.root}`)}`} \xB7 ${s.system.name} v${s.system.version} ${style.dim(`(${s.system.source}${s.system.linked ? ", linked" : ""}) \xB7 ${synced}`)}`,
2929
- row("sync", sync),
2930
- row("drift", drift),
2931
- row("check", `${counts}${baseline}${check.incomplete ? style.warn(" \xB7 incomplete: not a pass") : ""}`),
2932
- row("components", `${s.components.used} of ${s.components.system} System components used, at ${plural(s.components.sites, "site")}`),
2933
- row("requests", s.requests.open + s.requests.addressed + s.requests.unreadable === 0 ? style.dim("none") : [s.requests.open > 0 ? style.warn(`${s.requests.open} open`) : "0 open", `${s.requests.addressed} addressed`, ...s.requests.unreadable > 0 ? [style.error(`${s.requests.unreadable} unreadable`)] : []].join(" \xB7 ")),
2934
- `${style.bold("Next:")} ${s.next}`
2935
- ];
2956
+ if (header.alg !== "RS256") throw new Error(`signed with ${String(header.alg)}, not GitHub's RS256`);
2957
+ const jwk = await keyFor(typeof header.kid === "string" ? header.kid : void 0);
2958
+ if (!jwk) throw new Error("not signed by a key GitHub publishes");
2959
+ if (!verify("RSA-SHA256", Buffer.from(`${head}.${body}`), createPublicKey({ key: { kty: "RSA", n: String(jwk.n), e: String(jwk.e) }, format: "jwk" }), Buffer.from(signature, "base64url"))) throw new Error("its signature is not GitHub's");
2960
+ if (claims.iss !== issuer) throw new Error(`issued by ${String(claims.iss)}, not GitHub Actions`);
2961
+ const audiences = Array.isArray(claims.aud) ? claims.aud : [claims.aud];
2962
+ if (!audiences.some((a) => typeof a === "string" && audienceOf(a) === audience)) throw new Error(`issued for ${audiences.map(String).join(", ")}, not this hub (${audience})`);
2963
+ const seconds = now().getTime() / 1e3;
2964
+ if (typeof claims.exp !== "number" || claims.exp < seconds - LEEWAY_SECONDS) throw new Error("expired");
2965
+ if (typeof claims.nbf === "number" && claims.nbf > seconds + LEEWAY_SECONDS) throw new Error("not valid yet");
2966
+ const { repository, repository_owner: owner, repository_owner_id: ownerId, sha, ref, event_name: event } = claims;
2967
+ if (typeof repository !== "string" || typeof owner !== "string" || typeof sha !== "string" || typeof ref !== "string") throw new Error("missing its repository, owner, commit or ref");
2968
+ if (!(typeof ownerId === "string" && ownerIds.has(ownerId)) && !ownerNames.has(owner.toLowerCase())) throw new Error(`from ${owner}${typeof ownerId === "string" ? ` (id ${ownerId})` : ""}, which this hub does not take records from`);
2969
+ if (!ref.startsWith("refs/heads/") || !branches.has(ref.slice("refs/heads/".length))) throw new Error(`from ${ref}: this hub takes records from ${[...branches].join(" and ")} only (YOUIYOUX_HUB_BRANCHES)`);
2970
+ return { repository, repository_owner: owner, sha, ref, ...typeof event === "string" ? { event_name: event } : {} };
2971
+ };
2936
2972
  }
2937
- function renderStatus(report) {
2938
- if (report.projects.length === 0) return `${style.warn("!")} no product here: no ${PROJECT_FILE} upward, no ${WORKSPACE_FILE}, and none in this repository below here
2939
- Next: youiyoux adopt makes this product's own UI its design system, or youiyoux init --design <dir> follows one`;
2940
- const blocks = report.projects.map((p) => failed(p) ? [`${style.bold(p.root)} ${style.error("\u2716")} ${p.error}`, `${style.bold("Next:")} ${p.next}`] : projectLines(p));
2941
- const head = report.projects.length > 1 ? [`${style.bold("workspace")} ${style.dim(`\xB7 ${plural(report.projects.length, "product")}${report.workspace ? ` from ${report.workspace}` : " in this repository"}`)}`, ""] : [];
2942
- return [...head, ...blocks.flatMap((b, i) => i === 0 ? b : ["", ...b])].join("\n");
2973
+ function recordMismatch(claims, record) {
2974
+ if (claims.repository.toLowerCase() !== record.repo.toLowerCase()) return `the record names ${record.repo}, but it was sent from ${claims.repository}`;
2975
+ if (record.commit !== claims.sha) return `the record names commit ${record.commit ?? "none"}, but it was sent from ${claims.sha}`;
2976
+ if (!claims.ref.startsWith("refs/heads/")) return `it was sent from ${claims.ref}, not a branch`;
2977
+ if (claims.event_name !== void 0 && claims.event_name.startsWith("pull_request")) return `it was sent from a ${claims.event_name} run, not a push to the branch`;
2978
+ const branch = claims.ref.slice("refs/heads/".length);
2979
+ if (record.ref !== branch) return `the record names ${record.ref ?? "no branch"}, but it was sent from ${branch}`;
2980
+ return null;
2943
2981
  }
2944
- var statusExitCode = (report) => report.projects.some((p) => failed(p) || p.check.incomplete || p.check.errors > 0 || p.sync.state === "conflict") ? 1 : 0;
2945
2982
 
2946
- // ../cli/src/workspace-hub.ts
2947
- import { randomBytes as randomBytes2 } from "node:crypto";
2948
- import { createServer } from "node:http";
2949
- var KEY_HEADER = "x-youi-session";
2950
- function send(res, status, body) {
2951
- res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
2983
+ // ../cli/src/org-hub.ts
2984
+ var VIEW_KEY_ENV = "YOUIYOUX_HUB_VIEW_KEY";
2985
+ var GITHUB_OWNERS_ENV = "YOUIYOUX_HUB_GITHUB_OWNERS";
2986
+ var AUDIENCE_ENV = "YOUIYOUX_HUB_AUDIENCE";
2987
+ var GITHUB_TOKEN_HEADER = "x-youi-github-oidc";
2988
+ var FUTURE_MS = 5 * 60 * 1e3;
2989
+ var ORG_META = "youi-org";
2990
+ var COOKIE = "youi_hub_session";
2991
+ var MAX_RECORD = 2 * 1024 * 1024;
2992
+ var MIN_SECRET = 24;
2993
+ var SESSION_DAYS = 30;
2994
+ function secretsProblem(ingest, view, github = false) {
2995
+ const missing = [!github && !ingest && "YOUIYOUX_HUB_TOKEN", !view && VIEW_KEY_ENV].filter(Boolean);
2996
+ if (missing.length > 0) return `set ${missing.join(" and ")} in the environment: a random string of ${MIN_SECRET} characters or more${missing.length > 1 ? " each" : ""} (openssl rand -base64 32)${github ? "" : `, the token for CI and the view key for people; or ${GITHUB_OWNERS_ENV} and ${AUDIENCE_ENV}, so GitHub Actions proves each record's repository and no CI token is shared`}`;
2997
+ if (ingest !== void 0 && ingest.length < MIN_SECRET || view.length < MIN_SECRET) return `${ingest !== void 0 ? "YOUIYOUX_HUB_TOKEN and " : ""}${VIEW_KEY_ENV} must be ${MIN_SECRET} characters or more (openssl rand -base64 32)`;
2998
+ if (ingest === view) return `YOUIYOUX_HUB_TOKEN and ${VIEW_KEY_ENV} must differ: whoever can read the hub must not be able to write to it`;
2999
+ return null;
3000
+ }
3001
+ var digest = (text) => createHash("sha256").update(text).digest();
3002
+ var same = (given, expected) => timingSafeEqual(digest(given), digest(expected));
3003
+ var bearer = (req) => /^Bearer (.+)$/.exec(req.headers.authorization ?? "")?.[1] ?? null;
3004
+ var cookie = (req, name) => {
3005
+ for (const part of (req.headers.cookie ?? "").split(";")) {
3006
+ const [key2, ...value] = part.trim().split("=");
3007
+ if (key2 === name) return value.join("=");
3008
+ }
3009
+ return null;
3010
+ };
3011
+ var HEADERS = {
3012
+ "X-Content-Type-Options": "nosniff",
3013
+ "Referrer-Policy": "no-referrer",
3014
+ "X-Frame-Options": "DENY",
3015
+ "X-Robots-Tag": "noindex, nofollow",
3016
+ "Content-Security-Policy": csp([])
3017
+ };
3018
+ function csp(inline) {
3019
+ const hashes = inline.map((script) => ` 'sha256-${createHash("sha256").update(script).digest("base64")}'`).join("");
3020
+ return `default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; script-src 'self'${hashes}; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'`;
3021
+ }
3022
+ function send(res, status, body, extra = {}) {
3023
+ res.writeHead(status, { ...HEADERS, "Content-Type": "application/json", "Cache-Control": "no-store", ...extra });
2952
3024
  res.end(JSON.stringify(body));
2953
3025
  }
2954
- async function readBody(req) {
2955
- let text = "";
3026
+ async function readBody(req, limit) {
3027
+ const chunks = [];
3028
+ let size = 0;
2956
3029
  for await (const chunk2 of req) {
2957
- text += String(chunk2);
2958
- if (text.length > 4096) throw new Error("request too large");
3030
+ size += chunk2.length;
3031
+ if (size > limit) throw Object.assign(new Error(`a record is at most ${limit / 1024 / 1024} MB`), { status: 413 });
3032
+ chunks.push(chunk2);
2959
3033
  }
2960
- return text ? JSON.parse(text) : {};
3034
+ return Buffer.concat(chunks).toString("utf8");
2961
3035
  }
2962
- async function startWorkspaceHub(options) {
2963
- const key2 = randomBytes2(18).toString("base64url");
2964
- let report = await options.status();
2965
- let current = options.current;
2966
- let busy = false;
3036
+ function unredacted(record) {
3037
+ const found = /* @__PURE__ */ new Set();
3038
+ const walk = (node) => {
3039
+ if (typeof node === "string") {
3040
+ if (/(?:^|[\s"'`(=,:;])(?:~\/|\/(?:Users|home|root)\/)|[A-Za-z]:\\Users\\/.test(node)) found.add("a path outside the repository");
3041
+ if (/\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:"'<>]+(?::[^\s/@"'<>]*)?@/i.test(node) && !node.includes("[redacted]@")) found.add("a URL with credentials");
3042
+ } else if (Array.isArray(node)) node.forEach(walk);
3043
+ else if (node && typeof node === "object") Object.values(node).forEach(walk);
3044
+ };
3045
+ walk(record.products);
3046
+ return [...found];
3047
+ }
3048
+ var failed = (p) => "error" in p;
3049
+ function orgView(latest, now) {
3050
+ const systems = /* @__PURE__ */ new Map();
3051
+ let products = 0;
3052
+ let unreadable = 0;
3053
+ for (const { record } of latest) {
3054
+ for (const product2 of record.products) {
3055
+ products++;
3056
+ if (failed(product2)) {
3057
+ unreadable++;
3058
+ continue;
3059
+ }
3060
+ const system = systems.get(product2.system.id) ?? { name: product2.system.name, latest: product2.system.version, versions: /* @__PURE__ */ new Map() };
3061
+ if (compareVersions(product2.system.version, system.latest) > 0) system.latest = product2.system.version;
3062
+ const version = product2.lock?.systemVersion ?? null;
3063
+ system.versions.set(version, [...system.versions.get(version) ?? [], { repo: record.repo, id: product2.id }]);
3064
+ systems.set(product2.system.id, system);
3065
+ }
3066
+ }
3067
+ const statuses = latest.flatMap(({ record }) => record.products.filter((p) => !failed(p)));
3068
+ const behind = statuses.filter((p) => p.lock === null || p.lock.systemVersion !== systems.get(p.system.id).latest).length;
3069
+ return {
3070
+ schema: "youi.org/v1",
3071
+ generatedAt: now.toISOString(),
3072
+ repos: latest.map(({ record, receivedAt }) => ({ repo: record.repo, commit: record.commit, ref: record.ref, generatedAt: record.generatedAt, receivedAt, products: record.products })),
3073
+ systems: [...systems].sort(([a], [b]) => a.localeCompare(b)).map(([id, s]) => ({
3074
+ id,
3075
+ name: s.name,
3076
+ latest: s.latest,
3077
+ versions: [...s.versions].sort(([a], [b]) => a === null ? 1 : b === null ? -1 : compareVersions(b, a)).map(([version, list]) => ({ version, products: list }))
3078
+ })),
3079
+ totals: { repos: latest.length, products, unreadable, behind, failing: statuses.filter((p) => p.check.errors > 0 || p.check.incomplete).length }
3080
+ };
3081
+ }
3082
+ var TYPES = { ".js": "text/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".svg": "image/svg+xml", ".woff2": "font/woff2", ".png": "image/png", ".webp": "image/webp", ".json": "application/json" };
3083
+ async function startOrgHub(options) {
3084
+ const problem = secretsProblem(options.github ? void 0 : options.ingestToken, options.viewKey, Boolean(options.github));
3085
+ if (problem) throw new Error(problem);
3086
+ const host = options.host ?? "127.0.0.1";
3087
+ const local = ["127.0.0.1", "localhost", "::1"].includes(host);
3088
+ const secure = options.secureCookies ?? !local;
3089
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
3090
+ const session = createHmac("sha256", options.viewKey).update("youiyoux hub viewer").digest("base64url");
3091
+ const page = options.dist ? (await readFile6(join17(options.dist, "index.html"), "utf8")).replace("</head>", ` <meta name="${ORG_META}" content="/api/org" />
3092
+ </head>`) : null;
3093
+ const pagePolicy = csp(page ? [...page.matchAll(/<script>([\s\S]*?)<\/script>/g)].map((m) => m[1]) : []);
3094
+ const viewer = (req) => {
3095
+ const given = cookie(req, COOKIE) ?? null;
3096
+ const key2 = bearer(req);
3097
+ return given !== null && same(given, session) || key2 !== null && same(key2, options.viewKey);
3098
+ };
3099
+ const cookieHeader = (value, maxAge) => `${COOKIE}=${value}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
3100
+ const serveFile = async (res, path) => {
3101
+ const root = join17(options.dist, "assets");
3102
+ const file = normalize(join17(options.dist, path));
3103
+ if (!(file.startsWith(`${root}${sep3}`) || file === join17(options.dist, "favicon.svg"))) return send(res, 404, { error: "not found" });
3104
+ const body = await readFile6(file).catch(() => null);
3105
+ if (!body) return send(res, 404, { error: "not found" });
3106
+ res.writeHead(200, { ...HEADERS, "Content-Type": TYPES[extname(file)] ?? "application/octet-stream", "Cache-Control": path.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache" });
3107
+ res.end(body);
3108
+ };
2967
3109
  const server = createServer((req, res) => {
2968
3110
  void (async () => {
2969
- const path = (req.url ?? "").split("?")[0];
2970
- if (req.method === "GET" && path === "/api/workspace") return send(res, 200, { report, current });
2971
- if (req.method !== "POST" || path !== "/api/workspace/open" && path !== "/api/workspace/refresh") {
2972
- return send(res, 404, { error: "Not in a workspace hub: Rules and Changes are read-only here; youiyoux ui --project <dir> opens the rule editor on one product" });
2973
- }
2974
- if (req.headers[KEY_HEADER] !== key2) return send(res, 401, { error: "this tab does not carry this run's key" });
2975
- if (busy) return send(res, 409, { error: "another product is opening; try again in a moment" });
2976
- busy = true;
3111
+ const path = (req.url ?? "/").split("?")[0];
3112
+ const method = req.method ?? "GET";
2977
3113
  try {
2978
- if (path === "/api/workspace/refresh") {
2979
- report = await options.status();
2980
- return send(res, 200, { report, current });
3114
+ if (path === "/api/health" && method === "GET") return send(res, 200, { ok: true });
3115
+ if (path === "/api/records" && method === "POST") {
3116
+ const oidc = req.headers[GITHUB_TOKEN_HEADER];
3117
+ let sender = null;
3118
+ if (options.github) {
3119
+ if (typeof oidc !== "string") return send(res, 401, { error: "this hub takes records from GitHub Actions with its OIDC token: give the workflow permissions: id-token: write (youiyoux hooks install --record does)" });
3120
+ try {
3121
+ sender = await options.github(oidc);
3122
+ } catch (error) {
3123
+ return send(res, 401, { error: `the GitHub Actions token was refused: ${error instanceof Error ? error.message : String(error)}` });
3124
+ }
3125
+ } else {
3126
+ const token = bearer(req);
3127
+ if (token === null || !same(token, options.ingestToken)) return send(res, 401, { error: "this hub takes records only with its ingest token (YOUIYOUX_HUB_TOKEN)" });
3128
+ }
3129
+ if (!(req.headers["content-type"] ?? "").startsWith("application/json")) return send(res, 415, { error: "a record is JSON (content-type: application/json)" });
3130
+ let parsed;
3131
+ try {
3132
+ parsed = JSON.parse(await readBody(req, MAX_RECORD));
3133
+ } catch (error) {
3134
+ const status = error.status;
3135
+ return send(res, status ?? 400, { error: status ? error.message : "a record is JSON" });
3136
+ }
3137
+ const record = CiRecord.safeParse(parsed);
3138
+ if (!record.success) return send(res, 422, { error: `not a youi.record/v1: ${record.error.issues.slice(0, 3).map((i) => `${i.path.join(".") || "record"}: ${i.message}`).join("; ")}` });
3139
+ if (sender) {
3140
+ const mismatch = recordMismatch(sender, record.data);
3141
+ if (mismatch) return send(res, 403, { error: `${mismatch}: a repository's CI sends only its own record` });
3142
+ }
3143
+ const received = now();
3144
+ if (Date.parse(record.data.generatedAt) > received.getTime() + FUTURE_MS) return send(res, 422, { error: `the record is dated ${record.data.generatedAt}, after the hub's clock (${received.toISOString()}) by more than five minutes: it would stay its repository's latest, so it was not kept` });
3145
+ const leaks = unredacted(record.data);
3146
+ if (leaks.length > 0) return send(res, 422, { error: `the record holds ${leaks.join(" and ")}, so it was not kept: youiyoux record redacts these` });
3147
+ await options.store.putRecord(record.data, received.toISOString());
3148
+ return send(res, 201, { kept: record.data.repo, products: record.data.products.length });
2981
3149
  }
2982
- const { index } = await readBody(req);
2983
- const entry = typeof index === "number" && Number.isInteger(index) ? report.projects[index] : void 0;
2984
- if (entry === void 0 || options.roots[index] === void 0) return send(res, 400, { error: `no product ${JSON.stringify(index)} in this workspace` });
2985
- if ("error" in entry) return send(res, 400, { error: `${entry.root} cannot be opened: ${entry.error}`, problems: [entry.next] });
2986
- const code = await options.open(options.roots[index]);
2987
- if (code !== 0) return send(res, 500, { error: `the snapshot of ${entry.root} failed (exit ${code}); the hub still shows the product it showed` });
2988
- current = index;
2989
- return send(res, 200, { report, current });
3150
+ if (path === "/api/session" && method === "POST") {
3151
+ let key2;
3152
+ try {
3153
+ key2 = JSON.parse(await readBody(req, 4096)).key;
3154
+ } catch {
3155
+ return send(res, 400, { error: 'send { "key": "\u2026" }' });
3156
+ }
3157
+ if (typeof key2 !== "string" || !same(key2, options.viewKey)) return send(res, 401, { error: "that is not this hub's view key" });
3158
+ return send(res, 200, { ok: true }, { "Set-Cookie": cookieHeader(session, SESSION_DAYS * 86400) });
3159
+ }
3160
+ if (path === "/api/session" && method === "DELETE") return send(res, 200, { ok: true }, { "Set-Cookie": cookieHeader("", 0) });
3161
+ if (path === "/api/org" && method === "GET") {
3162
+ if (!viewer(req)) return send(res, 401, { error: "sign in with the hub's view key" });
3163
+ return send(res, 200, orgView(await options.store.latestRecords(), now()));
3164
+ }
3165
+ if (path.startsWith("/api/")) return send(res, 404, { error: "this hub is read-only: it shows the organisation, and takes records from CI" });
3166
+ if (method !== "GET" && method !== "HEAD") return send(res, 405, { error: "read-only" });
3167
+ if (!page) return send(res, 404, { error: "the hub's page is not built here; the API answers at /api/org" });
3168
+ if (path.startsWith("/assets/") || path === "/favicon.svg") return await serveFile(res, path);
3169
+ res.writeHead(200, { ...HEADERS, "Content-Security-Policy": pagePolicy, "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-cache" });
3170
+ res.end(page);
2990
3171
  } catch (error) {
2991
- return send(res, 400, { error: error instanceof Error ? error.message : String(error) });
2992
- } finally {
2993
- busy = false;
3172
+ process.stderr.write(`youiyoux hub: ${method} ${path} failed: ${error instanceof Error ? error.message : String(error)}
3173
+ `);
3174
+ send(res, 500, { error: "the hub could not read its store; its log says why" });
2994
3175
  }
2995
3176
  })();
2996
3177
  });
2997
- await new Promise((done) => server.listen(0, "127.0.0.1", done));
3178
+ await new Promise((done, fail) => {
3179
+ server.once("error", fail);
3180
+ server.listen(options.port ?? 0, host, done);
3181
+ });
2998
3182
  const address = server.address();
2999
3183
  const port = typeof address === "object" && address ? address.port : 0;
3000
3184
  return {
3001
- url: `http://127.0.0.1:${port}`,
3002
- key: key2,
3185
+ url: `http://${host.includes(":") ? `[${host}]` : host}:${port}`,
3003
3186
  close: () => new Promise((done) => server.close(() => done()))
3004
3187
  };
3005
3188
  }
3006
3189
 
3007
- // ../cli/src/index-store.ts
3008
- import { appendFile, mkdir as mkdir4, readFile as readFile6, stat as stat2, rename as rename2, mkdtemp as mkdtemp4, rm as rm6 } from "node:fs/promises";
3009
- import { dirname as dirname10, join as join18, resolve as resolve11 } from "node:path";
3010
- var SOURCES_FILE = "index-sources.json";
3011
- var SCANS_LOG = "scans.jsonl";
3012
- var EMPTY = { discover: null, inventories: [], projects: [] };
3013
- async function readIndexSources(home) {
3014
- try {
3015
- const raw = JSON.parse(await readFile6(join18(home, SOURCES_FILE), "utf8"));
3016
- return { discover: raw.discover ?? null, inventories: raw.inventories ?? [], projects: raw.projects ?? [] };
3017
- } catch {
3018
- return EMPTY;
3190
+ // ../cli/src/report/safe.ts
3191
+ import { realpathSync } from "node:fs";
3192
+ import { readFile as readFile7 } from "node:fs/promises";
3193
+ import { homedir as homedir2, userInfo as userInfo2 } from "node:os";
3194
+ import { join as join18 } from "node:path";
3195
+
3196
+ // ../cli/src/report/filters.ts
3197
+ var SHOWCASE_GLOBS = ["**/{__stories__,.storybook,storybook,storybook-static,example,examples,demo,demos,playground,showcase,showcases}/**", "**/*.stories.*", "**/*.story.*"];
3198
+ var isShowcase = (file) => /(?:^|\/)(?:__stories__|\.?storybook|storybook-static|examples?|demos?|playground|showcases?)(?:\/|$)|\.(?:stories|story)\./i.test(file.replaceAll("\\", "/"));
3199
+ var ROUTE_EXPORT = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|generateMetadata|generateStaticParams|generateViewport|metadata|viewport)$/;
3200
+ var ROUTE_FILE = /(?:^|\/)(?:page|layout|route|loading|error|global-error|not-found|template|default)\.[jt]sx?$/;
3201
+ function uiDefinition(definition, modules) {
3202
+ if (isShowcase(definition.file)) return false;
3203
+ if (definition.kind !== "react") return true;
3204
+ if (ROUTE_EXPORT.test(definition.name)) return false;
3205
+ const module = modules.get(definition.file);
3206
+ if (!module || module.jsx.length === 0) return false;
3207
+ if (ROUTE_FILE.test(definition.file) && module.exports.some((e) => e.kind === "local" && e.name === "default" && (e.local === definition.name || definition.name === "default"))) return false;
3208
+ return module.jsx.some((jsx) => {
3209
+ let node = jsx.node.parent();
3210
+ while (node) {
3211
+ if (["function_declaration", "variable_declarator", "class_declaration"].includes(String(node.kind())) && node.field("name")?.text() === definition.name) return true;
3212
+ node = node.parent();
3213
+ }
3214
+ return false;
3215
+ });
3216
+ }
3217
+ function maskFunctions(source, pattern) {
3218
+ const chars = source.split("");
3219
+ for (const match of source.matchAll(pattern)) {
3220
+ let depth = 1;
3221
+ let end = match.index + match[0].length;
3222
+ for (; end < source.length && depth > 0; end++) {
3223
+ if (source[end] === "(") depth++;
3224
+ if (source[end] === ")") depth--;
3225
+ }
3226
+ for (let i = match.index; i < end; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
3019
3227
  }
3228
+ return chars.join("");
3020
3229
  }
3021
- async function noteSource(home, change) {
3022
- const sources = await readIndexSources(home);
3023
- const next2 = {
3024
- discover: change.discover ?? sources.discover,
3025
- inventories: change.inventory && !sources.inventories.includes(change.inventory) ? [...sources.inventories, change.inventory] : sources.inventories,
3026
- projects: change.project ? [...sources.projects.filter((p) => p.root !== change.project.root), change.project] : sources.projects
3230
+ var withoutVariables = (source) => maskFunctions(source, /\bvar\s*\(/g);
3231
+ var COMPONENT_WORDS = /^(?:button|btn|input|field|card|badge|chip|pill|tag|dialog|modal|popover|tooltip|toast|tabs?|nav|navbar|sidebar|header|footer|menu|dropdown|select|checkbox|radio|switch|toggle|slider|avatar|table|panel|sheet|drawer|banner|alert|hero|form|icon|kbd|callout|accordion|toolbar)$/;
3232
+ var CONTEXT_WORDS = /^(?:phone|mobile|tablet|desktop|wide|narrow|compact|print|touch|landscape|portrait)$/;
3233
+ var SCALE_STEP = /^(?:\d+(?:\.\d+)?|\d*x*[sl]|\dx[sl]|sm|md|lg|base|full|none|default|px|half)$/i;
3234
+ var GROUP_WORDS = /* @__PURE__ */ new Set(["space", "spacing", "radius", "radii", "rounded", "color", "colour", "colors", "size", "gap"]);
3235
+ var componentOf = (token) => token.cssName.split("-").find((part) => COMPONENT_WORDS.test(part)) ?? null;
3236
+ function tokenUsage(modules) {
3237
+ const count = /* @__PURE__ */ new Map();
3238
+ const byFile = /* @__PURE__ */ new Map();
3239
+ const aliases = /* @__PURE__ */ new Set();
3240
+ for (const module of modules) {
3241
+ if (!module.source.includes("--")) continue;
3242
+ const names = /* @__PURE__ */ new Set();
3243
+ for (const declaration of module.source.split(/[;{}]/)) {
3244
+ const defined = /^\s*(--[\w-]+)\s*:\s*(var\(\s*--[\w-]+\s*\)\s*$)?/.exec(declaration);
3245
+ if (defined) {
3246
+ names.add(defined[1]);
3247
+ if (defined[2]) aliases.add(defined[1]);
3248
+ } else for (const [, name] of declaration.matchAll(/var\(\s*(--[\w-]+)/g)) {
3249
+ names.add(name);
3250
+ count.set(name, (count.get(name) ?? 0) + 1);
3251
+ }
3252
+ }
3253
+ if (names.size > 0) byFile.set(module.file, names);
3254
+ }
3255
+ return { count, byFile, aliases };
3256
+ }
3257
+ function genericity(token, prefix) {
3258
+ const parts = token.cssName.split("-").filter(Boolean);
3259
+ const rest = parts.filter((part, i) => !(i === 0 && part === prefix) && !GROUP_WORDS.has(part));
3260
+ if (rest.some((part) => COMPONENT_WORDS.test(part) || CONTEXT_WORDS.test(part))) return 0;
3261
+ return rest.every((part) => SCALE_STEP.test(part)) ? 2 : 1;
3262
+ }
3263
+ function prefixOf(tokens) {
3264
+ const counts = /* @__PURE__ */ new Map();
3265
+ for (const token of tokens) {
3266
+ const first = token.cssName.split("-")[0];
3267
+ if (first.length <= 3 && token.cssName.includes("-")) counts.set(first, (counts.get(first) ?? 0) + 1);
3268
+ }
3269
+ const [best, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [null, 0];
3270
+ return best && n >= tokens.length / 4 ? best : null;
3271
+ }
3272
+ function scaleTokens(index, file, usage) {
3273
+ const value = (t) => `${t.id.split(".")[0]}\0${JSON.stringify(t.light)}`;
3274
+ const prefix = prefixOf(index.tokens);
3275
+ const rank = new Map(index.tokens.map((t) => [t, genericity(t, prefix)]));
3276
+ const general = new Set(index.tokens.filter((t) => rank.get(t) > 0).map(value));
3277
+ const path = file.toLowerCase();
3278
+ const local = usage.byFile.get(file) ?? /* @__PURE__ */ new Set();
3279
+ const varOf = new Map([...index.byVar].map(([name, t]) => [t, name]));
3280
+ const cssName = (t) => varOf.get(t) ?? `--${t.cssName}`;
3281
+ const tokens = index.tokens.filter((t) => {
3282
+ if (rank.get(t) > 0) return true;
3283
+ const component = componentOf(t);
3284
+ return component !== null && !general.has(value(t)) && new RegExp(`(?:^|[/._-])${component}s?(?:[/._-]|$)`).test(path);
3285
+ });
3286
+ const score = (t) => [rank.get(t), Number(local.has(cssName(t))), Number(!usage.aliases.has(cssName(t))), usage.count.get(cssName(t)) ?? 0, -t.cssName.length];
3287
+ const higher = (a, b) => {
3288
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return b[i] - a[i];
3289
+ return 0;
3290
+ };
3291
+ const sorted = tokens.map((t) => ({ t, s: score(t) })).sort((a, b) => higher(a.s, b.s) || a.t.cssName.localeCompare(b.t.cssName)).map(({ t }) => t);
3292
+ return { ...index, tokens: sorted };
3293
+ }
3294
+ var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3295
+ async function reportTokens(module, tokens, usage) {
3296
+ if (isShowcase(module.file) || module.lang !== "css" && module.jsx.length === 0) return [];
3297
+ let source = withoutVariables(module.source);
3298
+ if (module.lang !== "css") {
3299
+ source = maskFunctions(source, /(?:repeating-)?(?:linear|radial|conic)-gradient\s*\(/gi);
3300
+ source = source.replace(/(?<=[[,]\s*)(["'`])(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\([^)\n]*\))\1(?=\s*[,\]])/g, (literal) => " ".repeat(literal.length));
3301
+ }
3302
+ const clean = source === module.source ? module : await parseModule(module.file, source);
3303
+ if (!clean) return [];
3304
+ const index = scaleTokens(tokens, module.file, usage);
3305
+ return hardcodedValues(clean, index, { isTokenSource: module.lang === "css", added: () => true }).filter((f) => {
3306
+ if (/\bvar\(/.test(f.text)) return false;
3307
+ const kind = /^Hardcoded (colour|radius|spacing|type)\b/.exec(f.message)?.[1];
3308
+ const groups = kind === "colour" ? /^(color|colour)\./ : kind === "radius" ? /^(radius|radii|rounded)\./ : kind === "spacing" ? /^(space|spacing)\./ : /^(type|typography|fontSize)\./;
3309
+ const named2 = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
3310
+ return named2.length > 0 && named2.every((t) => groups.test(t.id));
3311
+ });
3312
+ }
3313
+ function publicLook(caption) {
3314
+ const name = /^(?:<)?([\w.$:-]+)/.exec(caption)?.[1] ?? "Component";
3315
+ const classes = [...caption.matchAll(/\bclass(?:Name)?=("(?:[^"\\]|\\.)*"|'[^']*')/g)].map((m) => `class=${m[1]}`);
3316
+ return [name, ...classes].join(" ");
3317
+ }
3318
+
3319
+ // ../cli/src/report/safe.ts
3320
+ var REDACTED = "[redacted]";
3321
+ var OUTSIDE = "[path outside the repo]";
3322
+ var MIN_VALUE = 6;
3323
+ var BENIGN_ENV = /^(?:npm_|NPM_|PNPM_|NODE_|NVM_|TERM|COLORTERM|LANG$|LANGUAGE$|LC_|SHELL$|SHLVL$|PWD$|OLDPWD$|_$|PATH$|MANPATH$|INFOPATH$|TMPDIR$|TMP$|TEMP$|EDITOR$|VISUAL$|PAGER$|LESS|LOGNAME$|USER$|USERNAME$|HOME$|XPC_|__CF|COMMAND_MODE$|DISPLAY$|HOSTTYPE$|OSTYPE$|MACHTYPE$|INIT_CWD$|FORCE_COLOR$|NO_COLOR$|CI$|COREPACK_|VSCODE_|ITERM_|TERM_|SECURITYSESSIONID$|LaunchInstanceID$|XDG_|HOMEBREW_|SSH_AUTH_SOCK$|STARSHIP_|ZSH|OMZ|P9K|POWERLEVEL|VITEST|TEST$|MODE$|DEV$|PROD$|SSR$|BASE_URL$)/;
3324
+ var PLAIN_WORD = /^[A-Za-z]+$/;
3325
+ var PLAIN = /^(?:true|false|yes|no|on|off|null|undefined|\d+(?:\.\d+)?)$/i;
3326
+ var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3327
+ async function dotenvValues(root) {
3328
+ const files = await findFiles(root, [".env", ".env.*", "**/.env", "**/.env.*"]).catch(() => []);
3329
+ const values = [];
3330
+ for (const file of files) {
3331
+ const text = await readFile7(join18(root, file), "utf8").catch(() => "");
3332
+ for (const line of text.split(/\r?\n/)) {
3333
+ const match = /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*(.*)$/.exec(line);
3334
+ if (!match) continue;
3335
+ const value = match[1].trim().replace(/^(['"`])(.*)\1$/, "$2").replace(/\s+#.*$/, "");
3336
+ if (value.length >= MIN_VALUE) values.push(value);
3337
+ }
3338
+ }
3339
+ return values;
3340
+ }
3341
+ function looksSecret(text) {
3342
+ if (/^(?:sk|pk|rk)_(?:live|test)_|^sk-[A-Za-z0-9]|^gh[pousr]_|^github_pat_|^xox[abposr]-|^AKIA[0-9A-Z]{12}|^AIza[0-9A-Za-z_-]{20}|^glpat-|^eyJ[A-Za-z0-9_-]{10,}\./.test(text)) return true;
3343
+ if (text.length < 20 || /[\s:[\]]/.test(text)) return false;
3344
+ const classes = [/[a-z]/, /[A-Z]/, /\d/].filter((c) => c.test(text)).length;
3345
+ if (classes < 2 || !/\d/.test(text)) return false;
3346
+ const counts = /* @__PURE__ */ new Map();
3347
+ for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
3348
+ const entropy = [...counts.values()].reduce((sum, n) => sum - n / text.length * Math.log2(n / text.length), 0);
3349
+ return entropy >= 3.5;
3350
+ }
3351
+ function withoutRoot(text, root) {
3352
+ const at = escapeRegExp(root);
3353
+ return text.replace(new RegExp(`${at}/`, "g"), "").replace(new RegExp(`${at}(?![\\w.-])`, "g"), ".");
3354
+ }
3355
+ var Redactor = class _Redactor {
3356
+ roots;
3357
+ home;
3358
+ username;
3359
+ values;
3360
+ includeCode;
3361
+ constructor(options) {
3362
+ const real = (path) => {
3363
+ try {
3364
+ return realpathSync(path);
3365
+ } catch {
3366
+ return path;
3367
+ }
3368
+ };
3369
+ this.roots = [.../* @__PURE__ */ new Set([options.root, real(options.root)])].sort((a, b) => b.length - a.length);
3370
+ this.home = options.home ?? homedir2();
3371
+ this.username = options.username ?? (() => {
3372
+ try {
3373
+ return userInfo2().username;
3374
+ } catch {
3375
+ return "";
3376
+ }
3377
+ })();
3378
+ const env = Object.entries(options.env ?? process.env).flatMap(([name, value]) => value && !BENIGN_ENV.test(name) && !PLAIN.test(value.trim()) && !(PLAIN_WORD.test(value) && value.length < 12) ? [value] : []);
3379
+ this.values = [...new Set([...env, ...options.dotenv ?? []].filter((v) => v.length >= MIN_VALUE && !this.isRoot(v)))].sort((a, b) => b.length - a.length);
3380
+ this.includeCode = options.includeCode === true;
3381
+ }
3382
+ static async forRepo(root, options = {}) {
3383
+ return new _Redactor({ ...options, root, dotenv: await dotenvValues(root) });
3384
+ }
3385
+ isRoot(value) {
3386
+ return this.roots.some((root) => value === root || root.startsWith(`${value}/`));
3387
+ }
3388
+ /** One string, cleaned. */
3389
+ text(input) {
3390
+ let text = input;
3391
+ for (const root of this.roots) text = withoutRoot(text, root);
3392
+ text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+(?::[^\s/@"'<>]*)?@/gi, `$1${REDACTED}@`);
3393
+ for (const value of this.values) text = text.split(value).join(REDACTED);
3394
+ text = text.replace(/(^|[\s"'`(=,:;])(?:~\/|\/(?:Users|home|root|var|private|tmp|opt|mnt|media|Volumes|srv|etc|usr|workspace|workspaces|builds|runner|github|nix)\b)[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
3395
+ text = text.replace(/(^|[\s"'`(=,:;])[A-Za-z]:\\[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
3396
+ if (this.home.length > 1) text = text.split(this.home).join("~");
3397
+ if (this.username.length >= 3) text = text.replace(new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), REDACTED);
3398
+ return text;
3399
+ }
3400
+ /** A line of code (--include-code): cleaned, and strings that look like keys or tokens blanked. */
3401
+ code(input) {
3402
+ const assigned = /((?:key|token|secret|passw(?:or)?d|pwd|auth|credential|session|cookie|private|dsn|api[_-]?key)[A-Za-z0-9_]*["']?\s*[:=]\s*)(["'`])([^"'`]{4,})\2/gi;
3403
+ const text = input.replace(assigned, `$1$2${REDACTED}$2`).replace(/(["'`])([^"'`\s]{12,})\1/g, (whole, quote, inner) => looksSecret(inner) ? `${quote}${REDACTED}${quote}` : whole).replace(/[A-Za-z0-9_\-+/=.]{20,}/g, (token) => looksSecret(token) ? REDACTED : token);
3404
+ return this.text(text);
3405
+ }
3406
+ /** The whole model, every string cleaned; code only when it was asked for. */
3407
+ model(value) {
3408
+ const walk = (node, key2) => {
3409
+ if (typeof node === "string") return key2 === "code" ? this.code(node) : this.text(node);
3410
+ if (Array.isArray(node)) return node.map((item) => walk(key2 === "looks" && !this.includeCode && typeof item === "string" ? publicLook(item) : item, null));
3411
+ if (node && typeof node === "object") {
3412
+ const out = {};
3413
+ for (const [k, v] of Object.entries(node)) {
3414
+ if (k === "code" && !this.includeCode) continue;
3415
+ out[k] = k === "light" || k === "dark" ? v : walk(v, k);
3416
+ }
3417
+ return out;
3418
+ }
3419
+ return node;
3420
+ };
3421
+ return walk(value, null);
3422
+ }
3423
+ /** What sensitive text is still in `output`: the names of the kinds found, never the values. */
3424
+ leaks(output) {
3425
+ const found = [];
3426
+ if (this.roots.some((root) => output.includes(root))) found.push("an absolute path to the repo");
3427
+ if (this.home.length > 1 && output.includes(this.home)) found.push("the home directory");
3428
+ if (this.username.length >= 3 && new RegExp(`[/\\\\~]${escapeRegExp(this.username)}(?![A-Za-z0-9])`).test(output)) found.push("the username in a path");
3429
+ if (this.values.some((v) => output.includes(v))) found.push("an environment or .env value");
3430
+ if (/\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@/i.test(output)) found.push("a URL with credentials");
3431
+ return found;
3432
+ }
3433
+ /** Last pass over a written output: clean it, then refuse it if anything sensitive survived. */
3434
+ scrub(output) {
3435
+ let text = output;
3436
+ if (this.leaks(text).length === 0) return text;
3437
+ for (const root of this.roots) text = withoutRoot(text, root);
3438
+ if (this.home.length > 1) text = text.split(this.home).join("~");
3439
+ for (const value of this.values) text = text.split(value).join(REDACTED);
3440
+ text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+:[^\s/@"'<>]+@/gi, `$1${REDACTED}@`);
3441
+ if (this.username.length >= 3) text = text.replace(new RegExp(`([/\\\\~])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), `$1${REDACTED}`);
3442
+ const left = this.leaks(text);
3443
+ if (left.length > 0) throw new Error(`the report still held ${left.join(", ")} after redaction, so nothing was written`);
3444
+ return text;
3445
+ }
3446
+ };
3447
+
3448
+ // ../cli/src/status.ts
3449
+ import { realpath as realpath5 } from "node:fs/promises";
3450
+ import { dirname as dirname9, join as join19, relative as relative8, resolve as resolve10 } from "node:path";
3451
+ var placeOf = (cwd, root) => {
3452
+ const rel = relative8(cwd, root) || ".";
3453
+ return rel.split(/[\\/]/).filter((part) => part === "..").length > 1 ? resolve10(root) : rel;
3454
+ };
3455
+ var WRITES2 = /* @__PURE__ */ new Set(["created", "updated", "merged", "adopted", "conflict", "removed"]);
3456
+ function sourceOf(project2) {
3457
+ if (project2.linked) return `your checkout ${project2.linked}`;
3458
+ const source = project2.config.source;
3459
+ if ("package" in source) return `package ${source.package}`;
3460
+ if ("git" in source) return `git ${source.git} at ${source.ref}`;
3461
+ return resolve10(project2.root) === resolve10(project2.repo.root) ? "this repo" : `path ${source.path}`;
3462
+ }
3463
+ function nextStep(s, migrations) {
3464
+ if (s.sync.state === "never") return "youiyoux sync: this product has never been synced with its System";
3465
+ if (s.sync.state === "conflict") return "youiyoux sync: resolve the conflicts it reports, then youiyoux sync --adopt records them";
3466
+ if (migrations > 0) return `youiyoux upgrade: ${s.system.name} v${s.system.version} comes with ${plural(migrations, "migration")} for this product's code, which upgrade runs with the sync`;
3467
+ if (s.sync.state === "behind") return `youiyoux sync: ${plural(s.sync.changes.length, "file")} would change`;
3468
+ if (s.check.incomplete) return "youiyoux check: some files or rules could not be checked, and it says which";
3469
+ if (s.check.errors > 0) return `youiyoux check: fix ${plural(s.check.errors, "error")}`;
3470
+ if (s.requests.open > 0) return `youiyoux requests: answer ${plural(s.requests.open, "open request")}`;
3471
+ if (s.check.warnings > 0 && !s.check.baseline) return `youiyoux check: ${plural(s.check.warnings, "warning")} to fix, or youiyoux baseline records them as accepted debt`;
3472
+ return "nothing to do";
3473
+ }
3474
+ async function projectStatus(project2, cwd = process.cwd()) {
3475
+ const lock = await readLock(project2.root);
3476
+ const { actions } = await syncProject(project2, { dryRun: true });
3477
+ const changes = actions.filter((a) => WRITES2.has(a.action)).map((a) => ({ file: a.file, action: a.action }));
3478
+ const baselineFile2 = baselinePath(project2);
3479
+ const check = await runCheck(project2, { baseline: await readTextOrNull(baselineFile2) === null ? null : baselineFile2 });
3480
+ const comparison = check.baseline?.comparison;
3481
+ const counted = comparison ? [...comparison.new, ...comparison.regressed] : check.violations;
3482
+ const usages = check.scan.report.usages;
3483
+ const { RequestStore } = await import("./src-R7JGKOYI.js");
3484
+ const requests = await new RequestStore(project2.root, project2.config.id).list();
3485
+ const status = {
3486
+ id: project2.config.id,
3487
+ root: placeOf(cwd, project2.root),
3488
+ system: { id: project2.system.config.id, name: project2.system.config.name, version: project2.system.config.version, source: sourceOf(project2), linked: Boolean(project2.linked), commit: project2.designCommit ?? null },
3489
+ lock: lock ? { systemVersion: lock.systemVersion, syncedAt: lock.syncedAt } : null,
3490
+ sync: { state: lock === null ? "never" : actions.some((a) => a.action === "conflict") ? "conflict" : changes.length > 0 ? "behind" : "current", changes },
3491
+ drift: (await verifyLock(project2) ?? []).map((i) => ({ file: i.file, severity: i.severity, message: i.message })),
3492
+ check: {
3493
+ errors: check.errors,
3494
+ warnings: counted.filter((v) => v.severity === "warn").length,
3495
+ baseline: comparison ? { known: comparison.known.length, new: comparison.new.length + comparison.regressed.length, fixed: comparison.fixed.reduce((n, f) => n + f.gone, 0) } : null,
3496
+ incomplete: check.incomplete
3497
+ },
3498
+ components: { system: project2.system.components.length, used: new Set(usages.map((u) => u.element)).size, sites: usages.length },
3499
+ requests: { open: requests.requests.filter((r) => r.status === "open").length, addressed: requests.requests.filter((r) => r.status === "addressed").length, unreadable: requests.unreadable.length }
3500
+ };
3501
+ return { ...status, next: nextStep(status, lock ? pendingMigrations(project2, lock).length : 0) };
3502
+ }
3503
+ async function statusTargets(cwd, named2) {
3504
+ if (named2) return { roots: [resolve10(named2)], workspace: null };
3505
+ const product2 = findUp(cwd, PROJECT_FILE);
3506
+ if (product2) return { roots: [product2], workspace: null };
3507
+ const at = findUp(cwd, WORKSPACE_FILE);
3508
+ if (at) {
3509
+ const file = join19(at, WORKSPACE_FILE);
3510
+ const workspace = await readDefinition(file, WorkspaceConfig);
3511
+ return { roots: workspace.projects.map((p) => resolve10(at, p)), workspace: file };
3512
+ }
3513
+ const top = findUp(cwd, ".git") ?? null;
3514
+ if (top === null) return { roots: [], workspace: null };
3515
+ const configs = await findFiles(cwd, [`**/${PROJECT_FILE}`]);
3516
+ const roots = [];
3517
+ for (const config of configs) {
3518
+ const dir = resolve10(cwd, dirname9(config));
3519
+ if (await realpath5(findUp(dir, ".git") ?? dir) === await realpath5(top)) roots.push(dir);
3520
+ }
3521
+ return { roots, workspace: null };
3522
+ }
3523
+ async function workspaceStatus(cwd, named2) {
3524
+ const { roots, workspace } = await statusTargets(cwd, named2);
3525
+ const projects = [];
3526
+ for (const root of roots) {
3527
+ try {
3528
+ projects.push(await projectStatus(await loadProject(root), cwd));
3529
+ } catch (error) {
3530
+ const label = placeOf(cwd, root);
3531
+ projects.push({ root: label, error: error instanceof Error ? error.message : String(error), next: await readTextOrNull(join19(root, PROJECT_FILE)) === null ? `youiyoux adopt or youiyoux init --design <dir> in ${label}` : `fix ${join19(label, PROJECT_FILE)}` });
3532
+ }
3533
+ }
3534
+ return { schema: "youi.status/v1", generatedAt: (/* @__PURE__ */ new Date()).toISOString(), workspace: workspace ? relative8(cwd, workspace) || WORKSPACE_FILE : null, projects };
3535
+ }
3536
+ var failed2 = (p) => "error" in p;
3537
+ function projectLines(s) {
3538
+ const row = (label, text) => ` ${padEnd(label, 11)} ${text}`;
3539
+ const synced = s.lock ? `synced ${s.lock.syncedAt.slice(0, 10)}${s.lock.systemVersion !== s.system.version ? ` at v${s.lock.systemVersion}` : ""}` : "never synced";
3540
+ const sync = { current: style.ok("up to date"), behind: style.warn(`behind: ${plural(s.sync.changes.length, "file")} would change`), conflict: style.error(`conflict: ${plural(s.sync.changes.filter((c) => c.action === "conflict").length, "file")}`), never: style.warn("never synced") }[s.sync.state];
3541
+ const edited = s.drift.filter((d) => d.severity === "warn");
3542
+ const broken = s.drift.filter((d) => d.severity === "error");
3543
+ const drift = s.drift.length === 0 ? style.ok("none") : [broken.length > 0 ? style.error(`${plural(broken.length, "file")} wrong (${broken.slice(0, 3).map((d) => d.file.split("/").at(-1)).join(", ")})`) : "", edited.length > 0 ? style.warn(`${plural(edited.length, "owned file")} edited locally, merged on sync (${edited.slice(0, 3).map((d) => d.file.split("/").at(-1)).join(", ")})`) : ""].filter(Boolean).join("; ");
3544
+ const { check } = s;
3545
+ const counts = `${check.errors > 0 ? style.error(plural(check.errors, "error")) : style.ok("0 errors")} \xB7 ${plural(check.warnings, "warning")}`;
3546
+ const baseline = check.baseline ? style.dim(` beyond the baseline (${check.baseline.known} accepted${check.baseline.fixed > 0 ? `, ${check.baseline.fixed} fixed` : ""})`) : "";
3547
+ return [
3548
+ `${style.bold(s.id)}${s.root === s.id ? "" : ` ${style.dim(`\xB7 ${s.root}`)}`} \xB7 ${s.system.name} v${s.system.version} ${style.dim(`(${s.system.source}${s.system.linked ? ", linked" : ""}) \xB7 ${synced}`)}`,
3549
+ row("sync", sync),
3550
+ row("drift", drift),
3551
+ row("check", `${counts}${baseline}${check.incomplete ? style.warn(" \xB7 incomplete: not a pass") : ""}`),
3552
+ row("components", `${s.components.used} of ${s.components.system} System components used, at ${plural(s.components.sites, "site")}`),
3553
+ row("requests", s.requests.open + s.requests.addressed + s.requests.unreadable === 0 ? style.dim("none") : [s.requests.open > 0 ? style.warn(`${s.requests.open} open`) : "0 open", `${s.requests.addressed} addressed`, ...s.requests.unreadable > 0 ? [style.error(`${s.requests.unreadable} unreadable`)] : []].join(" \xB7 ")),
3554
+ `${style.bold("Next:")} ${s.next}`
3555
+ ];
3556
+ }
3557
+ function renderStatus(report) {
3558
+ if (report.projects.length === 0) return `${style.warn("!")} no product here: no ${PROJECT_FILE} upward, no ${WORKSPACE_FILE}, and none in this repository below here
3559
+ Next: youiyoux adopt makes this product's own UI its design system, or youiyoux init --design <dir> follows one`;
3560
+ const blocks = report.projects.map((p) => failed2(p) ? [`${style.bold(p.root)} ${style.error("\u2716")} ${p.error}`, `${style.bold("Next:")} ${p.next}`] : projectLines(p));
3561
+ const head = report.projects.length > 1 ? [`${style.bold("workspace")} ${style.dim(`\xB7 ${plural(report.projects.length, "product")}${report.workspace ? ` from ${report.workspace}` : " in this repository"}`)}`, ""] : [];
3562
+ return [...head, ...blocks.flatMap((b, i) => i === 0 ? b : ["", ...b])].join("\n");
3563
+ }
3564
+ var statusExitCode = (report) => report.projects.some((p) => failed2(p) || p.check.incomplete || p.check.errors > 0 || p.sync.state === "conflict") ? 1 : 0;
3565
+
3566
+ // ../cli/src/record.ts
3567
+ var run6 = promisify5(execFile5);
3568
+ var git3 = async (cwd, args) => run6("git", args, { cwd }).then((r) => r.stdout.trim() || null, () => null);
3569
+ var HUB_URL_ENV = "YOUIYOUX_HUB_URL";
3570
+ var HUB_TOKEN_ENV = "YOUIYOUX_HUB_TOKEN";
3571
+ function toolVersion(root = TOOL_ROOT) {
3572
+ for (const file of [join20(root, "packages/youiyoux/package.json"), join20(root, "../package.json")]) {
3573
+ try {
3574
+ const pkg = JSON.parse(readFileSync2(file, "utf8"));
3575
+ if (pkg.name === "youiyoux" && pkg.version) return pkg.version;
3576
+ } catch {
3577
+ }
3578
+ }
3579
+ return null;
3580
+ }
3581
+ async function buildRecord(cwd, options = {}) {
3582
+ const env = options.env ?? process.env;
3583
+ const top = await git3(cwd, ["rev-parse", "--show-toplevel"]);
3584
+ if (!top) throw new Error("youiyoux record runs in a git checkout: the record names its repository and commit");
3585
+ const repo = options.repo ?? env.GITHUB_REPOSITORY ?? githubSlug(await git3(top, ["remote", "get-url", "origin"]) ?? "");
3586
+ if (!repo || !RepoName.safeParse(repo).success) throw new Error(`${repo ? `"${repo}" is not` : "no repository name found: pass"} --repo owner/name${repo ? "" : " (or run where GITHUB_REPOSITORY is set, or with a GitHub origin remote)"}`);
3587
+ const commit = env.GITHUB_SHA ?? await git3(top, ["rev-parse", "HEAD"]);
3588
+ const branch = env.GITHUB_REF_NAME ?? await git3(top, ["rev-parse", "--abbrev-ref", "HEAD"]);
3589
+ const report = await workspaceStatus(top);
3590
+ if (report.projects.length === 0) throw new Error("no product here: no youi.project.json at the top of this repository, no youi.workspace.json, and none below it");
3591
+ const redactor = await Redactor.forRepo(top, { env });
3592
+ const products = JSON.parse(redactor.scrub(JSON.stringify(redactor.model(report.projects))));
3593
+ return CiRecord.parse({ schema: "youi.record/v1", repo, commit: commit && /^[0-9a-f]{40}$/.test(commit) ? commit : null, ref: branch && branch !== "HEAD" ? branch : null, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), tool: toolVersion() ?? "unknown", products });
3594
+ }
3595
+ function recordsUrl(hub) {
3596
+ let url;
3597
+ try {
3598
+ url = new URL(hub);
3599
+ } catch {
3600
+ throw new Error(`${HUB_URL_ENV} "${hub}" is not a URL: expected the hub's address, such as https://design-hub.acme.dev`);
3601
+ }
3602
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
3603
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new Error(`the hub must be https (${url.protocol}//${url.host} would send the token in the clear); http is only for this machine`);
3604
+ if (url.username || url.password) throw new Error(`${HUB_URL_ENV} carries a user name or password: the token goes in ${HUB_TOKEN_ENV}`);
3605
+ return new URL(`${url.pathname.replace(/\/+$/, "")}/api/records`, url.origin);
3606
+ }
3607
+ async function sendRecord(record, hub, credentials, fetcher = fetch) {
3608
+ const url = recordsUrl(hub);
3609
+ const headers = { "content-type": "application/json", ...credentials.token ? { authorization: `Bearer ${credentials.token}` } : {}, ...credentials.github ? { [GITHUB_TOKEN_HEADER]: credentials.github } : {} };
3610
+ const response = await fetcher(url, { method: "POST", headers, body: JSON.stringify(record), redirect: "error", signal: AbortSignal.timeout(3e4) }).catch((error) => {
3611
+ throw new Error(`could not reach the hub at ${url.origin}: ${error instanceof Error ? error.message : String(error)}`);
3612
+ });
3613
+ if (response.ok) return url.origin;
3614
+ const body = await response.json().catch(() => ({}));
3615
+ const why = typeof body.error === "string" ? body.error : response.statusText;
3616
+ throw new Error(`the hub at ${url.origin} refused the record (${response.status}): ${why}`);
3617
+ }
3618
+ function renderRecord(record, sentTo) {
3619
+ const failed3 = record.products.filter((p) => "error" in p).length;
3620
+ const head = `${style.ok(sentTo ? "sent" : "recorded")} ${record.repo}${record.commit ? style.dim(` at ${record.commit.slice(0, 7)}`) : ""}${record.ref ? style.dim(` on ${record.ref}`) : ""}: ${plural(record.products.length, "product")}${failed3 > 0 ? style.warn(` (${failed3} could not be read)`) : ""}${sentTo ? ` to ${sentTo}` : ""}`;
3621
+ return [head, style.dim(" redacted as the shareable report is: repo-relative paths, no home, username, env values or credentials; no code")].join("\n");
3622
+ }
3623
+
3624
+ // ../cli/src/workspace-hub.ts
3625
+ import { randomBytes as randomBytes2 } from "node:crypto";
3626
+ import { createServer as createServer2 } from "node:http";
3627
+ var KEY_HEADER = "x-youi-session";
3628
+ function send2(res, status, body) {
3629
+ res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
3630
+ res.end(JSON.stringify(body));
3631
+ }
3632
+ async function readBody2(req) {
3633
+ let text = "";
3634
+ for await (const chunk2 of req) {
3635
+ text += String(chunk2);
3636
+ if (text.length > 4096) throw new Error("request too large");
3637
+ }
3638
+ return text ? JSON.parse(text) : {};
3639
+ }
3640
+ async function startWorkspaceHub(options) {
3641
+ const key2 = randomBytes2(18).toString("base64url");
3642
+ let report = await options.status();
3643
+ let current = options.current;
3644
+ let busy = false;
3645
+ const server = createServer2((req, res) => {
3646
+ void (async () => {
3647
+ const path = (req.url ?? "").split("?")[0];
3648
+ if (req.method === "GET" && path === "/api/workspace") return send2(res, 200, { report, current });
3649
+ if (req.method !== "POST" || path !== "/api/workspace/open" && path !== "/api/workspace/refresh") {
3650
+ return send2(res, 404, { error: "Not in a workspace hub: Rules and Changes are read-only here; youiyoux ui --project <dir> opens the rule editor on one product" });
3651
+ }
3652
+ if (req.headers[KEY_HEADER] !== key2) return send2(res, 401, { error: "this tab does not carry this run's key" });
3653
+ if (busy) return send2(res, 409, { error: "another product is opening; try again in a moment" });
3654
+ busy = true;
3655
+ try {
3656
+ if (path === "/api/workspace/refresh") {
3657
+ report = await options.status();
3658
+ return send2(res, 200, { report, current });
3659
+ }
3660
+ const { index } = await readBody2(req);
3661
+ const entry = typeof index === "number" && Number.isInteger(index) ? report.projects[index] : void 0;
3662
+ if (entry === void 0 || options.roots[index] === void 0) return send2(res, 400, { error: `no product ${JSON.stringify(index)} in this workspace` });
3663
+ if ("error" in entry) return send2(res, 400, { error: `${entry.root} cannot be opened: ${entry.error}`, problems: [entry.next] });
3664
+ const code = await options.open(options.roots[index]);
3665
+ if (code !== 0) return send2(res, 500, { error: `the snapshot of ${entry.root} failed (exit ${code}); the hub still shows the product it showed` });
3666
+ current = index;
3667
+ return send2(res, 200, { report, current });
3668
+ } catch (error) {
3669
+ return send2(res, 400, { error: error instanceof Error ? error.message : String(error) });
3670
+ } finally {
3671
+ busy = false;
3672
+ }
3673
+ })();
3674
+ });
3675
+ await new Promise((done) => server.listen(0, "127.0.0.1", done));
3676
+ const address = server.address();
3677
+ const port = typeof address === "object" && address ? address.port : 0;
3678
+ return {
3679
+ url: `http://127.0.0.1:${port}`,
3680
+ key: key2,
3681
+ close: () => new Promise((done) => server.close(() => done()))
3682
+ };
3683
+ }
3684
+
3685
+ // ../cli/src/index-store.ts
3686
+ import { appendFile, mkdir as mkdir4, readFile as readFile8, stat as stat2, rename as rename2, mkdtemp as mkdtemp4, rm as rm6 } from "node:fs/promises";
3687
+ import { dirname as dirname10, join as join21, resolve as resolve11 } from "node:path";
3688
+ var SOURCES_FILE = "index-sources.json";
3689
+ var SCANS_LOG = "scans.jsonl";
3690
+ var EMPTY = { discover: null, inventories: [], projects: [] };
3691
+ async function readIndexSources(home) {
3692
+ try {
3693
+ const raw = JSON.parse(await readFile8(join21(home, SOURCES_FILE), "utf8"));
3694
+ return { discover: raw.discover ?? null, inventories: raw.inventories ?? [], projects: raw.projects ?? [] };
3695
+ } catch {
3696
+ return EMPTY;
3697
+ }
3698
+ }
3699
+ async function noteSource(home, change) {
3700
+ const sources = await readIndexSources(home);
3701
+ const next2 = {
3702
+ discover: change.discover ?? sources.discover,
3703
+ inventories: change.inventory && !sources.inventories.includes(change.inventory) ? [...sources.inventories, change.inventory] : sources.inventories,
3704
+ projects: change.project ? [...sources.projects.filter((p) => p.root !== change.project.root), change.project] : sources.projects
3027
3705
  };
3028
- await writeFileAtomic(join18(home, SOURCES_FILE), `${JSON.stringify(next2, null, 2)}
3706
+ await writeFileAtomic(join21(home, SOURCES_FILE), `${JSON.stringify(next2, null, 2)}
3029
3707
  `);
3030
3708
  }
3031
3709
  async function recordInventory(store, home, report, file) {
3032
- const run7 = inventoryRun(report, resolve11(file));
3033
- const logged = { run: run7.run, source: run7.source, scannedAt: run7.scannedAt, projects: run7.projects, families: run7.families };
3710
+ const run8 = inventoryRun(report, resolve11(file));
3711
+ const logged = { run: run8.run, source: run8.source, scannedAt: run8.scannedAt, projects: run8.projects, families: run8.families };
3034
3712
  await mkdir4(home, { recursive: true });
3035
- await appendFile(join18(home, SCANS_LOG), `${JSON.stringify({ schema: "youi.scan-log/v1", ...logged })}
3713
+ await appendFile(join21(home, SCANS_LOG), `${JSON.stringify({ schema: "youi.scan-log/v1", ...logged })}
3036
3714
  `);
3037
- await store.recordInventory(run7);
3038
- await noteSource(home, { inventory: run7.source });
3715
+ await store.recordInventory(run8);
3716
+ await noteSource(home, { inventory: run8.source });
3039
3717
  await linkCandidates(store);
3040
- return run7;
3718
+ return run8;
3041
3719
  }
3042
3720
  async function recordProject(store, project2, usage) {
3043
3721
  await store.replaceGraph(`project:${project2.root}`, projectGraph({ project: project2.config.id, rules: project2.system.rules, components: project2.system.components.map((c) => ({ id: c.contract.id, name: c.contract.name })), usage }));
3044
3722
  }
3045
3723
  async function readLog(home) {
3046
- const text = await readFile6(join18(home, SCANS_LOG), "utf8").catch(() => "");
3724
+ const text = await readFile8(join21(home, SCANS_LOG), "utf8").catch(() => "");
3047
3725
  return text.split("\n").flatMap((line) => {
3048
3726
  if (!line.trim()) return [];
3049
3727
  try {
@@ -3060,7 +3738,7 @@ async function rebuildIndex(path, options = {}) {
3060
3738
  const home = indexHome(path, env);
3061
3739
  await mkdir4(dirname10(path), { recursive: true });
3062
3740
  const scratch = !env.DATABASE_URL ? await mkdtemp4(`${path}.rebuild-`) : null;
3063
- const target = scratch ? join18(scratch, "index.db") : path;
3741
+ const target = scratch ? join21(scratch, "index.db") : path;
3064
3742
  const store = scratch ? await openSqlite(target) : await sharedStore(path, env);
3065
3743
  try {
3066
3744
  if (env.DATABASE_URL) await store.reset();
@@ -3068,7 +3746,7 @@ async function rebuildIndex(path, options = {}) {
3068
3746
  const sources = await readIndexSources(home);
3069
3747
  const cache = options.discoverCache ?? sources.discover;
3070
3748
  let discover = { cache, chosen: 0, all: 0 };
3071
- if (cache && await stat2(join18(cache, "index.json")).then(() => true, () => false)) {
3749
+ if (cache && await stat2(join21(cache, "index.json")).then(() => true, () => false)) {
3072
3750
  const loaded = await ingestCatalog(store, cache);
3073
3751
  discover = { cache, chosen: loaded.chosen, all: loaded.all };
3074
3752
  }
@@ -3085,25 +3763,25 @@ async function rebuildIndex(path, options = {}) {
3085
3763
  await store.recordInventory(inventoryRun(parsed.data, file));
3086
3764
  replayed++;
3087
3765
  }
3088
- const failed2 = [];
3766
+ const failed3 = [];
3089
3767
  let projects = 0;
3090
3768
  const gone = [];
3091
3769
  for (const source of sources.projects) {
3092
3770
  if (!await stat2(source.root).then((s) => s.isDirectory(), () => false)) {
3093
3771
  gone.push(source.root);
3094
- failed2.push({ root: source.root, reason: "folder not found (moved or deleted), skipped" });
3772
+ failed3.push({ root: source.root, reason: "folder not found (moved or deleted), skipped" });
3095
3773
  continue;
3096
3774
  }
3097
3775
  try {
3098
3776
  const project2 = await loadProject(source.root, source.options);
3099
- const usage = ScanReport.safeParse(await readJson(source.usage ?? join18(project2.root, ".youi/usage.json")));
3777
+ const usage = ScanReport.safeParse(await readJson(source.usage ?? join21(project2.root, ".youi/usage.json")));
3100
3778
  await recordProject(store, project2, usage.success ? usage.data : null);
3101
3779
  projects++;
3102
3780
  } catch (error) {
3103
- failed2.push({ root: source.root, reason: error instanceof Error ? error.message.split("\n")[0] : String(error) });
3781
+ failed3.push({ root: source.root, reason: error instanceof Error ? error.message.split("\n")[0] : String(error) });
3104
3782
  }
3105
3783
  }
3106
- const broken = failed2.filter((f) => !gone.includes(f.root));
3784
+ const broken = failed3.filter((f) => !gone.includes(f.root));
3107
3785
  if (broken.length) throw new Error(`Index rebuild failed: ${broken.map((f) => `${f.root}: ${f.reason}`).join("; ")}`);
3108
3786
  await linkCandidates(store);
3109
3787
  const tables = (await store.stats()).tables;
@@ -3127,7 +3805,7 @@ async function rebuildIndex(path, options = {}) {
3127
3805
  throw error;
3128
3806
  }
3129
3807
  }
3130
- return { location: scratch ? path : store.location, discover, scans: log.length, inventories: { replayed, missing }, projects: { replayed: projects, failed: failed2 }, tables, durationMs: Math.round(performance.now() - started) };
3808
+ return { location: scratch ? path : store.location, discover, scans: log.length, inventories: { replayed, missing }, projects: { replayed: projects, failed: failed3 }, tables, durationMs: Math.round(performance.now() - started) };
3131
3809
  } finally {
3132
3810
  if (scratch) {
3133
3811
  await store.close();
@@ -3315,7 +3993,7 @@ function renderAdoptionRequest(request) {
3315
3993
  // ../cli/src/report/run.ts
3316
3994
  import { randomBytes as randomBytes3 } from "node:crypto";
3317
3995
  import { mkdir as mkdir5, rename as rename3, stat as stat3, writeFile as writeFile2 } from "node:fs/promises";
3318
- import { join as join25, relative as relative10, resolve as resolve14 } from "node:path";
3996
+ import { join as join27, relative as relative10, resolve as resolve14 } from "node:path";
3319
3997
 
3320
3998
  // ../cli/src/report/card.ts
3321
3999
  import { deflateSync } from "node:zlib";
@@ -3480,209 +4158,86 @@ function fit(text, width, max, min) {
3480
4158
  var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
3481
4159
  let c = n;
3482
4160
  for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
3483
- return c >>> 0;
3484
- });
3485
- function crc32(bytes) {
3486
- let c = 4294967295;
3487
- for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
3488
- return (c ^ 4294967295) >>> 0;
3489
- }
3490
- function chunk(type, data) {
3491
- const head = Buffer.alloc(8);
3492
- head.writeUInt32BE(data.length, 0);
3493
- head.write(type, 4, "ascii");
3494
- const crc = Buffer.alloc(4);
3495
- crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
3496
- return Buffer.concat([head, data, crc]);
3497
- }
3498
- function encodePng(width, height, rgb) {
3499
- const header = Buffer.alloc(13);
3500
- header.writeUInt32BE(width, 0);
3501
- header.writeUInt32BE(height, 4);
3502
- header.set([8, 2, 0, 0, 0], 8);
3503
- const raw = Buffer.alloc((width * 3 + 1) * height);
3504
- for (let row = 0; row < height; row++) {
3505
- raw[row * (width * 3 + 1)] = 0;
3506
- raw.set(rgb.subarray(row * width * 3, (row + 1) * width * 3), row * (width * 3 + 1) + 1);
3507
- }
3508
- return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", header), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", new Uint8Array())]);
3509
- }
3510
- function drawCard(input) {
3511
- const canvas = new Canvas(CARD_WIDTH, CARD_HEIGHT, INK.background);
3512
- const margin = 72;
3513
- const inner = CARD_WIDTH - margin * 2;
3514
- canvas.text(margin, 64, "youiyoux report", 3, INK.muted);
3515
- const name = fit(input.repo, inner, 9, 5);
3516
- canvas.text(margin, 104, name.text, name.scale, INK.text);
3517
- const total = input.parts.reduce((n, part) => n + part.places, 0);
3518
- const value = fit(String(total), inner / 2, 14, 8);
3519
- const valueWidth = canvas.text(margin, 236, value.text, value.scale, total > 0 ? INK.text : INK.ok);
3520
- const words = total === 0 ? "places to fix: clean" : total === 1 ? "place to fix" : "places to fix";
3521
- canvas.text(margin + valueWidth + 28, 236 + 7 * value.scale - 7 * 4, words, 4, INK.muted);
3522
- const top = 380;
3523
- const steps = [INK.accent, INK.accent2, INK.accent3];
3524
- canvas.rect(margin, top, inner, 20, INK.track, 10);
3525
- if (total > 0) {
3526
- let x2 = margin;
3527
- const shown2 = input.parts.filter((part) => part.places > 0);
3528
- const room = inner - 6 * (shown2.length - 1);
3529
- input.parts.forEach((part, i) => {
3530
- if (part.places === 0) return;
3531
- const last = part === shown2.at(-1);
3532
- const width = last ? margin + inner - x2 : Math.max(10, Math.round(part.places / total * room));
3533
- canvas.rect(x2, top, width, 20, steps[i] ?? INK.accent3, 10);
3534
- x2 += width + 6;
3535
- });
3536
- }
3537
- let x = margin;
3538
- input.parts.forEach((part, i) => {
3539
- canvas.rect(x, top + 52, 18, 18, part.places > 0 ? steps[i] ?? INK.accent3 : INK.track, 4);
3540
- x += 32;
3541
- x += canvas.text(x, top + 52, part.label.split(" ")[0], 3, INK.muted) + 16;
3542
- x += canvas.text(x, top + 52, String(part.places), 3, part.places > 0 ? INK.text : INK.ok) + 40;
3543
- });
3544
- const credit = "npx youiyoux";
3545
- const creditWidth = textWidth(credit, 3);
3546
- canvas.text(CARD_WIDTH - margin - creditWidth, 560, credit, 3, INK.accent);
3547
- const footer = fit(input.footer, inner - creditWidth - 40, 3, 2);
3548
- canvas.text(margin, 560 + (footer.scale === 3 ? 0 : 4), footer.text, footer.scale, INK.muted);
3549
- return encodePng(CARD_WIDTH, CARD_HEIGHT, canvas.pixels);
3550
- }
3551
-
3552
- // ../cli/src/report/model.ts
3553
- import { readFile as readFile8 } from "node:fs/promises";
3554
- import { join as join22 } from "node:path";
3555
-
3556
- // ../cli/src/report/filters.ts
3557
- var SHOWCASE_GLOBS = ["**/{__stories__,.storybook,storybook,storybook-static,example,examples,demo,demos,playground,showcase,showcases}/**", "**/*.stories.*", "**/*.story.*"];
3558
- var isShowcase = (file) => /(?:^|\/)(?:__stories__|\.?storybook|storybook-static|examples?|demos?|playground|showcases?)(?:\/|$)|\.(?:stories|story)\./i.test(file.replaceAll("\\", "/"));
3559
- var ROUTE_EXPORT = /^(?:GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS|generateMetadata|generateStaticParams|generateViewport|metadata|viewport)$/;
3560
- var ROUTE_FILE = /(?:^|\/)(?:page|layout|route|loading|error|global-error|not-found|template|default)\.[jt]sx?$/;
3561
- function uiDefinition(definition, modules) {
3562
- if (isShowcase(definition.file)) return false;
3563
- if (definition.kind !== "react") return true;
3564
- if (ROUTE_EXPORT.test(definition.name)) return false;
3565
- const module = modules.get(definition.file);
3566
- if (!module || module.jsx.length === 0) return false;
3567
- if (ROUTE_FILE.test(definition.file) && module.exports.some((e) => e.kind === "local" && e.name === "default" && (e.local === definition.name || definition.name === "default"))) return false;
3568
- return module.jsx.some((jsx) => {
3569
- let node = jsx.node.parent();
3570
- while (node) {
3571
- if (["function_declaration", "variable_declarator", "class_declaration"].includes(String(node.kind())) && node.field("name")?.text() === definition.name) return true;
3572
- node = node.parent();
3573
- }
3574
- return false;
3575
- });
3576
- }
3577
- function maskFunctions(source, pattern) {
3578
- const chars = source.split("");
3579
- for (const match of source.matchAll(pattern)) {
3580
- let depth = 1;
3581
- let end = match.index + match[0].length;
3582
- for (; end < source.length && depth > 0; end++) {
3583
- if (source[end] === "(") depth++;
3584
- if (source[end] === ")") depth--;
3585
- }
3586
- for (let i = match.index; i < end; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
3587
- }
3588
- return chars.join("");
3589
- }
3590
- var withoutVariables = (source) => maskFunctions(source, /\bvar\s*\(/g);
3591
- var COMPONENT_WORDS = /^(?:button|btn|input|field|card|badge|chip|pill|tag|dialog|modal|popover|tooltip|toast|tabs?|nav|navbar|sidebar|header|footer|menu|dropdown|select|checkbox|radio|switch|toggle|slider|avatar|table|panel|sheet|drawer|banner|alert|hero|form|icon|kbd|callout|accordion|toolbar)$/;
3592
- var CONTEXT_WORDS = /^(?:phone|mobile|tablet|desktop|wide|narrow|compact|print|touch|landscape|portrait)$/;
3593
- var SCALE_STEP = /^(?:\d+(?:\.\d+)?|\d*x*[sl]|\dx[sl]|sm|md|lg|base|full|none|default|px|half)$/i;
3594
- var GROUP_WORDS = /* @__PURE__ */ new Set(["space", "spacing", "radius", "radii", "rounded", "color", "colour", "colors", "size", "gap"]);
3595
- var componentOf = (token) => token.cssName.split("-").find((part) => COMPONENT_WORDS.test(part)) ?? null;
3596
- function tokenUsage(modules) {
3597
- const count = /* @__PURE__ */ new Map();
3598
- const byFile = /* @__PURE__ */ new Map();
3599
- const aliases = /* @__PURE__ */ new Set();
3600
- for (const module of modules) {
3601
- if (!module.source.includes("--")) continue;
3602
- const names = /* @__PURE__ */ new Set();
3603
- for (const declaration of module.source.split(/[;{}]/)) {
3604
- const defined = /^\s*(--[\w-]+)\s*:\s*(var\(\s*--[\w-]+\s*\)\s*$)?/.exec(declaration);
3605
- if (defined) {
3606
- names.add(defined[1]);
3607
- if (defined[2]) aliases.add(defined[1]);
3608
- } else for (const [, name] of declaration.matchAll(/var\(\s*(--[\w-]+)/g)) {
3609
- names.add(name);
3610
- count.set(name, (count.get(name) ?? 0) + 1);
3611
- }
3612
- }
3613
- if (names.size > 0) byFile.set(module.file, names);
3614
- }
3615
- return { count, byFile, aliases };
4161
+ return c >>> 0;
4162
+ });
4163
+ function crc32(bytes) {
4164
+ let c = 4294967295;
4165
+ for (const byte of bytes) c = CRC_TABLE[(c ^ byte) & 255] ^ c >>> 8;
4166
+ return (c ^ 4294967295) >>> 0;
3616
4167
  }
3617
- function genericity(token, prefix) {
3618
- const parts = token.cssName.split("-").filter(Boolean);
3619
- const rest = parts.filter((part, i) => !(i === 0 && part === prefix) && !GROUP_WORDS.has(part));
3620
- if (rest.some((part) => COMPONENT_WORDS.test(part) || CONTEXT_WORDS.test(part))) return 0;
3621
- return rest.every((part) => SCALE_STEP.test(part)) ? 2 : 1;
4168
+ function chunk(type, data) {
4169
+ const head = Buffer.alloc(8);
4170
+ head.writeUInt32BE(data.length, 0);
4171
+ head.write(type, 4, "ascii");
4172
+ const crc = Buffer.alloc(4);
4173
+ crc.writeUInt32BE(crc32(Buffer.concat([head.subarray(4), data])), 0);
4174
+ return Buffer.concat([head, data, crc]);
3622
4175
  }
3623
- function prefixOf(tokens) {
3624
- const counts = /* @__PURE__ */ new Map();
3625
- for (const token of tokens) {
3626
- const first = token.cssName.split("-")[0];
3627
- if (first.length <= 3 && token.cssName.includes("-")) counts.set(first, (counts.get(first) ?? 0) + 1);
4176
+ function encodePng(width, height, rgb) {
4177
+ const header = Buffer.alloc(13);
4178
+ header.writeUInt32BE(width, 0);
4179
+ header.writeUInt32BE(height, 4);
4180
+ header.set([8, 2, 0, 0, 0], 8);
4181
+ const raw = Buffer.alloc((width * 3 + 1) * height);
4182
+ for (let row = 0; row < height; row++) {
4183
+ raw[row * (width * 3 + 1)] = 0;
4184
+ raw.set(rgb.subarray(row * width * 3, (row + 1) * width * 3), row * (width * 3 + 1) + 1);
3628
4185
  }
3629
- const [best, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [null, 0];
3630
- return best && n >= tokens.length / 4 ? best : null;
3631
- }
3632
- function scaleTokens(index, file, usage) {
3633
- const value = (t) => `${t.id.split(".")[0]}\0${JSON.stringify(t.light)}`;
3634
- const prefix = prefixOf(index.tokens);
3635
- const rank = new Map(index.tokens.map((t) => [t, genericity(t, prefix)]));
3636
- const general = new Set(index.tokens.filter((t) => rank.get(t) > 0).map(value));
3637
- const path = file.toLowerCase();
3638
- const local = usage.byFile.get(file) ?? /* @__PURE__ */ new Set();
3639
- const varOf = new Map([...index.byVar].map(([name, t]) => [t, name]));
3640
- const cssName = (t) => varOf.get(t) ?? `--${t.cssName}`;
3641
- const tokens = index.tokens.filter((t) => {
3642
- if (rank.get(t) > 0) return true;
3643
- const component = componentOf(t);
3644
- return component !== null && !general.has(value(t)) && new RegExp(`(?:^|[/._-])${component}s?(?:[/._-]|$)`).test(path);
3645
- });
3646
- const score = (t) => [rank.get(t), Number(local.has(cssName(t))), Number(!usage.aliases.has(cssName(t))), usage.count.get(cssName(t)) ?? 0, -t.cssName.length];
3647
- const higher = (a, b) => {
3648
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return b[i] - a[i];
3649
- return 0;
3650
- };
3651
- const sorted = tokens.map((t) => ({ t, s: score(t) })).sort((a, b) => higher(a.s, b.s) || a.t.cssName.localeCompare(b.t.cssName)).map(({ t }) => t);
3652
- return { ...index, tokens: sorted };
4186
+ return Buffer.concat([Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]), chunk("IHDR", header), chunk("IDAT", deflateSync(raw, { level: 9 })), chunk("IEND", new Uint8Array())]);
3653
4187
  }
3654
- var escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3655
- async function reportTokens(module, tokens, usage) {
3656
- if (isShowcase(module.file) || module.lang !== "css" && module.jsx.length === 0) return [];
3657
- let source = withoutVariables(module.source);
3658
- if (module.lang !== "css") {
3659
- source = maskFunctions(source, /(?:repeating-)?(?:linear|radial|conic)-gradient\s*\(/gi);
3660
- source = source.replace(/(?<=[[,]\s*)(["'`])(?:#[0-9a-fA-F]{3,8}|(?:rgba?|hsla?)\([^)\n]*\))\1(?=\s*[,\]])/g, (literal) => " ".repeat(literal.length));
4188
+ function drawCard(input) {
4189
+ const canvas = new Canvas(CARD_WIDTH, CARD_HEIGHT, INK.background);
4190
+ const margin = 72;
4191
+ const inner = CARD_WIDTH - margin * 2;
4192
+ canvas.text(margin, 64, "youiyoux report", 3, INK.muted);
4193
+ const name = fit(input.repo, inner, 9, 5);
4194
+ canvas.text(margin, 104, name.text, name.scale, INK.text);
4195
+ const total = input.parts.reduce((n, part) => n + part.places, 0);
4196
+ const value = fit(String(total), inner / 2, 14, 8);
4197
+ const valueWidth = canvas.text(margin, 236, value.text, value.scale, total > 0 ? INK.text : INK.ok);
4198
+ const words = total === 0 ? "places to fix: clean" : total === 1 ? "place to fix" : "places to fix";
4199
+ canvas.text(margin + valueWidth + 28, 236 + 7 * value.scale - 7 * 4, words, 4, INK.muted);
4200
+ const top = 380;
4201
+ const steps = [INK.accent, INK.accent2, INK.accent3];
4202
+ canvas.rect(margin, top, inner, 20, INK.track, 10);
4203
+ if (total > 0) {
4204
+ let x2 = margin;
4205
+ const shown2 = input.parts.filter((part) => part.places > 0);
4206
+ const room = inner - 6 * (shown2.length - 1);
4207
+ input.parts.forEach((part, i) => {
4208
+ if (part.places === 0) return;
4209
+ const last = part === shown2.at(-1);
4210
+ const width = last ? margin + inner - x2 : Math.max(10, Math.round(part.places / total * room));
4211
+ canvas.rect(x2, top, width, 20, steps[i] ?? INK.accent3, 10);
4212
+ x2 += width + 6;
4213
+ });
3661
4214
  }
3662
- const clean = source === module.source ? module : await parseModule(module.file, source);
3663
- if (!clean) return [];
3664
- const index = scaleTokens(tokens, module.file, usage);
3665
- return hardcodedValues(clean, index, { isTokenSource: module.lang === "css", added: () => true }).filter((f) => {
3666
- if (/\bvar\(/.test(f.text)) return false;
3667
- const kind = /^Hardcoded (colour|radius|spacing|type)\b/.exec(f.message)?.[1];
3668
- const groups = kind === "colour" ? /^(color|colour)\./ : kind === "radius" ? /^(radius|radii|rounded)\./ : kind === "spacing" ? /^(space|spacing)\./ : /^(type|typography|fontSize)\./;
3669
- const named2 = index.tokens.filter((t) => new RegExp(`[\\s(]${escape(t.id)}(?=[\\s,)]|$)`).test(f.fix));
3670
- return named2.length > 0 && named2.every((t) => groups.test(t.id));
4215
+ let x = margin;
4216
+ input.parts.forEach((part, i) => {
4217
+ canvas.rect(x, top + 52, 18, 18, part.places > 0 ? steps[i] ?? INK.accent3 : INK.track, 4);
4218
+ x += 32;
4219
+ x += canvas.text(x, top + 52, part.label.split(" ")[0], 3, INK.muted) + 16;
4220
+ x += canvas.text(x, top + 52, String(part.places), 3, part.places > 0 ? INK.text : INK.ok) + 40;
3671
4221
  });
3672
- }
3673
- function publicLook(caption) {
3674
- const name = /^(?:<)?([\w.$:-]+)/.exec(caption)?.[1] ?? "Component";
3675
- const classes = [...caption.matchAll(/\bclass(?:Name)?=("(?:[^"\\]|\\.)*"|'[^']*')/g)].map((m) => `class=${m[1]}`);
3676
- return [name, ...classes].join(" ");
4222
+ const credit = "npx youiyoux";
4223
+ const creditWidth = textWidth(credit, 3);
4224
+ canvas.text(CARD_WIDTH - margin - creditWidth, 560, credit, 3, INK.accent);
4225
+ const footer = fit(input.footer, inner - creditWidth - 40, 3, 2);
4226
+ canvas.text(margin, 560 + (footer.scale === 3 ? 0 : 4), footer.text, footer.scale, INK.muted);
4227
+ return encodePng(CARD_WIDTH, CARD_HEIGHT, canvas.pixels);
3677
4228
  }
3678
4229
 
4230
+ // ../cli/src/report/model.ts
4231
+ import { readFile as readFile10 } from "node:fs/promises";
4232
+ import { join as join25 } from "node:path";
4233
+
3679
4234
  // ../cli/src/report/groups.ts
3680
4235
  import { existsSync as existsSync3 } from "node:fs";
3681
- import { join as join20 } from "node:path";
4236
+ import { join as join23 } from "node:path";
3682
4237
 
3683
4238
  // ../cli/src/report/imports.ts
3684
- import { realpathSync } from "node:fs";
3685
- import { dirname as dirname11, join as join19, relative as relative9, resolve as resolve12 } from "node:path";
4239
+ import { realpathSync as realpathSync2 } from "node:fs";
4240
+ import { dirname as dirname11, join as join22, relative as relative9, resolve as resolve12 } from "node:path";
3686
4241
  import ts from "typescript";
3687
4242
  var resolvers = /* @__PURE__ */ new Map();
3688
4243
  function resolverOf(root) {
@@ -3701,13 +4256,13 @@ function clearImportCache() {
3701
4256
  }
3702
4257
  async function verifiedImport(root, keeper, from, suggested) {
3703
4258
  const { options, cache } = resolverOf(root);
3704
- let local = relative9(dirname11(join19(root, from)), join19(root, keeper)).replaceAll("\\", "/").replace(/\.(tsx?|jsx?|mjs)$/, "");
4259
+ let local = relative9(dirname11(join22(root, from)), join22(root, keeper)).replaceAll("\\", "/").replace(/\.(tsx?|jsx?|mjs)$/, "");
3705
4260
  if (!local.startsWith(".")) local = `./${local}`;
3706
4261
  for (const specifier of /* @__PURE__ */ new Set([suggested, local])) {
3707
4262
  const found = ts.resolveModuleName(specifier, resolve12(root, from), options, ts.sys, cache).resolvedModule?.resolvedFileName;
3708
4263
  if (found) {
3709
4264
  try {
3710
- if (realpathSync(found) === realpathSync(join19(root, keeper))) return specifier;
4265
+ if (realpathSync2(found) === realpathSync2(join22(root, keeper))) return specifier;
3711
4266
  } catch {
3712
4267
  }
3713
4268
  }
@@ -4025,7 +4580,7 @@ function commonDir(files) {
4025
4580
  async function sameNamedGroups(facts, defs, usage, importText) {
4026
4581
  const found = [];
4027
4582
  for (const f of facts) {
4028
- const twins = [...f.twins].flatMap(([name, list]) => [...Map.groupBy(list, (t) => f.packageOf(t.file)).values()].map((same) => [name, same]));
4583
+ const twins = [...f.twins].flatMap(([name, list]) => [...Map.groupBy(list, (t) => f.packageOf(t.file)).values()].map((same2) => [name, same2]));
4029
4584
  for (const [name, list] of twins) {
4030
4585
  if (list.length < 2) continue;
4031
4586
  const threshold = GENERIC_NAMES.has(name) ? GENERIC_IDENTICAL : NEAR_IDENTICAL;
@@ -4049,8 +4604,8 @@ async function sameNamedGroups(facts, defs, usage, importText) {
4049
4604
  continue;
4050
4605
  }
4051
4606
  const [keep, ...copies] = members.map((i) => ordered[i]);
4052
- const same = members.every((i, n) => members.slice(n + 1).every((j) => (score.get(`${i}:${j}`) ?? 0) >= 0.97));
4053
- found.push(await sameNamed(f, name, keep, copies, same ? "same" : "near"));
4607
+ const same2 = members.every((i, n) => members.slice(n + 1).every((j) => (score.get(`${i}:${j}`) ?? 0) >= 0.97));
4608
+ found.push(await sameNamed(f, name, keep, copies, same2 ? "same" : "near"));
4054
4609
  }
4055
4610
  const exported = singles.map((i) => ordered[i]).filter((d) => d.exported);
4056
4611
  if (!GENERIC_NAMES.has(name) && exported.length >= 2) found.push(await sameNamed(f, name, exported[0], exported.slice(1), "different"));
@@ -4115,19 +4670,19 @@ function factsOf(project2, id, modules, eligible, workspaceNames, dependencies,
4115
4670
  if (dirs.has(file.slice(0, at))) break;
4116
4671
  dirs.add(file.slice(0, at));
4117
4672
  }
4118
- const roots = [...dirs].filter((dir) => existsSync3(join20(project2.root, dir, "package.json"))).sort((a, b) => b.length - a.length);
4673
+ const roots = [...dirs].filter((dir) => existsSync3(join23(project2.root, dir, "package.json"))).sort((a, b) => b.length - a.length);
4119
4674
  const nested = (file) => roots.find((dir) => file.startsWith(`${dir}/`)) ?? "";
4120
4675
  return { project: project2, id, dependencies, packageImports, jsxCounts, eligible, twins, dirs, importer, packageOf: nested };
4121
4676
  }
4122
4677
 
4123
4678
  // ../cli/src/report/projects.ts
4124
4679
  var import_yaml3 = __toESM(require_dist(), 1);
4125
- import { readFile as readFile7 } from "node:fs/promises";
4126
- import { basename as basename4, join as join21, resolve as resolve13 } from "node:path";
4680
+ import { readFile as readFile9 } from "node:fs/promises";
4681
+ import { basename as basename4, join as join24, resolve as resolve13 } from "node:path";
4127
4682
  var UI_FILES = ["**/*.{tsx,jsx,astro,vue,svelte}"];
4128
4683
  var strings = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : [];
4129
4684
  async function workspaceGlobs(root, pkg) {
4130
- const pnpm = await readFile7(join21(root, "pnpm-workspace.yaml"), "utf8").catch(() => null);
4685
+ const pnpm = await readFile9(join24(root, "pnpm-workspace.yaml"), "utf8").catch(() => null);
4131
4686
  if (pnpm !== null) {
4132
4687
  try {
4133
4688
  const globs2 = strings((0, import_yaml3.parse)(pnpm)?.packages);
@@ -4142,7 +4697,7 @@ async function workspaceGlobs(root, pkg) {
4142
4697
  var hasUi = async (dir) => (await findFiles(dir, UI_FILES, DEFAULT_EXCLUDE)).length > 0;
4143
4698
  async function findProjects(dir) {
4144
4699
  const root = resolve13(dir);
4145
- const pkg = await readJson(join21(root, "package.json"));
4700
+ const pkg = await readJson(join24(root, "package.json"));
4146
4701
  const name = typeof pkg?.name === "string" && pkg.name.trim() ? pkg.name.trim() : basename4(root);
4147
4702
  const single = { dir: ".", root, name: typeof pkg?.name === "string" ? pkg.name : null };
4148
4703
  const workspace = await workspaceGlobs(root, pkg);
@@ -4153,12 +4708,12 @@ async function findProjects(dir) {
4153
4708
  const projects = [];
4154
4709
  const skipped = [];
4155
4710
  for (const member of members) {
4156
- const memberRoot = join21(root, member);
4711
+ const memberRoot = join24(root, member);
4157
4712
  if (!await hasUi(memberRoot)) {
4158
4713
  skipped.push(member);
4159
4714
  continue;
4160
4715
  }
4161
- const memberPkg = await readJson(join21(memberRoot, "package.json"));
4716
+ const memberPkg = await readJson(join24(memberRoot, "package.json"));
4162
4717
  projects.push({ dir: member, root: memberRoot, name: typeof memberPkg?.name === "string" ? memberPkg.name : null });
4163
4718
  }
4164
4719
  if (projects.length === 0) return { root, name, layout: `one project (${workspace.source} names no package with UI files)`, projects: [single], skipped };
@@ -4185,7 +4740,7 @@ var Lines = class {
4185
4740
  if (!this.enabled) return { at };
4186
4741
  let lines = this.files.get(file);
4187
4742
  if (!lines) {
4188
- lines = (await readFile8(join22(this.root, file), "utf8").catch(() => "")).split("\n");
4743
+ lines = (await readFile10(join25(this.root, file), "utf8").catch(() => "")).split("\n");
4189
4744
  this.files.set(file, lines);
4190
4745
  }
4191
4746
  const code = lines[line - 1]?.trim();
@@ -4200,15 +4755,15 @@ function hardcodedValue(finding3) {
4200
4755
  const value = bracket ?? (declaration ? `${declaration[2]} (${declaration[1]})` : finding3.text);
4201
4756
  return { word: word === "type" ? "text size" : word, value, tokens };
4202
4757
  }
4203
- async function modulesOf(project2, target, failed2) {
4758
+ async function modulesOf(project2, target, failed3) {
4204
4759
  const modules = [];
4205
4760
  for (const file of target.files) {
4206
4761
  if (isShowcase(file)) continue;
4207
4762
  try {
4208
- const module = await parseModule(file, await readFile8(join22(project2.root, file), "utf8"));
4763
+ const module = await parseModule(file, await readFile10(join25(project2.root, file), "utf8"));
4209
4764
  if (module) modules.push(module);
4210
4765
  } catch (error) {
4211
- failed2.push(`${repoPath(project2.dir, file)}: ${errorText(error)}`);
4766
+ failed3.push(`${repoPath(project2.dir, file)}: ${errorText(error)}`);
4212
4767
  }
4213
4768
  }
4214
4769
  return modules;
@@ -4229,7 +4784,7 @@ async function grouped(project2, found, lines, describe) {
4229
4784
  return findings2.sort((a, b) => b.count - a.count || a.title.localeCompare(b.title));
4230
4785
  }
4231
4786
  async function analyseProject(project2, index, inventory, inventoryError, workspaceNames, lines) {
4232
- const failed2 = [];
4787
+ const failed3 = [];
4233
4788
  const incomplete = [];
4234
4789
  const notes = [];
4235
4790
  const projectId = inventory?.projects[index]?.id;
@@ -4237,7 +4792,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
4237
4792
  let target = null;
4238
4793
  try {
4239
4794
  target = await loadInventoryTarget(project2.root);
4240
- modules = await modulesOf(project2, target, failed2);
4795
+ modules = await modulesOf(project2, target, failed3);
4241
4796
  } catch (error) {
4242
4797
  incomplete.push(`The code could not be read (${errorText(error)})`);
4243
4798
  }
@@ -4280,7 +4835,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
4280
4835
  const components = [...byComponent.values()].map((c) => ({ name: c.name, file: c.file, uses: c.roles.length, role: [...new Set(c.roles)].sort((a, b) => c.roles.filter((r) => r === b).length - c.roles.filter((r) => r === a).length)[0] })).sort((a, b) => b.uses - a.uses || a.name.localeCompare(b.name)).slice(0, TOP_COMPONENTS);
4281
4836
  const inventoryProject = inventory?.projects[index];
4282
4837
  if (inventoryError) incomplete.push(`The component inventory could not be built (${inventoryError}); run ${NPX} inventory --project ${project2.dir} to see why`);
4283
- for (const f of inventoryProject?.failed ?? []) failed2.push(`${repoPath(project2.dir, f.file)}: ${f.reason}`);
4838
+ for (const f of inventoryProject?.failed ?? []) failed3.push(`${repoPath(project2.dir, f.file)}: ${f.reason}`);
4284
4839
  let hardcoded = [];
4285
4840
  let offLibrary = [];
4286
4841
  const everyLine = () => true;
@@ -4309,7 +4864,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
4309
4864
  incomplete.push(`Library choices were not compared (${errorText(error)}); run ${NPX} detect ${project2.dir} to see why`);
4310
4865
  }
4311
4866
  }
4312
- const pkg = await readFile8(join22(project2.root, "package.json"), "utf8").then((text) => JSON.parse(text), () => null);
4867
+ const pkg = await readFile10(join25(project2.root, "package.json"), "utf8").then((text) => JSON.parse(text), () => null);
4313
4868
  const dependencies = new Set(["dependencies", "devDependencies", "peerDependencies"].flatMap((key2) => Object.keys(pkg?.[key2] ?? {})));
4314
4869
  const importer = changeProject ? await importOf(changeProject) : () => "";
4315
4870
  const facts = factsOf(project2, projectId ?? null, modules, eligible, workspaceNames, dependencies, target?.files ?? [], importer);
@@ -4317,7 +4872,7 @@ async function analyseProject(project2, index, inventory, inventoryError, worksp
4317
4872
  dir: project2.dir,
4318
4873
  name: project2.name,
4319
4874
  files: inventoryProject?.files ?? modules.length,
4320
- failed: failed2,
4875
+ failed: failed3,
4321
4876
  libraries,
4322
4877
  unused,
4323
4878
  definitions: inventory ? inventory.definitions.filter((d) => d.project === projectId && !isShowcase(d.file)).length : 0,
@@ -4751,9 +5306,9 @@ ${model.projects.map((p) => projectSection(p, many, looks)).join("\n")}
4751
5306
 
4752
5307
  // ../cli/src/report/looks.ts
4753
5308
  import { spawn as spawn2 } from "node:child_process";
4754
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "node:fs";
4755
- import { homedir as homedir2 } from "node:os";
4756
- import { dirname as dirname12, join as join23 } from "node:path";
5309
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "node:fs";
5310
+ import { homedir as homedir3 } from "node:os";
5311
+ import { dirname as dirname12, join as join26 } from "node:path";
4757
5312
  var LOOKS_PER_PROJECT = 4;
4758
5313
  var DOWNLOAD_SIZE = "~100 MB";
4759
5314
  var DEPENDENCIES = "Install the repo's dependencies to see rendered looks";
@@ -4761,31 +5316,31 @@ var UNDRAWABLE = /not installed|loads no stylesheet|drawn from (?:its|their) (?:
4761
5316
  var playwrightDir = () => dirname12(playwrightCli());
4762
5317
  function browsersPath(env) {
4763
5318
  const set = env.PLAYWRIGHT_BROWSERS_PATH;
4764
- if (set === "0") return join23(playwrightDir(), ".local-browsers");
5319
+ if (set === "0") return join26(playwrightDir(), ".local-browsers");
4765
5320
  if (set) return set;
4766
- if (process.platform === "darwin") return join23(homedir2(), "Library/Caches/ms-playwright");
4767
- if (process.platform === "win32") return join23(env.LOCALAPPDATA ?? join23(homedir2(), "AppData/Local"), "ms-playwright");
4768
- return join23(env.XDG_CACHE_HOME ?? join23(homedir2(), ".cache"), "ms-playwright");
5321
+ if (process.platform === "darwin") return join26(homedir3(), "Library/Caches/ms-playwright");
5322
+ if (process.platform === "win32") return join26(env.LOCALAPPDATA ?? join26(homedir3(), "AppData/Local"), "ms-playwright");
5323
+ return join26(env.XDG_CACHE_HOME ?? join26(homedir3(), ".cache"), "ms-playwright");
4769
5324
  }
4770
5325
  function chromiumStatus(env = process.env) {
4771
5326
  if (env.YOUI_CHROMIUM) return { available: existsSync4(env.YOUI_CHROMIUM), where: env.YOUI_CHROMIUM };
4772
5327
  try {
4773
- const browsers = JSON.parse(readFileSync2(join23(playwrightDir(), "browsers.json"), "utf8"));
5328
+ const browsers = JSON.parse(readFileSync3(join26(playwrightDir(), "browsers.json"), "utf8"));
4774
5329
  const revision2 = browsers.browsers.find((b) => b.name === "chromium-headless-shell")?.revision;
4775
- const dir = join23(browsersPath(env), `chromium_headless_shell-${revision2}`);
4776
- return { available: revision2 !== void 0 && existsSync4(join23(dir, "INSTALLATION_COMPLETE")), where: dir };
5330
+ const dir = join26(browsersPath(env), `chromium_headless_shell-${revision2}`);
5331
+ return { available: revision2 !== void 0 && existsSync4(join26(dir, "INSTALLATION_COMPLETE")), where: dir };
4777
5332
  } catch {
4778
5333
  return { available: false, where: "playwright-core is not installed" };
4779
5334
  }
4780
5335
  }
4781
5336
  function downloadChromium() {
4782
5337
  return new Promise((resolve16) => {
4783
- const child = spawn2(process.execPath, [join23(playwrightDir(), "cli.js"), "install", "chromium-headless-shell"], { stdio: ["ignore", 2, 2] });
5338
+ const child = spawn2(process.execPath, [join26(playwrightDir(), "cli.js"), "install", "chromium-headless-shell"], { stdio: ["ignore", 2, 2] });
4784
5339
  child.on("error", () => resolve16(false));
4785
5340
  child.on("close", (code) => resolve16(code === 0));
4786
5341
  });
4787
5342
  }
4788
- var dataUri = (file) => file && existsSync4(file) ? `data:image/png;base64,${readFileSync2(file).toString("base64")}` : null;
5343
+ var dataUri = (file) => file && existsSync4(file) ? `data:image/png;base64,${readFileSync3(file).toString("base64")}` : null;
4789
5344
  async function addLooks(model, repoRoot, options = {}) {
4790
5345
  const drawn = [];
4791
5346
  const problems = [];
@@ -4796,9 +5351,9 @@ async function addLooks(model, repoRoot, options = {}) {
4796
5351
  const where2 = section.dir === "." ? "this repo" : section.dir;
4797
5352
  try {
4798
5353
  const installed = (name) => {
4799
- let dir = join23(repoRoot, section.dir);
5354
+ let dir = join26(repoRoot, section.dir);
4800
5355
  while (true) {
4801
- if (existsSync4(join23(dir, "node_modules", name, "package.json"))) return true;
5356
+ if (existsSync4(join26(dir, "node_modules", name, "package.json"))) return true;
4802
5357
  if (dir === repoRoot || dirname12(dir) === dir) return false;
4803
5358
  dir = dirname12(dir);
4804
5359
  }
@@ -4807,13 +5362,13 @@ async function addLooks(model, repoRoot, options = {}) {
4807
5362
  missing.push(where2);
4808
5363
  continue;
4809
5364
  }
4810
- const projectRoot = join23(repoRoot, section.dir);
5365
+ const projectRoot = join26(repoRoot, section.dir);
4811
5366
  const target = await loadInventoryTarget(projectRoot);
4812
5367
  if (findStylesheets(projectRoot, target.files, { tsconfig: target.tsconfig, override: target.specimenCss, templates: target.templates }).found.length === 0) {
4813
5368
  missing.push(where2);
4814
5369
  continue;
4815
5370
  }
4816
- const report = await renderShots(join23(repoRoot, section.dir), names, { download: false, base: "HEAD", ...options.executablePath ? { executablePath: options.executablePath } : {}, ...options.cache ? { cache: options.cache } : {}, ...options.log ? { log: options.log } : {} });
5371
+ const report = await renderShots(join26(repoRoot, section.dir), names, { download: false, base: "HEAD", ...options.executablePath ? { executablePath: options.executablePath } : {}, ...options.cache ? { cache: options.cache } : {}, ...options.log ? { log: options.log } : {} });
4817
5372
  const unavailable = report.notes.some((note) => UNDRAWABLE.test(note));
4818
5373
  if (unavailable) {
4819
5374
  missing.push(where2);
@@ -4881,135 +5436,6 @@ function renderMarkdown(model, reportFile) {
4881
5436
  return lines.join("\n");
4882
5437
  }
4883
5438
 
4884
- // ../cli/src/report/safe.ts
4885
- import { realpathSync as realpathSync2 } from "node:fs";
4886
- import { readFile as readFile9 } from "node:fs/promises";
4887
- import { homedir as homedir3, userInfo as userInfo2 } from "node:os";
4888
- import { join as join24 } from "node:path";
4889
- var REDACTED = "[redacted]";
4890
- var OUTSIDE = "[path outside the repo]";
4891
- var MIN_VALUE = 6;
4892
- var BENIGN_ENV = /^(?:npm_|NPM_|PNPM_|NODE_|NVM_|TERM|COLORTERM|LANG$|LANGUAGE$|LC_|SHELL$|SHLVL$|PWD$|OLDPWD$|_$|PATH$|MANPATH$|INFOPATH$|TMPDIR$|TMP$|TEMP$|EDITOR$|VISUAL$|PAGER$|LESS|LOGNAME$|USER$|USERNAME$|HOME$|XPC_|__CF|COMMAND_MODE$|DISPLAY$|HOSTTYPE$|OSTYPE$|MACHTYPE$|INIT_CWD$|FORCE_COLOR$|NO_COLOR$|CI$|COREPACK_|VSCODE_|ITERM_|TERM_|SECURITYSESSIONID$|LaunchInstanceID$|XDG_|HOMEBREW_|SSH_AUTH_SOCK$|STARSHIP_|ZSH|OMZ|P9K|POWERLEVEL|VITEST|TEST$|MODE$|DEV$|PROD$|SSR$|BASE_URL$)/;
4893
- var PLAIN_WORD = /^[A-Za-z]+$/;
4894
- var PLAIN = /^(?:true|false|yes|no|on|off|null|undefined|\d+(?:\.\d+)?)$/i;
4895
- var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4896
- async function dotenvValues(root) {
4897
- const files = await findFiles(root, [".env", ".env.*", "**/.env", "**/.env.*"]).catch(() => []);
4898
- const values = [];
4899
- for (const file of files) {
4900
- const text = await readFile9(join24(root, file), "utf8").catch(() => "");
4901
- for (const line of text.split(/\r?\n/)) {
4902
- const match = /^\s*(?:export\s+)?[A-Za-z_][A-Za-z0-9_.-]*\s*=\s*(.*)$/.exec(line);
4903
- if (!match) continue;
4904
- const value = match[1].trim().replace(/^(['"`])(.*)\1$/, "$2").replace(/\s+#.*$/, "");
4905
- if (value.length >= MIN_VALUE) values.push(value);
4906
- }
4907
- }
4908
- return values;
4909
- }
4910
- function looksSecret(text) {
4911
- if (/^(?:sk|pk|rk)_(?:live|test)_|^sk-[A-Za-z0-9]|^gh[pousr]_|^github_pat_|^xox[abposr]-|^AKIA[0-9A-Z]{12}|^AIza[0-9A-Za-z_-]{20}|^glpat-|^eyJ[A-Za-z0-9_-]{10,}\./.test(text)) return true;
4912
- if (text.length < 20 || /[\s:[\]]/.test(text)) return false;
4913
- const classes = [/[a-z]/, /[A-Z]/, /\d/].filter((c) => c.test(text)).length;
4914
- if (classes < 2 || !/\d/.test(text)) return false;
4915
- const counts = /* @__PURE__ */ new Map();
4916
- for (const c of text) counts.set(c, (counts.get(c) ?? 0) + 1);
4917
- const entropy = [...counts.values()].reduce((sum, n) => sum - n / text.length * Math.log2(n / text.length), 0);
4918
- return entropy >= 3.5;
4919
- }
4920
- var Redactor = class _Redactor {
4921
- roots;
4922
- home;
4923
- username;
4924
- values;
4925
- includeCode;
4926
- constructor(options) {
4927
- const real = (path) => {
4928
- try {
4929
- return realpathSync2(path);
4930
- } catch {
4931
- return path;
4932
- }
4933
- };
4934
- this.roots = [.../* @__PURE__ */ new Set([options.root, real(options.root)])].sort((a, b) => b.length - a.length);
4935
- this.home = options.home ?? homedir3();
4936
- this.username = options.username ?? (() => {
4937
- try {
4938
- return userInfo2().username;
4939
- } catch {
4940
- return "";
4941
- }
4942
- })();
4943
- const env = Object.entries(options.env ?? process.env).flatMap(([name, value]) => value && !BENIGN_ENV.test(name) && !PLAIN.test(value.trim()) && !(PLAIN_WORD.test(value) && value.length < 12) ? [value] : []);
4944
- this.values = [...new Set([...env, ...options.dotenv ?? []].filter((v) => v.length >= MIN_VALUE && !this.isRoot(v)))].sort((a, b) => b.length - a.length);
4945
- this.includeCode = options.includeCode === true;
4946
- }
4947
- static async forRepo(root, options = {}) {
4948
- return new _Redactor({ ...options, root, dotenv: await dotenvValues(root) });
4949
- }
4950
- isRoot(value) {
4951
- return this.roots.some((root) => value === root || root.startsWith(`${value}/`));
4952
- }
4953
- /** One string, cleaned. */
4954
- text(input) {
4955
- let text = input;
4956
- for (const root of this.roots) text = text.split(`${root}/`).join("").split(root).join(".");
4957
- text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+(?::[^\s/@"'<>]*)?@/gi, `$1${REDACTED}@`);
4958
- for (const value of this.values) text = text.split(value).join(REDACTED);
4959
- text = text.replace(/(^|[\s"'`(=,:;])(?:~\/|\/(?:Users|home|root|var|private|tmp|opt|mnt|media|Volumes|srv|etc|usr|workspace|workspaces|builds|runner|github|nix)\b)[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
4960
- text = text.replace(/(^|[\s"'`(=,:;])[A-Za-z]:\\[^\s"'`<>)]*/g, `$1${OUTSIDE}`);
4961
- if (this.home.length > 1) text = text.split(this.home).join("~");
4962
- if (this.username.length >= 3) text = text.replace(new RegExp(`(?<![A-Za-z0-9])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), REDACTED);
4963
- return text;
4964
- }
4965
- /** A line of code (--include-code): cleaned, and strings that look like keys or tokens blanked. */
4966
- code(input) {
4967
- const assigned = /((?:key|token|secret|passw(?:or)?d|pwd|auth|credential|session|cookie|private|dsn|api[_-]?key)[A-Za-z0-9_]*["']?\s*[:=]\s*)(["'`])([^"'`]{4,})\2/gi;
4968
- const text = input.replace(assigned, `$1$2${REDACTED}$2`).replace(/(["'`])([^"'`\s]{12,})\1/g, (whole, quote, inner) => looksSecret(inner) ? `${quote}${REDACTED}${quote}` : whole).replace(/[A-Za-z0-9_\-+/=.]{20,}/g, (token) => looksSecret(token) ? REDACTED : token);
4969
- return this.text(text);
4970
- }
4971
- /** The whole model, every string cleaned; code only when it was asked for. */
4972
- model(value) {
4973
- const walk = (node, key2) => {
4974
- if (typeof node === "string") return key2 === "code" ? this.code(node) : this.text(node);
4975
- if (Array.isArray(node)) return node.map((item) => walk(key2 === "looks" && !this.includeCode && typeof item === "string" ? publicLook(item) : item, null));
4976
- if (node && typeof node === "object") {
4977
- const out = {};
4978
- for (const [k, v] of Object.entries(node)) {
4979
- if (k === "code" && !this.includeCode) continue;
4980
- out[k] = k === "light" || k === "dark" ? v : walk(v, k);
4981
- }
4982
- return out;
4983
- }
4984
- return node;
4985
- };
4986
- return walk(value, null);
4987
- }
4988
- /** What sensitive text is still in `output`: the names of the kinds found, never the values. */
4989
- leaks(output) {
4990
- const found = [];
4991
- if (this.roots.some((root) => output.includes(root))) found.push("an absolute path to the repo");
4992
- if (this.home.length > 1 && output.includes(this.home)) found.push("the home directory");
4993
- if (this.username.length >= 3 && new RegExp(`[/\\\\~]${escapeRegExp(this.username)}(?![A-Za-z0-9])`).test(output)) found.push("the username in a path");
4994
- if (this.values.some((v) => output.includes(v))) found.push("an environment or .env value");
4995
- if (/\b[a-z][a-z0-9+.-]*:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@/i.test(output)) found.push("a URL with credentials");
4996
- return found;
4997
- }
4998
- /** Last pass over a written output: clean it, then refuse it if anything sensitive survived. */
4999
- scrub(output) {
5000
- let text = output;
5001
- if (this.leaks(text).length === 0) return text;
5002
- for (const root of this.roots) text = text.split(`${root}/`).join("").split(root).join(".");
5003
- if (this.home.length > 1) text = text.split(this.home).join("~");
5004
- for (const value of this.values) text = text.split(value).join(REDACTED);
5005
- text = text.replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@:"'<>]+:[^\s/@"'<>]+@/gi, `$1${REDACTED}@`);
5006
- if (this.username.length >= 3) text = text.replace(new RegExp(`([/\\\\~])${escapeRegExp(this.username)}(?![A-Za-z0-9])`, "g"), `$1${REDACTED}`);
5007
- const left = this.leaks(text);
5008
- if (left.length > 0) throw new Error(`the report still held ${left.join(", ")} after redaction, so nothing was written`);
5009
- return text;
5010
- }
5011
- };
5012
-
5013
5439
  // ../cli/src/report/run.ts
5014
5440
  var REPORT_FILE = "youiyoux-report.html";
5015
5441
  var CARD_FILE = "youiyoux-card.png";
@@ -5070,7 +5496,7 @@ async function runReport(options = {}, io = {}) {
5070
5496
  const clean = redactor.model(model);
5071
5497
  const outDir = resolve14(options.out ?? root);
5072
5498
  await mkdir5(outDir, { recursive: true });
5073
- const files = { report: join25(outDir, REPORT_FILE), card: join25(outDir, CARD_FILE), summary: join25(outDir, SUMMARY_FILE) };
5499
+ const files = { report: join27(outDir, REPORT_FILE), card: join27(outDir, CARD_FILE), summary: join27(outDir, SUMMARY_FILE) };
5074
5500
  const t = clean.totals;
5075
5501
  const card = {
5076
5502
  repo: redactor.scrub(clean.repo),
@@ -5190,14 +5616,27 @@ ${style.bold("In a product repo")}
5190
5616
  a marked rule in AGENTS.md and CLAUDE.md, and the youiyoux MCP server for
5191
5617
  Claude Code (.mcp.json) and Codex (.codex/config.toml); default both.
5192
5618
  Idempotent; remove takes out only youi's parts. No youi.project.json needed
5193
- hooks install [--claude] [--git] [--ci [--diff]] [--strict]
5619
+ hooks install [--claude] [--git] [--ci [--diff]] [--record] [--strict]
5194
5620
  live checks, existing config kept (default: --claude --git):
5195
5621
  Claude Code PostToolUse (advisory context after each edit: violations
5196
5622
  plus the drift the edit adds, each with its fix),
5197
5623
  git pre-commit (advisory on staged files and their drift; --strict blocks
5198
5624
  new errors and drift; Codex has no post-edit hook, so this and AGENTS.md
5199
5625
  cover it), CI workflow (authoritative, pinned rules version, PR
5200
- annotations; --diff also fails pull requests on drift they add)
5626
+ annotations; --diff also fails pull requests on drift they add);
5627
+ --record: a workflow that sends youiyoux record to the team's hub on each
5628
+ push to the default branch
5629
+ record [--send [--hub url]] [--repo owner/name] [--out file] [--json]
5630
+ this repository's record for the team's hosted hub (youi.record/v1): each
5631
+ product's status, redacted as the shareable report is, no code; --send
5632
+ posts it to YOUIYOUX_HUB_URL, proved by GitHub Actions' OIDC token
5633
+ (id-token: write), else with YOUIYOUX_HUB_TOKEN (environment only)
5634
+ hub serve [--port n] [--host h] [--db file]
5635
+ the team's hosted hub, read-only: CI sends records, people sign in with
5636
+ YOUIYOUX_HUB_VIEW_KEY and see every repository and each System's adoption
5637
+ by version. YOUIYOUX_HUB_GITHUB_OWNERS and YOUIYOUX_HUB_AUDIENCE: GitHub
5638
+ Actions proves each record's repository (else YOUIYOUX_HUB_TOKEN, shared).
5639
+ Records are kept in Postgres (DATABASE_URL) or a SQLite file
5201
5640
 
5202
5641
  ${style.bold("System")} ${style.dim("approved elements: build only with these")}
5203
5642
  show <component> the contract: import, legal prop values, guidance, examples
@@ -5342,6 +5781,12 @@ var OPTIONS = {
5342
5781
  package: { type: "string" },
5343
5782
  fork: { type: "boolean", default: false },
5344
5783
  to: { type: "string" },
5784
+ repo: { type: "string" },
5785
+ send: { type: "boolean", default: false },
5786
+ hub: { type: "string" },
5787
+ host: { type: "string" },
5788
+ db: { type: "string" },
5789
+ record: { type: "boolean", default: false },
5345
5790
  cache: { type: "string" },
5346
5791
  role: { type: "string" },
5347
5792
  license: { type: "string" },
@@ -5362,7 +5807,7 @@ var OPTIONS = {
5362
5807
  var INVENTORY_OPTIONS = { ...OPTIONS, project: { type: "string", multiple: true }, include: { type: "string", multiple: true }, exclude: { type: "string", multiple: true } };
5363
5808
  var UsageError = class extends Error {
5364
5809
  };
5365
- var run6 = promisify5(execFile5);
5810
+ var run7 = promisify6(execFile6);
5366
5811
  var print = (text) => void process.stdout.write(`${text}
5367
5812
  `);
5368
5813
  var json3 = (value) => print(JSON.stringify(value, null, 2));
@@ -5373,13 +5818,13 @@ async function project(values) {
5373
5818
  return loadProject(values.project ?? process.cwd(), loadOptions(values));
5374
5819
  }
5375
5820
  async function readJsonOrFile(value) {
5376
- const text = value.trim().startsWith("{") ? value : await readFile10(value, "utf8");
5821
+ const text = value.trim().startsWith("{") ? value : await readFile11(value, "utf8");
5377
5822
  return JSON.parse(text);
5378
5823
  }
5379
5824
  async function cmdScan(values) {
5380
5825
  const p = await project(values);
5381
5826
  const { report } = await scanProject(p);
5382
- const out = values.out ?? join26(p.root, ".youi/usage.json");
5827
+ const out = values.out ?? join28(p.root, ".youi/usage.json");
5383
5828
  await writeFileAtomic(resolve15(out), `${JSON.stringify(report)}
5384
5829
  `);
5385
5830
  const recorded = await recordUsage(values, p, report, resolve15(out));
@@ -5406,7 +5851,7 @@ function indexOf(values) {
5406
5851
  }
5407
5852
  async function cmdGaps(values) {
5408
5853
  const dirs = inventoryDirs(values);
5409
- const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await readFile10(resolve15(values.inventory), "utf8"))) : await scanInventory(values, dirs);
5854
+ const inventory = values.inventory ? InventoryReport.parse(JSON.parse(await readFile11(resolve15(values.inventory), "utf8"))) : await scanInventory(values, dirs);
5410
5855
  const cwd = values.inventory ? dirname13(resolve15(values.inventory)) : process.cwd();
5411
5856
  const project2 = await loadProject(resolve15(dirs[0]), loadOptions(values)).catch(() => null);
5412
5857
  const system = project2 ? { name: project2.system.config.name, components: project2.system.components.map((c) => c.contract.name) } : null;
@@ -5426,7 +5871,7 @@ async function cmdInventory(values) {
5426
5871
  if (values.history) return cmdHistory(values, []);
5427
5872
  const dirs = inventoryDirs(values);
5428
5873
  const report = await scanInventory(values, dirs);
5429
- const out = resolve15(values.out ?? join26(dirs[0], ".youi/inventory.json"));
5874
+ const out = resolve15(values.out ?? join28(dirs[0], ".youi/inventory.json"));
5430
5875
  await writeFileAtomic(out, `${JSON.stringify(report, null, 2)}
5431
5876
  `);
5432
5877
  const recorded = await recordScan(values, report, out);
@@ -5456,7 +5901,7 @@ async function cmdIndex(values, positionals) {
5456
5901
  const [action, ...rest] = positionals;
5457
5902
  const { path, home } = indexOf(values);
5458
5903
  if (action === "path") {
5459
- if (values.json) json3({ store: storeLocation({ path }), sources: join26(home, SOURCES_FILE), scans: join26(home, SCANS_LOG) });
5904
+ if (values.json) json3({ store: storeLocation({ path }), sources: join28(home, SOURCES_FILE), scans: join28(home, SCANS_LOG) });
5460
5905
  else print(storeLocation({ path }));
5461
5906
  return 0;
5462
5907
  }
@@ -5513,7 +5958,7 @@ function baselineFile(values, p) {
5513
5958
  var checkScope = (values, positionals) => ({ ...positionals.length > 0 ? { files: positionals } : {}, ...values.rule ? { rule: values.rule } : {}, quiet: values.quiet });
5514
5959
  async function repoPrefix(root) {
5515
5960
  try {
5516
- return (await run6("git", ["rev-parse", "--show-prefix"], { cwd: root })).stdout.trim();
5961
+ return (await run7("git", ["rev-parse", "--show-prefix"], { cwd: root })).stdout.trim();
5517
5962
  } catch {
5518
5963
  return "";
5519
5964
  }
@@ -5585,7 +6030,7 @@ async function cmdPlan(values, positionals) {
5585
6030
  async function cmdRules(values) {
5586
6031
  const p = await project(values);
5587
6032
  if (values.preview) {
5588
- const draft = (0, import_yaml4.parse)(await readFile10(values.preview, "utf8"));
6033
+ const draft = (0, import_yaml4.parse)(await readFile11(values.preview, "utf8"));
5589
6034
  const evaluation = evaluateRule(p, await scanProject(p), draft);
5590
6035
  if (values.json) json3(evaluation);
5591
6036
  else print(renderRulePreview(evaluation));
@@ -5599,7 +6044,7 @@ async function cmdRules(values) {
5599
6044
  async function cmdMigrate(values, positionals) {
5600
6045
  const file = positionals[0];
5601
6046
  if (!file) throw new UsageError("youiyoux migrate <migration.yaml> [--write] [--diff]");
5602
- const spec = MigrationSpec.parse((0, import_yaml4.parse)(await readFile10(file, "utf8")));
6047
+ const spec = MigrationSpec.parse((0, import_yaml4.parse)(await readFile11(file, "utf8")));
5603
6048
  const result = await runMigration(await project(values), spec, { write: values.write });
5604
6049
  if (values.json) json3(result);
5605
6050
  else print(renderMigration(result, values.diff || !values.write));
@@ -5639,11 +6084,56 @@ async function cmdUpgrade(values) {
5639
6084
  else print([renderUpgrade(result, values.diff || result.status === "preview"), ...p.linked ? [style.warn(`! linked: the design system is your checkout ${p.linked}, not the config's source (youiyoux unlink to undo)`)] : []].join("\n"));
5640
6085
  return result.status === "blocked" ? 1 : 0;
5641
6086
  }
6087
+ async function cmdRecord(values) {
6088
+ const hub = values.hub ?? process.env[HUB_URL_ENV];
6089
+ const token = process.env[HUB_TOKEN_ENV] || null;
6090
+ if (values.send && !hub) throw new UsageError(`youiyoux record --send needs the hub's address: set ${HUB_URL_ENV} (or pass --hub <url>)`);
6091
+ const github = values.send ? await requestGithubToken(audienceOf(hub)) : null;
6092
+ if (values.send && !token && !github) throw new UsageError(`youiyoux record --send needs a way to prove where the record comes from: in GitHub Actions, permissions: id-token: write; elsewhere, the hub's ingest token in ${HUB_TOKEN_ENV} (from the environment only, never a flag)`);
6093
+ const record = await buildRecord(values.project ?? process.cwd(), values.repo ? { repo: values.repo } : {});
6094
+ if (values.out) await writeFileAtomic(resolve15(values.out), `${JSON.stringify(record, null, 2)}
6095
+ `);
6096
+ const sentTo = values.send ? await sendRecord(record, hub, { token, github }) : null;
6097
+ if (values.json) json3(record);
6098
+ else print([renderRecord(record, sentTo), ...values.out ? [style.dim(` written to ${values.out}`)] : [], ...!values.send && !values.out ? [style.dim(` youiyoux record --send posts it to the hub in ${HUB_URL_ENV}; --out <file> writes it; --json prints it`)] : []].join("\n"));
6099
+ return 0;
6100
+ }
6101
+ function builtHub() {
6102
+ for (const dir of [join28(TOOL_ROOT, "hub"), join28(TOOL_ROOT, "apps/web/dist")]) if (existsSync5(join28(dir, "index.html"))) return dir;
6103
+ return null;
6104
+ }
6105
+ async function cmdHubServe(values, positionals) {
6106
+ if (positionals[0] !== "serve" || positionals.length > 1) throw new UsageError("youiyoux hub serve [--port n] [--host h] [--db file] (the team's hosted hub: read-only, fed by youiyoux record in CI)");
6107
+ const owners = (process.env[GITHUB_OWNERS_ENV] ?? "").split(",").map((o) => o.trim()).filter(Boolean);
6108
+ const audience = process.env[AUDIENCE_ENV];
6109
+ if (owners.length > 0 && !audience) throw new UsageError(`${GITHUB_OWNERS_ENV} needs ${AUDIENCE_ENV}: the hub's address, exactly as CI's ${HUB_URL_ENV} names it (GitHub's token is issued for it)`);
6110
+ const branches = (process.env.YOUIYOUX_HUB_BRANCHES ?? "").split(",").map((b) => b.trim()).filter(Boolean);
6111
+ const github = owners.length > 0 ? githubVerifier({ owners, audience, branches }) : void 0;
6112
+ const problem = secretsProblem(github ? void 0 : process.env[HUB_TOKEN_ENV], process.env[VIEW_KEY_ENV], Boolean(github));
6113
+ if (problem) throw new UsageError(problem);
6114
+ const port = Number(values.port ?? process.env.PORT ?? 8080);
6115
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new UsageError(`--port ${JSON.stringify(values.port ?? process.env.PORT)}: expected a port number`);
6116
+ const { openStore, storeLocation: where2 } = await import("./src-624TVB6T.js");
6117
+ const path = resolve15(values.db ?? process.env.YOUI_HUB_DB ?? join28(process.env.XDG_DATA_HOME || join28(homedir4(), ".local/share"), "youiyoux/hub.db"));
6118
+ const store = await openStore({ path });
6119
+ const dist = builtHub();
6120
+ const hub = await startOrgHub({ store, ...github ? { github } : { ingestToken: process.env[HUB_TOKEN_ENV] }, viewKey: process.env[VIEW_KEY_ENV], dist, host: values.host ?? "127.0.0.1", port });
6121
+ const senders = github ? `CI sends records from GitHub Actions in ${owners.join(", ")}, on ${(branches.length > 0 ? branches : DEFAULT_BRANCHES).join(" or ")}, each proving its repository (OIDC, for ${audienceOf(audience)})${process.env[HUB_TOKEN_ENV] ? `; ${HUB_TOKEN_ENV} is not used` : ""}` : `CI sends records with ${HUB_TOKEN_ENV}, which can name any repository (${GITHUB_OWNERS_ENV} makes GitHub Actions prove each one's)`;
6122
+ print([`${style.ok("youiyoux hub")} ${hub.url} ${style.dim(`read-only \xB7 records kept in ${where2({ path })}`)}`, ...dist ? [] : [style.warn("! the hub's page is not built here (pnpm --filter web build): the API answers at /api/org")], style.dim(` ${senders}; people sign in with ${VIEW_KEY_ENV}. Put it behind https; --host 0.0.0.0 to listen beyond this machine`)].join("\n"));
6123
+ await new Promise((done) => {
6124
+ const stop = () => done();
6125
+ process.once("SIGINT", stop);
6126
+ process.once("SIGTERM", stop);
6127
+ });
6128
+ await hub.close();
6129
+ await store.close();
6130
+ return 0;
6131
+ }
5642
6132
  async function cmdInit(values) {
5643
6133
  if (!values.design) throw new UsageError("youiyoux init --design <dir|package> (a design repo checkout, the directory holding youi.config.json, or the npm package that holds it)");
5644
6134
  const root = values.project ?? process.cwd();
5645
6135
  const design = await designArg(values.design, root);
5646
- const result = await initProject({ root, ...design, schemaDir: join26(TOOL_ROOT, "schema"), ...values.system ? { system: values.system } : {}, ...values.components ? { componentsDir: values.components } : {}, force: values.force });
6136
+ const result = await initProject({ root, ...design, schemaDir: join28(TOOL_ROOT, "schema"), ...values.system ? { system: values.system } : {}, ...values.components ? { componentsDir: values.components } : {}, force: values.force });
5647
6137
  if (!values.json) {
5648
6138
  print(`${style.ok("created")} ${result.file}`);
5649
6139
  for (const note of result.notes) print(style.dim(` \xB7 ${note}`));
@@ -5654,27 +6144,27 @@ async function cmdInit(values) {
5654
6144
  }
5655
6145
  async function cmdAdopt(values) {
5656
6146
  const root = resolve15(values.project ?? process.cwd());
5657
- if (await exists(join26(root, PROJECT_FILE))) throw new Error(`${root} already has ${PROJECT_FILE}: it follows a design system already (youiyoux sync updates it)`);
6147
+ if (await exists(join28(root, PROJECT_FILE))) throw new Error(`${root} already has ${PROJECT_FILE}: it follows a design system already (youiyoux sync updates it)`);
5658
6148
  const plan = await planAdoption({ root, ...values.components ? { componentsDir: values.components } : {}, ...values.css ? { css: values.css } : {}, ...values.system ? { system: values.system } : {}, fork: values.fork });
5659
- const taken = (await Promise.all(plan.files.map(async (f) => await exists(join26(root, f.path)) ? f.path : null))).filter((f) => f !== null);
6149
+ const taken = (await Promise.all(plan.files.map(async (f) => await exists(join28(root, f.path)) ? f.path : null))).filter((f) => f !== null);
5660
6150
  if (taken.length > 0) throw new Error(`nothing was written: ${taken.slice(0, 5).join(", ")}${taken.length > 5 ? ` and ${taken.length - 5} more` : ""} already exist${taken.length === 1 ? "s" : ""}`);
5661
6151
  if (values["dry-run"]) {
5662
6152
  if (values.json) json3(plan);
5663
6153
  else print([...adoptLines(plan, true), "", style.dim(`dry run: nothing written; would write ${plan.files.map((f) => f.path).join(", ")}${plan.stylesheet ? ` and change ${plan.stylesheet.path}` : ""}`)].join("\n"));
5664
6154
  return 0;
5665
6155
  }
5666
- const stylesheet = plan.stylesheet ? await readFile10(join26(root, plan.stylesheet.path), "utf8") : null;
6156
+ const stylesheet = plan.stylesheet ? await readFile11(join28(root, plan.stylesheet.path), "utf8") : null;
5667
6157
  try {
5668
- for (const file of plan.files) await writeFileAtomic(join26(root, file.path), file.content);
5669
- if (plan.stylesheet) await writeFileAtomic(join26(root, plan.stylesheet.path), plan.stylesheet.content);
5670
- await initProject({ root, design: root, schemaDir: join26(TOOL_ROOT, "schema"), system: plan.system.id, componentsDir: plan.componentsDir, outputs: { ...plan.outputs, targets: [...plan.outputs.targets], dark: [...plan.outputs.dark] }, tokenSources: plan.tokenSources });
6158
+ for (const file of plan.files) await writeFileAtomic(join28(root, file.path), file.content);
6159
+ if (plan.stylesheet) await writeFileAtomic(join28(root, plan.stylesheet.path), plan.stylesheet.content);
6160
+ await initProject({ root, design: root, schemaDir: join28(TOOL_ROOT, "schema"), system: plan.system.id, componentsDir: plan.componentsDir, outputs: { ...plan.outputs, targets: [...plan.outputs.targets], dark: [...plan.outputs.dark] }, tokenSources: plan.tokenSources });
5671
6161
  await syncProject(await loadProject(root), { dryRun: true });
5672
6162
  } catch (error) {
5673
- for (const file of [...plan.files.map((f) => f.path), PROJECT_FILE]) await rm7(join26(root, file), { force: true });
5674
- for (const folder of [...new Set(plan.files.map((f) => dirname13(f.path)))].sort((a, b) => b.length - a.length)) await rmdir3(join26(root, folder)).catch(() => void 0);
5675
- await rmdir3(join26(root, "design/components")).catch(() => void 0);
5676
- await rmdir3(join26(root, "design")).catch(() => void 0);
5677
- if (plan.stylesheet && stylesheet !== null) await writeFileAtomic(join26(root, plan.stylesheet.path), stylesheet);
6163
+ for (const file of [...plan.files.map((f) => f.path), PROJECT_FILE]) await rm7(join28(root, file), { force: true });
6164
+ for (const folder of [...new Set(plan.files.map((f) => dirname13(f.path)))].sort((a, b) => b.length - a.length)) await rmdir3(join28(root, folder)).catch(() => void 0);
6165
+ await rmdir3(join28(root, "design/components")).catch(() => void 0);
6166
+ await rmdir3(join28(root, "design")).catch(() => void 0);
6167
+ if (plan.stylesheet && stylesheet !== null) await writeFileAtomic(join28(root, plan.stylesheet.path), stylesheet);
5678
6168
  throw new Error(`adopt was undone, nothing kept: ${error instanceof Error ? error.message : String(error)}`);
5679
6169
  }
5680
6170
  if (!values.json) print([...adoptLines(plan), ""].join("\n"));
@@ -5724,8 +6214,8 @@ async function cmdSystem(values, positionals) {
5724
6214
  }
5725
6215
  print([...lines, ""].join("\n"));
5726
6216
  const synced = await cmdSync({ ...values, project: p.root, package: void 0 });
5727
- const git3 = `push it and set source to { "git": "<its url>", "ref": "v${result.system.version}" }`;
5728
- print(`Next: make ${relative11(process.cwd(), result.target) || "."} a repository of its own and commit it. For each app to follow it, ${values.package ? `publish ${values.package}, add it as a dependency and set source to { "package": "${values.package}" }, or ${git3}` : `${git3}, or publish it as an npm package (--package names it)`}. Change components there from now on: youiyoux sync merges them into each product`);
6217
+ const git4 = `push it and set source to { "git": "<its url>", "ref": "v${result.system.version}" }`;
6218
+ print(`Next: make ${relative11(process.cwd(), result.target) || "."} a repository of its own and commit it. For each app to follow it, ${values.package ? `publish ${values.package}, add it as a dependency and set source to { "package": "${values.package}" }, or ${git4}` : `${git4}, or publish it as an npm package (--package names it)`}. Change components there from now on: youiyoux sync merges them into each product`);
5729
6219
  return synced;
5730
6220
  }
5731
6221
  async function cmdLink(values, positionals) {
@@ -5748,7 +6238,7 @@ async function cmdUnlink(values) {
5748
6238
  }
5749
6239
  async function designArg(value, root) {
5750
6240
  if (existsSync5(resolve15(value)) || !PACKAGE_NAME.test(value)) return { design: value };
5751
- return { design: await designRootOf({ package: value }, join26(resolve15(root), PROJECT_FILE), root), package: value };
6241
+ return { design: await designRootOf({ package: value }, join28(resolve15(root), PROJECT_FILE), root), package: value };
5752
6242
  }
5753
6243
  async function cmdBuild(values) {
5754
6244
  const root = resolve15(values.design ?? process.cwd());
@@ -5757,10 +6247,10 @@ async function cmdBuild(values) {
5757
6247
  const output = OutputConfig.parse({ cssVarPrefix: values.prefix ?? "youi", targets: ["css-vars", "tailwind-v4", "ts", "react-native"] });
5758
6248
  const started = performance.now();
5759
6249
  const built = await buildTokens(root, system.config, output, { contracts: system.components.map((c) => c.contract) });
5760
- const out = resolve15(values.out ?? join26(root, "build", system.config.id));
5761
- const boundary = values.out ? out : join26(root, "build");
5762
- if (!within(boundary, out) || built.files.some((file) => !within(out, join26(out, file.path)))) throw new Error(`Cannot build system ${JSON.stringify(system.config.id)}: an output path leaves the output directory.`);
5763
- for (const file of built.files) await writeFileAtomic(join26(out, file.path), file.content);
6250
+ const out = resolve15(values.out ?? join28(root, "build", system.config.id));
6251
+ const boundary = values.out ? out : join28(root, "build");
6252
+ if (!within(boundary, out) || built.files.some((file) => !within(out, join28(out, file.path)))) throw new Error(`Cannot build system ${JSON.stringify(system.config.id)}: an output path leaves the output directory.`);
6253
+ for (const file of built.files) await writeFileAtomic(join28(out, file.path), file.content);
5764
6254
  if (values.json) json3({ out, files: built.files.map((f) => f.path), tokens: built.tokens.length });
5765
6255
  else print(`${style.ok("built")} ${plural(built.tokens.length, "token")} \u2192 ${built.files.map((f) => f.path).join(", ")} ${style.dim(`in ${out} (${ms(performance.now() - started)})`)}`);
5766
6256
  return 0;
@@ -5772,7 +6262,7 @@ async function cmdParity(values) {
5772
6262
  const prefix = values.prefix ?? "youi";
5773
6263
  const dark = { selector: values.dark ?? ':root[data-theme="dark"]' };
5774
6264
  const built = await buildTokens(root, system.config, OutputConfig.parse({ cssVarPrefix: prefix, dark: [dark], targets: ["css-vars"] }));
5775
- const report = compareParity(await readFile10(values.css, "utf8"), { light: { selector: values.light ?? ":root" }, dark }, built.files[0].content, { light: { selector: ":root" }, dark }, prefix);
6265
+ const report = compareParity(await readFile11(values.css, "utf8"), { light: { selector: values.light ?? ":root" }, dark }, built.files[0].content, { light: { selector: ":root" }, dark }, prefix);
5776
6266
  if (values.json) json3(report);
5777
6267
  else {
5778
6268
  const ok = report.different.length === 0 && report.missing.length === 0;
@@ -5785,16 +6275,16 @@ async function cmdParity(values) {
5785
6275
  async function cmdSchema(values) {
5786
6276
  const schemas = renderJsonSchemas();
5787
6277
  if (values.check) {
5788
- const dir = join26(values.design ? resolve15(values.design) : TOOL_ROOT, "schema");
6278
+ const dir = join28(values.design ? resolve15(values.design) : TOOL_ROOT, "schema");
5789
6279
  const stale = [];
5790
- for (const { file, content } of schemas) if (await readFile10(join26(dir, file), "utf8").catch(() => null) !== content) stale.push(file);
6280
+ for (const { file, content } of schemas) if (await readFile11(join28(dir, file), "utf8").catch(() => null) !== content) stale.push(file);
5791
6281
  if (values.json) json3({ dir, stale });
5792
6282
  else print(stale.length === 0 ? `${style.ok("\u2713")} ${dir} matches this youi's schemas` : `${style.error("\u2716")} out of date in ${dir}: ${stale.join(", ")}; run youiyoux schema --write${values.design ? " --design <dir>" : ""}`);
5793
6283
  return stale.length === 0 ? 0 : 1;
5794
6284
  }
5795
6285
  if (values.write) {
5796
6286
  const root = values.design ? resolve15(values.design) : TOOL_ROOT;
5797
- for (const { file, content } of schemas) await writeFileAtomic(join26(root, "schema", file), content);
6287
+ for (const { file, content } of schemas) await writeFileAtomic(join28(root, "schema", file), content);
5798
6288
  print(`${style.ok("wrote")} ${schemas.map((s) => s.file).join(", ")}`);
5799
6289
  } else json3(Object.fromEntries(schemas.map((s) => [s.file, JSON.parse(s.content)])));
5800
6290
  return 0;
@@ -5814,16 +6304,24 @@ async function cmdHooks(values, positionals) {
5814
6304
  process.stdout.write(outcome.stdout);
5815
6305
  return outcome.exitCode;
5816
6306
  }
5817
- if (action !== "install") throw new UsageError("youiyoux hooks install [--claude] [--git] [--ci [--diff]] [--strict] or youiyoux hooks run claude|pre-commit");
6307
+ if (action !== "install") throw new UsageError("youiyoux hooks install [--claude] [--git] [--ci [--diff]] [--record] [--strict] or youiyoux hooks run claude|pre-commit");
6308
+ if (values.record && !values.claude && !values.git && !values.ci) {
6309
+ const result = { kind: "record", ...await installRecordWorkflow(values.project ?? process.cwd(), toolVersion(), values.force) };
6310
+ if (values.json) json3([result]);
6311
+ else print(`${result.action === "unchanged" ? style.dim(result.action) : style.ok(result.action)} ${result.kind}: ${result.file}
6312
+ ${style.dim(result.guarantee)}`);
6313
+ return 0;
6314
+ }
5818
6315
  const p = await loadCheckProject(values.project ?? process.cwd(), loadOptions(values), !values.project);
5819
6316
  if (values.ci && !configured(p)) throw new UsageError("Run youiyoux init --design <dir> before installing the pinned CI workflow; repo-mode CI can run youiyoux check --diff <base> --strict directly");
5820
- const target = { ...hookTarget(values), project: p.root, ...configured(p) && p.configFile && p.configFile !== join26(p.root, PROJECT_FILE) ? { config: p.configFile } : {}, ...values.root !== void 0 ? { root: p.root } : {} };
6317
+ const target = { ...hookTarget(values), project: p.root, ...configured(p) && p.configFile && p.configFile !== join28(p.root, PROJECT_FILE) ? { config: p.configFile } : {}, ...values.root !== void 0 ? { root: p.root } : {} };
5821
6318
  const all = !values.claude && !values.git && !values.ci;
5822
6319
  const results = [];
5823
6320
  if (values.claude || all) results.push({ kind: "Claude Code", ...await installClaudeHook(await repoRootOf(p.root), hookCommand(TOOL_ROOT, "claude", target)) });
5824
6321
  if (values.git || all) results.push({ kind: "git pre-commit", ...await installPreCommit(p.root, hookCommand(TOOL_ROOT, "pre-commit", target, values.strict ? ["--strict"] : []), values.strict) });
5825
6322
  if (values.git || all) results.push({ kind: "Codex", ...configured(p) ? codexFallback(p.root, p.config.agents.agentsMd) : await installRepoAgentInstructions(p.root) });
5826
6323
  if (values.ci && configured(p)) results.push({ kind: "CI", ...await installWorkflow(p, TOOL_ROOT, target, values.force, { diff: values.diff }) });
6324
+ if (values.record) results.push({ kind: "record", ...await installRecordWorkflow(p.root, toolVersion(), values.force) });
5827
6325
  if (values.json) json3(results);
5828
6326
  else for (const r of results) print(`${r.action === "unchanged" || r.action === "skipped" ? style.dim(r.action) : style.ok(r.action)} ${r.kind}: ${r.file}
5829
6327
  ${style.dim(r.guarantee)}`);
@@ -5836,7 +6334,7 @@ async function cmdAgents(values, positionals) {
5836
6334
  const repoRoot = await repoRootOf(projectRoot);
5837
6335
  const target = { project: projectRoot, ...values.config ? { config: resolve15(values.config) } : {}, ...values.design ? { design: resolve15(values.design) } : {}, ...values.root !== void 0 ? { root: resolve15(values.root || ".") } : {} };
5838
6336
  const server = mcpCommand(TOOL_ROOT, target);
5839
- const system = existsSync5(resolve15(values.config ?? join26(projectRoot, PROJECT_FILE))) ? await project(values) : null;
6337
+ const system = existsSync5(resolve15(values.config ?? join28(projectRoot, PROJECT_FILE))) ? await project(values) : null;
5840
6338
  const claudeServer = system && system.root === repoRoot ? { command: "node", args: mcpArgs(system) } : relativeTo(repoRoot, server);
5841
6339
  const both = !values.claude && !values.codex;
5842
6340
  const results = [];
@@ -5944,7 +6442,7 @@ async function cmdUi(values) {
5944
6442
  async function cmdDetect(values, positionals) {
5945
6443
  const target = detectTarget(positionals);
5946
6444
  if (!target) throw new UsageError("youiyoux detect <repo-path> or youiyoux detect <url\u2026> [--expose] (one repo, or one or more URLs; not both)");
5947
- const { repoReport, siteReport, writeReport } = await import("./src-ZG7JU6YJ.js");
6445
+ const { repoReport, siteReport, writeReport } = await import("./src-PBM76LFQ.js");
5948
6446
  const out = resolve15(values.out ?? ".youi/detect");
5949
6447
  const report = target.kind === "repo" ? await repoReport(target.path) : await siteReport(target.urls, { out, expose: values.expose, ...process.env.YOUI_CHROMIUM ? { executablePath: process.env.YOUI_CHROMIUM } : {} });
5950
6448
  const file = await writeReport(report, out);
@@ -5978,7 +6476,7 @@ async function cmdRequests(values, positionals) {
5978
6476
  const usage = "youiyoux requests [--status open|addressed|closed|all] [--route r] or youiyoux requests show <id> | reply <id> --message <text> | addressed <id> --message <what you changed>";
5979
6477
  const [action, id, ...rest] = positionals;
5980
6478
  if (rest.length > 0) throw new UsageError(usage);
5981
- const { RequestStore, requestBrief } = await import("./src-6SNQVIZS.js");
6479
+ const { RequestStore, requestBrief } = await import("./src-R7JGKOYI.js");
5982
6480
  const { project: project2, root } = await product(values);
5983
6481
  const store = new RequestStore(root, project2?.config.id ?? basename5(root));
5984
6482
  const agent = { kind: "agent", name: "youiyoux CLI" };
@@ -6011,8 +6509,8 @@ async function cmdScreens(values, positionals, io) {
6011
6509
  if (!["capture", "login"].includes(positionals[0] ?? "") || positionals.length > 1 || !values.base) throw new UsageError(usage);
6012
6510
  const { root } = await product(values);
6013
6511
  if (positionals[0] === "login") return screensLogin(values.base, values["storage-state"] ? resolve15(values["storage-state"]) : sessionFile(root), io);
6014
- const dir = resolve15(values.out ?? join26(root, ".youi/screens"));
6015
- if (!existsSync5(join26(dir, "screens.json"))) {
6512
+ const dir = resolve15(values.out ?? join28(root, ".youi/screens"));
6513
+ if (!existsSync5(join28(dir, "screens.json"))) {
6016
6514
  process.stderr.write(`${style.error("\u2716")} no screens.json in ${dir}: list the routes to capture there, as {"schema":"youiyoux.screens/v1","project":"<name>","source":"<what the app is>","capturedAt":"","viewport":{"width":1440,"height":900},"screens":[{"route":"/","path":"/"}]}
6017
6515
  `);
6018
6516
  return 1;
@@ -6020,7 +6518,7 @@ async function cmdScreens(values, positionals, io) {
6020
6518
  const session = chosenSession(root, values["storage-state"] ? resolve15(values["storage-state"]) : void 0);
6021
6519
  if (session?.saved) process.stderr.write(`${style.dim(`signed in with the session youiyoux screens login saved (${session.file}); delete it to capture signed out`)}
6022
6520
  `);
6023
- const { launchBrowser } = await import("./src-ZG7JU6YJ.js");
6521
+ const { launchBrowser } = await import("./src-PBM76LFQ.js");
6024
6522
  const browser = await launchBrowser(process.env.YOUI_CHROMIUM, { purpose: { what: "youiyoux screens capture", command: "youiyoux screens capture" } });
6025
6523
  try {
6026
6524
  const report = await captureScreens(dir, { base: values.base, root, browser, ...values.route ? { routes: [values.route] } : {}, ...session ? { storageState: session.file } : {}, log: (line) => process.stderr.write(`${style.dim(line)}
@@ -6077,7 +6575,7 @@ async function cmdReport(values, positionals, io) {
6077
6575
  return result.code;
6078
6576
  }
6079
6577
  async function cmdMcp(values) {
6080
- const { startStdioServer } = await import("./src-75ODGHE3.js");
6578
+ const { startStdioServer } = await import("./src-HVYMF4S2.js");
6081
6579
  await startStdioServer({ projectRoot: values.project ?? process.cwd(), ...loadOptions(values), ...values.cache ? { discoverCache: values.cache } : {} });
6082
6580
  return -1;
6083
6581
  }
@@ -6153,6 +6651,8 @@ var COMMANDS = {
6153
6651
  plan: cmdPlan,
6154
6652
  migrate: cmdMigrate,
6155
6653
  upgrade: cmdUpgrade,
6654
+ record: cmdRecord,
6655
+ hub: cmdHubServe,
6156
6656
  init: cmdInit,
6157
6657
  sync: cmdSync,
6158
6658
  scan: cmdScan,