uidex 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cli/cli.cjs +1024 -1041
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/headless/index.cjs +4 -448
  4. package/dist/headless/index.cjs.map +1 -1
  5. package/dist/headless/index.d.cts +41 -11
  6. package/dist/headless/index.d.ts +41 -11
  7. package/dist/headless/index.js +4 -450
  8. package/dist/headless/index.js.map +1 -1
  9. package/dist/index.cjs +147 -3252
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +43 -316
  12. package/dist/index.d.ts +43 -316
  13. package/dist/index.js +133 -3254
  14. package/dist/index.js.map +1 -1
  15. package/dist/playwright/index.cjs +175 -0
  16. package/dist/playwright/index.cjs.map +1 -1
  17. package/dist/playwright/index.d.cts +2 -0
  18. package/dist/playwright/index.d.ts +2 -0
  19. package/dist/playwright/index.js +167 -0
  20. package/dist/playwright/index.js.map +1 -1
  21. package/dist/playwright/states-reporter.cjs +123 -0
  22. package/dist/playwright/states-reporter.cjs.map +1 -0
  23. package/dist/playwright/states-reporter.d.cts +46 -0
  24. package/dist/playwright/states-reporter.d.ts +46 -0
  25. package/dist/playwright/states-reporter.js +88 -0
  26. package/dist/playwright/states-reporter.js.map +1 -0
  27. package/dist/playwright/states.cjs +118 -0
  28. package/dist/playwright/states.cjs.map +1 -0
  29. package/dist/playwright/states.d.cts +120 -0
  30. package/dist/playwright/states.d.ts +120 -0
  31. package/dist/playwright/states.js +88 -0
  32. package/dist/playwright/states.js.map +1 -0
  33. package/dist/react/index.cjs +163 -3255
  34. package/dist/react/index.cjs.map +1 -1
  35. package/dist/react/index.d.cts +62 -275
  36. package/dist/react/index.d.ts +62 -275
  37. package/dist/react/index.js +151 -3268
  38. package/dist/react/index.js.map +1 -1
  39. package/dist/scan/index.cjs +1194 -322
  40. package/dist/scan/index.cjs.map +1 -1
  41. package/dist/scan/index.d.cts +274 -8
  42. package/dist/scan/index.d.ts +274 -8
  43. package/dist/scan/index.js +1185 -322
  44. package/dist/scan/index.js.map +1 -1
  45. package/package.json +27 -31
  46. package/dist/cloud/index.cjs +0 -682
  47. package/dist/cloud/index.cjs.map +0 -1
  48. package/dist/cloud/index.d.cts +0 -270
  49. package/dist/cloud/index.d.ts +0 -270
  50. package/dist/cloud/index.js +0 -645
  51. package/dist/cloud/index.js.map +0 -1
package/dist/cli/cli.cjs CHANGED
@@ -181,14 +181,14 @@ ${helpText()}`);
181
181
  const provider = await selectProvider(opts, flags.provider);
182
182
  if (!provider) return out(0, "Cancelled.\n");
183
183
  if (sub === "install") {
184
- const result3 = await provider.install({
184
+ const result2 = await provider.install({
185
185
  cwd,
186
186
  force: flags.force === true
187
187
  });
188
- return out(0, formatChanges(provider, "Installed", result3));
188
+ return out(0, formatChanges(provider, "Installed", result2));
189
189
  }
190
- const result2 = await provider.uninstall({ cwd });
191
- return out(0, formatChanges(provider, "Uninstalled", result2));
190
+ const result = await provider.uninstall({ cwd });
191
+ return out(0, formatChanges(provider, "Uninstalled", result));
192
192
  }
193
193
  async function selectProvider(opts, explicit) {
194
194
  if (explicit) {
@@ -231,9 +231,9 @@ function parseFlags(args) {
231
231
  }
232
232
  return flags;
233
233
  }
234
- function formatChanges(provider, verb, result2) {
234
+ function formatChanges(provider, verb, result) {
235
235
  const lines = [`${verb} ${provider.label}:`];
236
- for (const c of result2.changes) lines.push(` ${describe(c)}`);
236
+ for (const c of result.changes) lines.push(` ${describe(c)}`);
237
237
  return lines.join("\n") + "\n";
238
238
  }
239
239
  function describe(c) {
@@ -300,7 +300,9 @@ var ALLOWED_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
300
300
  "output",
301
301
  "flows",
302
302
  "audit",
303
- "conventions"
303
+ "conventions",
304
+ "statesManifest",
305
+ "coverageBaseline"
304
306
  ]);
305
307
  var ALLOWED_SOURCE_KEYS = /* @__PURE__ */ new Set(["rootDir", "include", "exclude", "prefix"]);
306
308
  var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
@@ -310,7 +312,14 @@ var ALLOWED_CONVENTIONS_KEYS = /* @__PURE__ */ new Set([
310
312
  "flows",
311
313
  "regions"
312
314
  ]);
313
- var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set(["scopeLeak", "coverage", "acceptance"]);
315
+ var ALLOWED_AUDIT_KEYS = /* @__PURE__ */ new Set([
316
+ "scopeLeak",
317
+ "coverage",
318
+ "acceptance",
319
+ "states",
320
+ "pageCoverage",
321
+ "stateMatrix"
322
+ ]);
314
323
  function fail(msg) {
315
324
  throw new ConfigError(`Invalid .uidex.json: ${msg}`);
316
325
  }
@@ -369,6 +378,12 @@ function validateConfig(raw) {
369
378
  }
370
379
  if (raw.exclude !== void 0) assertStringArray(raw.exclude, `exclude`);
371
380
  if (raw.flows !== void 0) assertStringArray(raw.flows, `flows`);
381
+ if (raw.statesManifest !== void 0 && typeof raw.statesManifest !== "string") {
382
+ fail(`"statesManifest" must be a string`);
383
+ }
384
+ if (raw.coverageBaseline !== void 0 && typeof raw.coverageBaseline !== "string") {
385
+ fail(`"coverageBaseline" must be a string`);
386
+ }
372
387
  if (raw.audit !== void 0) {
373
388
  assertObject(raw.audit, "audit");
374
389
  for (const key of Object.keys(raw.audit)) {
@@ -408,7 +423,9 @@ function validateConfig(raw) {
408
423
  output: raw.output,
409
424
  flows: raw.flows,
410
425
  audit: raw.audit,
411
- conventions: raw.conventions
426
+ conventions: raw.conventions,
427
+ statesManifest: raw.statesManifest,
428
+ coverageBaseline: raw.coverageBaseline
412
429
  };
413
430
  }
414
431
  function parseConfig(json) {
@@ -598,6 +615,12 @@ var ENTITY_KINDS = [
598
615
  "primitive",
599
616
  "flow"
600
617
  ];
618
+ var CORE_STATE_KINDS = [
619
+ "loading",
620
+ "empty",
621
+ "populated",
622
+ "error"
623
+ ];
601
624
  function isMetaKind(kind) {
602
625
  return kind !== "route" && kind !== "flow";
603
626
  }
@@ -731,11 +754,11 @@ function createRegistry() {
731
754
  };
732
755
  const query = (predicate) => {
733
756
  const flows = getFlows();
734
- const result2 = [];
757
+ const result = [];
735
758
  for (const entity of allEntities()) {
736
- if (predicate(entity)) result2.push(freezeEntity(entity, flows));
759
+ if (predicate(entity)) result.push(freezeEntity(entity, flows));
737
760
  }
738
- return result2;
761
+ return result;
739
762
  };
740
763
  const byScope = (scope) => query(
741
764
  (entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
@@ -776,6 +799,295 @@ function createRegistry() {
776
799
  };
777
800
  }
778
801
 
802
+ // src/scanner/scan/page-coverage.ts
803
+ function compileRoute(pattern) {
804
+ const segs = pattern.split("/").filter(Boolean);
805
+ let specificity = 0;
806
+ const optionalTail = segs.length > 0 && /^\[\[\.{3}.+\]\]$/.test(segs[segs.length - 1]);
807
+ const effective = optionalTail ? segs.slice(0, -1) : segs;
808
+ const parts = effective.map((seg) => {
809
+ const catchAll = /^\[\.{3}.+\]$/.test(seg);
810
+ const dynamic = /^\[.+\]$/.test(seg);
811
+ if (catchAll) return "(?:[^/]+)(?:/[^/]+)*";
812
+ if (dynamic) return "[^/]+";
813
+ specificity++;
814
+ return seg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
815
+ });
816
+ let body = parts.length === 0 ? "" : "/" + parts.join("/");
817
+ if (optionalTail) body += "(?:/[^/]+)*";
818
+ if (body === "") body = "/";
819
+ const re = new RegExp(`^${body}/?$`);
820
+ return { test: (url) => re.test(url), specificity };
821
+ }
822
+ function matchUrlToRoute(url, routePaths) {
823
+ const path12 = stripQuery(url);
824
+ let best = null;
825
+ for (const rp of routePaths) {
826
+ const { test, specificity } = compileRoute(rp);
827
+ if (!test(path12)) continue;
828
+ if (!best || specificity > best.specificity)
829
+ best = { path: rp, specificity };
830
+ }
831
+ return best?.path ?? null;
832
+ }
833
+ function stripQuery(url) {
834
+ const q = url.indexOf("?");
835
+ const h = url.indexOf("#");
836
+ let end = url.length;
837
+ if (q !== -1) end = Math.min(end, q);
838
+ if (h !== -1) end = Math.min(end, h);
839
+ return url.slice(0, end);
840
+ }
841
+ function routeForCapture(c, routes, routePaths) {
842
+ if (c.route && routePaths.includes(c.route)) return c.route;
843
+ if (c.url) {
844
+ const m = matchUrlToRoute(c.url, routePaths);
845
+ if (m) return m;
846
+ }
847
+ if (c.kind === "page") {
848
+ const r = routes.find((r2) => r2.page === c.entity);
849
+ if (r) return r.path;
850
+ }
851
+ return null;
852
+ }
853
+ function checkPageCoverage(registry, manifest, baseline) {
854
+ const routes = registry.list("route");
855
+ if (routes.length === 0) return [];
856
+ const routePaths = routes.map((r) => r.path);
857
+ const captured = manifest.captured ?? [];
858
+ const optedOut = /* @__PURE__ */ new Set();
859
+ for (const r of routes) {
860
+ const page = registry.get("page", r.page);
861
+ if (page?.meta?.capture === false) optedOut.add(r.path);
862
+ }
863
+ const covered = /* @__PURE__ */ new Set();
864
+ for (const c of captured) {
865
+ const route = routeForCapture(c, routes, routePaths);
866
+ if (route) covered.add(route);
867
+ }
868
+ const baselineSet = new Set(baseline?.uncapturedPages ?? []);
869
+ const diagnostics = [];
870
+ for (const r of routes) {
871
+ if (covered.has(r.path) || optedOut.has(r.path)) continue;
872
+ const loc = registry.get("page", r.page)?.loc;
873
+ if (baselineSet.has(r.path)) {
874
+ diagnostics.push({
875
+ code: "page-backlog",
876
+ severity: "info",
877
+ message: `route "${r.path}" has no visual-states capture (acknowledged backlog)`,
878
+ file: loc?.file,
879
+ line: loc?.line,
880
+ hint: `Capture it (drive the route in a states spec) and it drops off the baseline, or opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`.`
881
+ });
882
+ continue;
883
+ }
884
+ diagnostics.push({
885
+ code: "page-no-capture",
886
+ severity: "error",
887
+ message: `route "${r.path}" has no visual-states capture and is not accounted for`,
888
+ file: loc?.file,
889
+ line: loc?.line,
890
+ entity: { kind: "route", id: r.path },
891
+ hint: `Capture the route in a states spec, opt it out with \`export const uidex = { page: "${r.page}", capture: false }\`, or grandfather it as acknowledged backlog with \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
892
+ });
893
+ }
894
+ for (const b of baselineSet) {
895
+ if (!routePaths.includes(b)) {
896
+ diagnostics.push({
897
+ code: "stale-page-baseline",
898
+ severity: "warning",
899
+ message: `coverage baseline lists "${b}", which is not a route in the registry`,
900
+ hint: `Remove it from the baseline (\`uidex scan --update-baseline\`); the route no longer exists.`
901
+ });
902
+ } else if (covered.has(b) || optedOut.has(b)) {
903
+ diagnostics.push({
904
+ code: "page-captured",
905
+ severity: "warning",
906
+ message: `route "${b}" is now captured but is still in the coverage baseline`,
907
+ hint: `Prune it with \`uidex scan --update-baseline\` so the backlog reflects reality.`
908
+ });
909
+ }
910
+ }
911
+ return diagnostics;
912
+ }
913
+ function computePageBaseline(registry, manifest) {
914
+ const uncaptured = [];
915
+ for (const d of checkPageCoverage(registry, manifest, {
916
+ uncapturedPages: []
917
+ })) {
918
+ if (d.code === "page-no-capture" && d.entity) uncaptured.push(d.entity.id);
919
+ }
920
+ return { uncapturedPages: uncaptured.sort() };
921
+ }
922
+
923
+ // src/scanner/scan/state-coverage.ts
924
+ var STATE_KINDS = ["page", "feature", "widget"];
925
+ function declaredStateEntities(registry) {
926
+ const out2 = [];
927
+ for (const kind of STATE_KINDS) {
928
+ for (const e of registry.list(kind)) {
929
+ const states = e.meta?.states;
930
+ if (!states || states.length === 0) continue;
931
+ out2.push({
932
+ kind,
933
+ id: e.id,
934
+ states: states.map((s) => s.name),
935
+ loc: e.loc
936
+ });
937
+ }
938
+ }
939
+ return out2;
940
+ }
941
+ function captureMatchesEntity(cap, ent) {
942
+ if (cap.entity !== ent.id) return false;
943
+ return cap.kind === void 0 || cap.kind === ent.kind;
944
+ }
945
+ function checkStateCoverage(registry, manifest) {
946
+ const diagnostics = [];
947
+ const declared = declaredStateEntities(registry);
948
+ const captured = manifest.captured ?? [];
949
+ for (const ent of declared) {
950
+ for (const state of ent.states) {
951
+ const hit = captured.some(
952
+ (c) => captureMatchesEntity(c, ent) && c.state === state
953
+ );
954
+ if (hit) continue;
955
+ diagnostics.push({
956
+ code: "missing-state-capture",
957
+ severity: "warning",
958
+ message: `${ent.kind} "${ent.id}" declares state "${state}" but no capture produced it`,
959
+ file: ent.loc?.file,
960
+ line: ent.loc?.line,
961
+ entity: { kind: ent.kind, id: ent.id },
962
+ hint: `Add a capture for "${ent.id}/${state}" to the states matrix, or remove the state from \`export const uidex = { states: [...] }\` if it no longer exists.`
963
+ });
964
+ }
965
+ }
966
+ for (const cap of captured) {
967
+ const candidates = declared.filter((ent2) => captureMatchesEntity(cap, ent2));
968
+ if (candidates.length === 0) continue;
969
+ const declaredSomewhere = candidates.some(
970
+ (ent2) => ent2.states.includes(cap.state)
971
+ );
972
+ if (declaredSomewhere) continue;
973
+ const ent = candidates[0];
974
+ diagnostics.push({
975
+ code: "orphan-state-capture",
976
+ severity: "warning",
977
+ message: `Capture produced state "${cap.state}" for ${ent.kind} "${ent.id}", which is not in its declared states [${ent.states.join(", ")}]`,
978
+ file: ent.loc?.file,
979
+ line: ent.loc?.line,
980
+ entity: { kind: ent.kind, id: ent.id },
981
+ hint: `Declare "${cap.state}" in \`export const uidex = { states: [...] }\` for "${ent.id}", or rename the capture to a declared state.`
982
+ });
983
+ }
984
+ return diagnostics;
985
+ }
986
+
987
+ // src/scanner/scan/state-matrix.ts
988
+ var CORE_KINDS = CORE_STATE_KINDS;
989
+ var STATE_ENTITY_KINDS = ["page", "feature", "widget"];
990
+ function resolveMatrixKind(registry, c) {
991
+ if (c.stateKind) return c.stateKind;
992
+ for (const kind of STATE_ENTITY_KINDS) {
993
+ const declared = registry.get(kind, c.entity)?.meta?.states?.find((s) => s.name === c.state)?.kind;
994
+ if (declared) return declared;
995
+ }
996
+ return CORE_KINDS.includes(c.state) ? c.state : "variant";
997
+ }
998
+ function coreKindsByRoute(registry, manifest) {
999
+ const routes = registry.list("route");
1000
+ const routePaths = routes.map((r) => r.path);
1001
+ const byRoute = /* @__PURE__ */ new Map();
1002
+ for (const c of manifest.captured ?? []) {
1003
+ const route = routeForCapture(c, routes, routePaths);
1004
+ if (!route) continue;
1005
+ if (!byRoute.has(route)) byRoute.set(route, /* @__PURE__ */ new Set());
1006
+ const kind = resolveMatrixKind(registry, c);
1007
+ if (CORE_KINDS.includes(kind)) byRoute.get(route).add(kind);
1008
+ }
1009
+ return byRoute;
1010
+ }
1011
+ function pageWaivers(registry, routePage) {
1012
+ return registry.get("page", routePage)?.meta?.waivers ?? {};
1013
+ }
1014
+ function isOptedOut(registry, page) {
1015
+ return page ? registry.get("page", page)?.meta?.capture === false : false;
1016
+ }
1017
+ function checkStateMatrix(registry, manifest, baseline) {
1018
+ const routes = registry.list("route");
1019
+ if (routes.length === 0) return [];
1020
+ const pageOf = new Map(routes.map((r) => [r.path, r.page]));
1021
+ const captured = coreKindsByRoute(registry, manifest);
1022
+ const baselined = baseline?.missingKinds ?? {};
1023
+ const diagnostics = [];
1024
+ for (const [route, kinds] of captured) {
1025
+ const page = pageOf.get(route);
1026
+ if (isOptedOut(registry, page)) continue;
1027
+ const waivers = page ? pageWaivers(registry, page) : {};
1028
+ const loc = page ? registry.get("page", page)?.loc : void 0;
1029
+ const baselinedKinds = new Set(baselined[route] ?? []);
1030
+ for (const kind of CORE_KINDS) {
1031
+ if (kinds.has(kind)) continue;
1032
+ if (waivers[kind]) continue;
1033
+ if (baselinedKinds.has(kind)) {
1034
+ diagnostics.push({
1035
+ code: "matrix-backlog",
1036
+ severity: "info",
1037
+ message: `route "${route}" does not render core kind "${kind}" (acknowledged MISSING_KINDS backlog)`,
1038
+ file: loc?.file,
1039
+ line: loc?.line,
1040
+ hint: `Capture the "${kind}" state for "${route}", waive it (\`waivers: { ${kind}: "why it can't" }\` on the page), or leave it in the baseline.`
1041
+ });
1042
+ continue;
1043
+ }
1044
+ diagnostics.push({
1045
+ code: "core-kind",
1046
+ severity: "error",
1047
+ message: `route "${route}" is captured but never renders core kind "${kind}"`,
1048
+ file: loc?.file,
1049
+ line: loc?.line,
1050
+ entity: { kind: "route", id: route },
1051
+ hint: `Capture the "${kind}" state, or if the route cannot render it add \`export const uidex = { page: "${page}", waivers: { ${kind}: "why it can't" } }\`, or grandfather it via \`uidex scan --update-baseline\` (the baseline change lands in the diff for review).`
1052
+ });
1053
+ }
1054
+ }
1055
+ for (const [route, kinds] of captured) {
1056
+ const page = pageOf.get(route);
1057
+ if (!page || isOptedOut(registry, page)) continue;
1058
+ const waivers = pageWaivers(registry, page);
1059
+ const loc = registry.get("page", page)?.loc;
1060
+ for (const kind of Object.keys(waivers)) {
1061
+ if (kinds.has(kind)) {
1062
+ diagnostics.push({
1063
+ code: "stale-waiver",
1064
+ severity: "warning",
1065
+ message: `page "${page}" waives core kind "${kind}", but route "${route}" does render it`,
1066
+ file: loc?.file,
1067
+ line: loc?.line,
1068
+ entity: { kind: "page", id: page },
1069
+ hint: `Remove the "${kind}" waiver \u2014 the route renders that kind, so the waiver's reason is stale.`
1070
+ });
1071
+ }
1072
+ }
1073
+ }
1074
+ return diagnostics;
1075
+ }
1076
+ function computeMissingKinds(registry, manifest) {
1077
+ const routes = registry.list("route");
1078
+ const pageOf = new Map(routes.map((r) => [r.path, r.page]));
1079
+ const captured = coreKindsByRoute(registry, manifest);
1080
+ const out2 = {};
1081
+ for (const [route, kinds] of captured) {
1082
+ const page = pageOf.get(route);
1083
+ if (isOptedOut(registry, page)) continue;
1084
+ const waivers = page ? pageWaivers(registry, page) : {};
1085
+ const missing = CORE_KINDS.filter((k) => !kinds.has(k) && !waivers[k]);
1086
+ if (missing.length > 0) out2[route] = missing;
1087
+ }
1088
+ return out2;
1089
+ }
1090
+
779
1091
  // src/scanner/scan/audit.ts
780
1092
  function audit(opts) {
781
1093
  const diagnostics = [];
@@ -785,6 +1097,9 @@ function audit(opts) {
785
1097
  const acceptanceEnabled = config.audit?.acceptance ?? true;
786
1098
  const scopeLeakEnabled = config.audit?.scopeLeak ?? true;
787
1099
  const coverageEnabled = config.audit?.coverage ?? true;
1100
+ const statesEnabled = config.audit?.states ?? true;
1101
+ const pageCoverageEnabled = config.audit?.pageCoverage ?? true;
1102
+ const stateMatrixEnabled = config.audit?.stateMatrix ?? true;
788
1103
  if (opts.resolveDiagnostics) diagnostics.push(...opts.resolveDiagnostics);
789
1104
  for (const ef of extracted) {
790
1105
  if (ef.diagnostics) diagnostics.push(...ef.diagnostics);
@@ -818,6 +1133,48 @@ function audit(opts) {
818
1133
  }
819
1134
  }
820
1135
  }
1136
+ if (check && opts.typesGenerated !== void 0) {
1137
+ const outRel = opts.typesOutputPath ?? config.output;
1138
+ const fresh = normalizeForCheck(opts.typesGenerated);
1139
+ if (opts.typesOnDisk === null || opts.typesOnDisk === void 0) {
1140
+ diagnostics.push({
1141
+ code: "gen-missing",
1142
+ severity: "error",
1143
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
1144
+ file: outRel,
1145
+ hint: "Run `uidex scan` (without --check) to regenerate"
1146
+ });
1147
+ } else if (normalizeForCheck(opts.typesOnDisk) !== fresh) {
1148
+ diagnostics.push({
1149
+ code: "gen-stale",
1150
+ severity: "error",
1151
+ message: `Generated file "${outRel}" is stale`,
1152
+ file: outRel,
1153
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
1154
+ });
1155
+ }
1156
+ }
1157
+ if (check && opts.locsGenerated !== void 0) {
1158
+ const outRel = opts.locsOutputPath ?? config.output;
1159
+ const fresh = normalizeForCheck(opts.locsGenerated);
1160
+ if (opts.locsOnDisk === null || opts.locsOnDisk === void 0) {
1161
+ diagnostics.push({
1162
+ code: "gen-missing",
1163
+ severity: "error",
1164
+ message: `Generated file "${outRel}" does not exist on disk; run \`uidex scan\` and commit the result`,
1165
+ file: outRel,
1166
+ hint: "Run `uidex scan` (without --check) to regenerate"
1167
+ });
1168
+ } else if (normalizeForCheck(opts.locsOnDisk) !== fresh) {
1169
+ diagnostics.push({
1170
+ code: "gen-stale",
1171
+ severity: "error",
1172
+ message: `Generated file "${outRel}" is stale`,
1173
+ file: outRel,
1174
+ hint: "Run `uidex scan` (without --check) to regenerate and commit the result"
1175
+ });
1176
+ }
1177
+ }
821
1178
  if (lint && acceptanceEnabled) {
822
1179
  for (const kind of ["widget", "feature", "page"]) {
823
1180
  for (const e of registry.list(kind)) {
@@ -1077,6 +1434,21 @@ function audit(opts) {
1077
1434
  }
1078
1435
  }
1079
1436
  }
1437
+ if (lint && statesEnabled && opts.capturedStates) {
1438
+ diagnostics.push(...checkStateCoverage(registry, opts.capturedStates));
1439
+ }
1440
+ if (lint && pageCoverageEnabled && opts.capturedStates) {
1441
+ diagnostics.push(
1442
+ ...checkPageCoverage(registry, opts.capturedStates, opts.pageBaseline)
1443
+ );
1444
+ }
1445
+ if (lint && stateMatrixEnabled && opts.capturedStates) {
1446
+ diagnostics.push(
1447
+ ...checkStateMatrix(registry, opts.capturedStates, {
1448
+ missingKinds: opts.pageBaseline?.missingKinds ?? {}
1449
+ })
1450
+ );
1451
+ }
1080
1452
  const summary = {
1081
1453
  errors: diagnostics.filter((d) => d.severity === "error").length,
1082
1454
  warnings: diagnostics.filter((d) => d.severity === "warning").length
@@ -1241,6 +1613,148 @@ function stableReplacer(_key, value) {
1241
1613
  }
1242
1614
 
1243
1615
  // src/scanner/scan/emit.ts
1616
+ var AUTHORED_KINDS = /* @__PURE__ */ new Set([
1617
+ "route",
1618
+ "page",
1619
+ "feature",
1620
+ "widget",
1621
+ "primitive",
1622
+ "flow"
1623
+ ]);
1624
+ function internedPayload(entities) {
1625
+ const fileIndex = /* @__PURE__ */ new Map();
1626
+ const files = [];
1627
+ const compact = entities.map((e) => {
1628
+ const loc = e.loc;
1629
+ if (!loc || typeof loc.file !== "string") return e;
1630
+ let idx = fileIndex.get(loc.file);
1631
+ if (idx === void 0) {
1632
+ idx = files.length;
1633
+ files.push(loc.file);
1634
+ fileIndex.set(loc.file, idx);
1635
+ }
1636
+ const rest = { ...loc };
1637
+ delete rest.file;
1638
+ return { ...e, loc: { f: idx, ...rest } };
1639
+ });
1640
+ return {
1641
+ filesDecl: `const files: readonly string[] = ${jsonStable(files)}`,
1642
+ parseExpr: `(JSON.parse(${JSON.stringify(
1643
+ JSON.stringify(compact, replacerSorted)
1644
+ )}) as any[]).map(hydrate)`
1645
+ };
1646
+ }
1647
+ var HYDRATE_FN = [
1648
+ "function hydrate(e: any): any {",
1649
+ ' if (e.loc && typeof e.loc.f === "number") {',
1650
+ " const { f, ...rest } = e.loc",
1651
+ " e.loc = { file: files[f], ...rest }",
1652
+ " }",
1653
+ " return e",
1654
+ "}"
1655
+ ].join("\n");
1656
+ function compactMeta(meta) {
1657
+ if (!meta || typeof meta !== "object") return meta;
1658
+ const composes = meta.composes;
1659
+ if (!Array.isArray(composes)) return meta;
1660
+ return {
1661
+ ...meta,
1662
+ composes: composes.map(
1663
+ (c) => c && typeof c === "object" && c.kind === "element" && Object.keys(c).length === 2 && typeof c.id === "string" ? c.id : c
1664
+ )
1665
+ };
1666
+ }
1667
+ function emitLocsModule(thin, uidexImport) {
1668
+ const kinds = [...new Set(thin.map((e) => e.kind))].sort();
1669
+ const kindIdx = new Map(kinds.map((k, i) => [k, i]));
1670
+ const fileIndex = /* @__PURE__ */ new Map();
1671
+ const files = [];
1672
+ const fi = (f) => {
1673
+ let i = fileIndex.get(f);
1674
+ if (i === void 0) {
1675
+ i = files.length;
1676
+ files.push(f);
1677
+ fileIndex.set(f, i);
1678
+ }
1679
+ return i;
1680
+ };
1681
+ const bare = [];
1682
+ const rich = [];
1683
+ for (const e of thin) {
1684
+ const loc = e.loc;
1685
+ const topOnlyKindIdLoc = Object.keys(e).every(
1686
+ (k) => k === "kind" || k === "id" || k === "loc"
1687
+ );
1688
+ const locOnlyKnown = !!loc && typeof loc.file === "string" && Object.keys(loc).every(
1689
+ (k) => k === "file" || k === "line" || k === "column"
1690
+ );
1691
+ if (topOnlyKindIdLoc && locOnlyKnown) {
1692
+ const row = [
1693
+ kindIdx.get(e.kind),
1694
+ e.id,
1695
+ fi(loc.file)
1696
+ ];
1697
+ if (loc.line != null || loc.column != null)
1698
+ row.push(Number(loc.line ?? 0));
1699
+ if (loc.column != null) row.push(Number(loc.column));
1700
+ bare.push(row);
1701
+ } else {
1702
+ const richEntity = { ...e };
1703
+ if (loc && typeof loc.file === "string") {
1704
+ const { file, ...rest } = loc;
1705
+ richEntity.loc = { f: fi(file), ...rest };
1706
+ }
1707
+ if (richEntity.meta) richEntity.meta = compactMeta(richEntity.meta);
1708
+ rich.push(richEntity);
1709
+ }
1710
+ }
1711
+ const str = (v) => JSON.stringify(JSON.stringify(v, replacerSorted));
1712
+ const l = [];
1713
+ l.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1714
+ l.push(
1715
+ "// Deferred element/region source locations for the inspector's copy-path."
1716
+ );
1717
+ l.push("// @ts-nocheck");
1718
+ l.push("/* eslint-disable */");
1719
+ l.push(`import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`);
1720
+ l.push("");
1721
+ l.push(`const files: readonly string[] = ${jsonStable(files)}`);
1722
+ l.push(`const kinds: readonly string[] = ${jsonStable(kinds)}`);
1723
+ l.push("function loc(fi: number, line?: number, col?: number) {");
1724
+ l.push(" const o: any = { file: files[fi] }");
1725
+ l.push(" if (line) o.line = line");
1726
+ l.push(" if (col) o.column = col");
1727
+ l.push(" return o");
1728
+ l.push("}");
1729
+ l.push(`const bare: any[] = JSON.parse(${str(bare)})`);
1730
+ l.push(`const rich: any[] = JSON.parse(${str(rich)})`);
1731
+ l.push("");
1732
+ l.push(
1733
+ "/** Add the thin element/region entities (source locations) to a registry. */"
1734
+ );
1735
+ l.push("export function loadLocs(target: Registry): Registry {");
1736
+ l.push(" for (const r of bare)");
1737
+ l.push(
1738
+ " target.add({ kind: kinds[r[0]], id: r[1], loc: loc(r[2], r[3], r[4]) } as Entity)"
1739
+ );
1740
+ l.push(" for (const e of rich) {");
1741
+ l.push(" const out: any = { ...e }");
1742
+ l.push(' if (e.loc && typeof e.loc.f === "number") {');
1743
+ l.push(" const { f, ...rest } = e.loc");
1744
+ l.push(" out.loc = { file: files[f], ...rest }");
1745
+ l.push(" }");
1746
+ l.push(" if (e.meta && Array.isArray(e.meta.composes)) {");
1747
+ l.push(
1748
+ ' out.meta = { ...e.meta, composes: e.meta.composes.map((c: any) => typeof c === "string" ? { id: c, kind: "element" } : c) }'
1749
+ );
1750
+ l.push(" }");
1751
+ l.push(" target.add(out)");
1752
+ l.push(" }");
1753
+ l.push(" return target");
1754
+ l.push("}");
1755
+ l.push("");
1756
+ return l.join("\n");
1757
+ }
1244
1758
  function sortById(arr) {
1245
1759
  return [...arr].sort((a, b) => a.id.localeCompare(b.id));
1246
1760
  }
@@ -1266,6 +1780,20 @@ function emitIdUnion(name, ids) {
1266
1780
  ${body}
1267
1781
  `;
1268
1782
  }
1783
+ var OPAQUE_ID_UNIONS = /* @__PURE__ */ new Set(["ElementId"]);
1784
+ function emitId(name, ids) {
1785
+ if (OPAQUE_ID_UNIONS.has(name)) {
1786
+ return `export type ${name} = string & { readonly __uidex?: ${JSON.stringify(name)} }
1787
+ `;
1788
+ }
1789
+ return emitIdUnion(name, ids);
1790
+ }
1791
+ function dataOutputPath(output) {
1792
+ return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".data.$1");
1793
+ }
1794
+ function locsOutputPath(output) {
1795
+ return output.replace(/\.(d\.ts|ts|tsx|mts|cts|js|mjs|cjs)$/, ".locs.$1");
1796
+ }
1269
1797
  function emit(opts) {
1270
1798
  const { registry, gitContext, uidexImport = "uidex" } = opts;
1271
1799
  const routes = [...registry.list("route")].sort(
@@ -1274,112 +1802,131 @@ function emit(opts) {
1274
1802
  const pages = sortById(registry.list("page"));
1275
1803
  const features = sortById(registry.list("feature"));
1276
1804
  const widgets = sortById(registry.list("widget"));
1805
+ const stateNames = /* @__PURE__ */ new Set();
1806
+ for (const e of [...pages, ...features, ...widgets]) {
1807
+ for (const s of e.meta?.states ?? []) stateNames.add(s.name);
1808
+ }
1277
1809
  const regions = sortById(registry.list("region"));
1278
1810
  const elements = sortById(registry.list("element"));
1279
1811
  const primitives = sortById(registry.list("primitive"));
1280
1812
  const flows = sortById(registry.list("flow"));
1281
- const lines = [];
1282
- lines.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1283
- lines.push("/* eslint-disable */");
1284
- lines.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
1285
- lines.push(
1286
- `import type { Entity, Registry } from ${JSON.stringify(uidexImport)}`
1813
+ const t = [];
1814
+ t.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1815
+ t.push(
1816
+ "// Types only \u2014 the runtime registry lives in the sibling `.data` module."
1287
1817
  );
1288
- lines.push("");
1289
- lines.push("// ---- id unions ----");
1290
- lines.push(
1291
- emitIdUnion(
1818
+ t.push("/* eslint-disable */");
1819
+ t.push("");
1820
+ t.push("// ---- id unions ----");
1821
+ t.push(
1822
+ emitId(
1292
1823
  "PageId",
1293
1824
  pages.map((e) => e.id)
1294
1825
  )
1295
1826
  );
1296
- lines.push(
1297
- emitIdUnion(
1827
+ t.push(
1828
+ emitId(
1298
1829
  "FeatureId",
1299
1830
  features.map((e) => e.id)
1300
1831
  )
1301
1832
  );
1302
- lines.push(
1303
- emitIdUnion(
1833
+ t.push(
1834
+ emitId(
1304
1835
  "WidgetId",
1305
1836
  widgets.map((e) => e.id)
1306
1837
  )
1307
1838
  );
1308
- lines.push(
1309
- emitIdUnion(
1839
+ t.push(
1840
+ emitId(
1310
1841
  "RegionId",
1311
1842
  regions.map((e) => e.id)
1312
1843
  )
1313
1844
  );
1314
- lines.push(
1315
- emitIdUnion(
1845
+ t.push(
1846
+ emitId(
1316
1847
  "ElementId",
1317
1848
  elements.map((e) => e.id)
1318
1849
  )
1319
1850
  );
1320
- lines.push(
1321
- emitIdUnion(
1851
+ t.push(
1852
+ emitId(
1322
1853
  "PrimitiveId",
1323
1854
  primitives.map((e) => e.id)
1324
1855
  )
1325
1856
  );
1326
- lines.push(
1327
- emitIdUnion(
1857
+ t.push(
1858
+ emitId(
1328
1859
  "FlowId",
1329
1860
  flows.map((e) => e.id)
1330
1861
  )
1331
1862
  );
1332
- lines.push(
1333
- emitIdUnion(
1863
+ t.push(
1864
+ emitId(
1334
1865
  "RouteId",
1335
1866
  routes.map((e) => e.path)
1336
1867
  )
1337
1868
  );
1338
- lines.push("");
1339
- lines.push("// ---- authoring-surface shape types ----");
1340
- lines.push("export namespace Uidex {");
1341
- lines.push(" export interface Page {");
1342
- lines.push(" page: PageId | false");
1343
- lines.push(" name?: string");
1344
- lines.push(" features?: readonly FeatureId[]");
1345
- lines.push(" widgets?: readonly WidgetId[]");
1346
- lines.push(" acceptance?: readonly string[]");
1347
- lines.push(" description?: string");
1348
- lines.push(" }");
1349
- lines.push(" export interface Feature {");
1350
- lines.push(" feature: FeatureId | false");
1351
- lines.push(" name?: string");
1352
- lines.push(" features?: readonly FeatureId[]");
1353
- lines.push(" acceptance?: readonly string[]");
1354
- lines.push(" description?: string");
1355
- lines.push(" }");
1356
- lines.push(" export interface Primitive {");
1357
- lines.push(" primitive: PrimitiveId");
1358
- lines.push(" name?: string");
1359
- lines.push(" description?: string");
1360
- lines.push(" }");
1361
- lines.push(" export interface Widget {");
1362
- lines.push(" widget: WidgetId");
1363
- lines.push(" name?: string");
1364
- lines.push(" acceptance?: readonly string[]");
1365
- lines.push(" description?: string");
1366
- lines.push(" }");
1367
- lines.push(" export interface Region {");
1368
- lines.push(" region: RegionId | false");
1369
- lines.push(" name?: string");
1370
- lines.push(" description?: string");
1371
- lines.push(" }");
1372
- lines.push(" export interface Flow {");
1373
- lines.push(" flow: FlowId");
1374
- lines.push(" name?: string");
1375
- lines.push(" description?: string");
1376
- lines.push(" }");
1377
- lines.push(" export interface NotFlow {");
1378
- lines.push(" notFlow: true");
1379
- lines.push(" }");
1380
- lines.push("}");
1381
- lines.push("");
1382
- lines.push("// ---- entities ----");
1869
+ t.push(emitId("StateId", [...stateNames]));
1870
+ t.push("");
1871
+ t.push("// ---- authoring-surface shape types ----");
1872
+ t.push("export namespace Uidex {");
1873
+ t.push(
1874
+ ' export type CoreStateKind = "loading" | "empty" | "populated" | "error"'
1875
+ );
1876
+ t.push(' export type StateKind = CoreStateKind | "variant"');
1877
+ t.push(" export interface State {");
1878
+ t.push(" name: string");
1879
+ t.push(" kind?: StateKind");
1880
+ t.push(" acceptance?: readonly string[]");
1881
+ t.push(" description?: string");
1882
+ t.push(" }");
1883
+ t.push(" export type Waivers = Partial<Record<CoreStateKind, string>>");
1884
+ t.push(" export interface Page {");
1885
+ t.push(" page: PageId | false");
1886
+ t.push(" name?: string");
1887
+ t.push(" features?: readonly FeatureId[]");
1888
+ t.push(" widgets?: readonly WidgetId[]");
1889
+ t.push(" acceptance?: readonly string[]");
1890
+ t.push(" states?: readonly (string | State)[]");
1891
+ t.push(" capture?: boolean");
1892
+ t.push(" waivers?: Waivers");
1893
+ t.push(" description?: string");
1894
+ t.push(" }");
1895
+ t.push(" export interface Feature {");
1896
+ t.push(" feature: FeatureId | false");
1897
+ t.push(" name?: string");
1898
+ t.push(" features?: readonly FeatureId[]");
1899
+ t.push(" acceptance?: readonly string[]");
1900
+ t.push(" states?: readonly (string | State)[]");
1901
+ t.push(" description?: string");
1902
+ t.push(" }");
1903
+ t.push(" export interface Primitive {");
1904
+ t.push(" primitive: PrimitiveId");
1905
+ t.push(" name?: string");
1906
+ t.push(" description?: string");
1907
+ t.push(" }");
1908
+ t.push(" export interface Widget {");
1909
+ t.push(" widget: WidgetId");
1910
+ t.push(" name?: string");
1911
+ t.push(" acceptance?: readonly string[]");
1912
+ t.push(" states?: readonly (string | State)[]");
1913
+ t.push(" description?: string");
1914
+ t.push(" }");
1915
+ t.push(" export interface Region {");
1916
+ t.push(" region: RegionId | false");
1917
+ t.push(" name?: string");
1918
+ t.push(" description?: string");
1919
+ t.push(" }");
1920
+ t.push(" export interface Flow {");
1921
+ t.push(" flow: FlowId");
1922
+ t.push(" name?: string");
1923
+ t.push(" description?: string");
1924
+ t.push(" }");
1925
+ t.push(" export interface NotFlow {");
1926
+ t.push(" notFlow: true");
1927
+ t.push(" }");
1928
+ t.push("}");
1929
+ t.push("");
1383
1930
  const allEntities = [
1384
1931
  ...routes,
1385
1932
  ...pages,
@@ -1390,25 +1937,39 @@ function emit(opts) {
1390
1937
  ...primitives,
1391
1938
  ...flows
1392
1939
  ];
1393
- lines.push(
1394
- `export const entities: ReadonlyArray<Entity> = ${jsonStable(allEntities)}`
1395
- );
1396
- lines.push("");
1397
- lines.push("// ---- git context ----");
1940
+ const authored = allEntities.filter((e) => AUTHORED_KINDS.has(e.kind));
1941
+ const thin = allEntities.filter((e) => !AUTHORED_KINDS.has(e.kind));
1398
1942
  const gc = gitContext ?? { branch: null, commit: null, pr: null };
1399
- lines.push(`export const gitContext = ${jsonStable(gc)} as const`);
1400
- lines.push("");
1401
- lines.push("// ---- registry factory ----");
1402
- lines.push("export function loadRegistry(target: Registry): Registry {");
1403
- lines.push(" for (const entity of entities) target.add(entity)");
1404
- lines.push(" return target");
1405
- lines.push("}");
1406
- lines.push("");
1407
- lines.push("// ---- preconfigured uidex instance ----");
1408
- lines.push("export const uidex = createUidex()");
1409
- lines.push("for (const entity of entities) uidex.registry.add(entity)");
1410
- lines.push("");
1411
- return lines.join("\n");
1943
+ const authoredPayload = internedPayload(authored);
1944
+ const d = [];
1945
+ d.push("// THIS FILE IS AUTO-GENERATED BY `uidex scan`. DO NOT EDIT.");
1946
+ d.push(
1947
+ "// Authored-metadata registry. The thin element/region entities (source"
1948
+ );
1949
+ d.push(
1950
+ "// locations only) live in the sibling `.locs` module, loaded lazily."
1951
+ );
1952
+ d.push("// @ts-nocheck");
1953
+ d.push("/* eslint-disable */");
1954
+ d.push(`import { createUidex } from ${JSON.stringify(uidexImport)}`);
1955
+ d.push(`import type { Registry } from ${JSON.stringify(uidexImport)}`);
1956
+ d.push("");
1957
+ d.push(authoredPayload.filesDecl);
1958
+ d.push(HYDRATE_FN);
1959
+ d.push(`export const entities = ${authoredPayload.parseExpr}`);
1960
+ d.push("");
1961
+ d.push(`export const gitContext = ${jsonStable(gc)} as const`);
1962
+ d.push("");
1963
+ d.push("export function loadRegistry(target: Registry): Registry {");
1964
+ d.push(" for (const entity of entities) target.add(entity)");
1965
+ d.push(" return target");
1966
+ d.push("}");
1967
+ d.push("");
1968
+ d.push("export const uidex = createUidex()");
1969
+ d.push("for (const entity of entities) uidex.registry.add(entity)");
1970
+ d.push("");
1971
+ const locs = emitLocsModule(thin, uidexImport);
1972
+ return { types: t.join("\n"), data: d.join("\n"), locs };
1412
1973
  }
1413
1974
 
1414
1975
  // src/scanner/scan/ast.ts
@@ -1445,14 +2006,14 @@ function makeLineAt(content) {
1445
2006
  function parseSource(file) {
1446
2007
  const lineAt = makeLineAt(file.content);
1447
2008
  try {
1448
- const result2 = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
2009
+ const result = (0, import_oxc_parser.parseSync)(file.sourcePath, file.content, {
1449
2010
  lang: langFor(file.sourcePath),
1450
2011
  sourceType: "module"
1451
2012
  });
1452
2013
  return {
1453
- program: result2.program,
1454
- hasErrors: result2.errors.length > 0,
1455
- comments: result2.comments ?? [],
2014
+ program: result.program,
2015
+ hasErrors: result.errors.length > 0,
2016
+ comments: result.comments ?? [],
1456
2017
  lineAt
1457
2018
  };
1458
2019
  } catch {
@@ -1512,20 +2073,35 @@ var ALLOWED_FIELDS = {
1512
2073
  "features",
1513
2074
  "widgets",
1514
2075
  "acceptance",
2076
+ "states",
2077
+ "capture",
2078
+ "waivers",
1515
2079
  "description"
1516
2080
  ]),
2081
+ // `capture` / `waivers` are route-scoped, and only a page maps to a route — so
2082
+ // they are page-only (the gates read them off the page). A feature/widget that
2083
+ // sets them gets an `uidex-export-unknown-field` error rather than a silent no-op.
1517
2084
  feature: /* @__PURE__ */ new Set([
1518
2085
  "feature",
1519
2086
  "name",
1520
2087
  "features",
1521
2088
  "acceptance",
2089
+ "states",
1522
2090
  "description"
1523
2091
  ]),
1524
2092
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
1525
- widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
2093
+ widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "states", "description"]),
1526
2094
  region: /* @__PURE__ */ new Set(["region", "name", "description"]),
1527
2095
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
1528
2096
  };
2097
+ var STATE_FIELDS = /* @__PURE__ */ new Set([
2098
+ "name",
2099
+ "kind",
2100
+ "acceptance",
2101
+ "description"
2102
+ ]);
2103
+ var CORE_STATE_KINDS2 = new Set(CORE_STATE_KINDS);
2104
+ var STATE_KINDS2 = /* @__PURE__ */ new Set([...CORE_STATE_KINDS, "variant"]);
1529
2105
  var FALSEABLE = /* @__PURE__ */ new Set([
1530
2106
  "page",
1531
2107
  "feature",
@@ -1915,6 +2491,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1915
2491
  }
1916
2492
  const featuresField = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1917
2493
  const widgetsField = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
2494
+ const states = kind === "page" || kind === "feature" || kind === "widget" ? readStatesField(byKey) : void 0;
2495
+ const capture = kind === "page" ? readBooleanField(byKey, "capture") : void 0;
2496
+ const waivers = kind === "page" ? readWaiversField(byKey) : void 0;
1918
2497
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1919
2498
  const metadata = {
1920
2499
  source: "ts-export",
@@ -1937,6 +2516,9 @@ function buildMetadata(value, file, sourcePath, headerPos, diagnostics) {
1937
2516
  metadata.widgets = widgetsField.values;
1938
2517
  metadata.widgetSpans = widgetsField.spans;
1939
2518
  }
2519
+ if (states?.length) metadata.states = states;
2520
+ if (capture !== void 0) metadata.capture = capture;
2521
+ if (waivers && Object.keys(waivers).length > 0) metadata.waivers = waivers;
1940
2522
  if (notFlow) metadata.notFlow = true;
1941
2523
  if (typeof id === "string" && idValue.kind === "string") {
1942
2524
  metadata.idSpan = idValue.span;
@@ -1973,6 +2555,48 @@ function readIdField(value, kind, fieldName) {
1973
2555
  value.pos
1974
2556
  );
1975
2557
  }
2558
+ function readWaiversField(byKey) {
2559
+ const entry = byKey.get("waivers");
2560
+ if (!entry) return void 0;
2561
+ if (entry.value.kind !== "object") {
2562
+ throw new ExtractError(
2563
+ "uidex-export-invalid-field",
2564
+ "`waivers` must be an object mapping a core kind to a reason.",
2565
+ entry.value.pos
2566
+ );
2567
+ }
2568
+ const waivers = {};
2569
+ for (const e of entry.value.entries) {
2570
+ if (!CORE_STATE_KINDS2.has(e.key)) {
2571
+ throw new ExtractError(
2572
+ "uidex-export-invalid-field",
2573
+ `waiver key "${e.key}" is not a core kind; only ${[...CORE_STATE_KINDS2].join(", ")} participate in the matrix.`,
2574
+ e.keyPos
2575
+ );
2576
+ }
2577
+ if (e.value.kind !== "string" || e.value.value.length === 0) {
2578
+ throw new ExtractError(
2579
+ "uidex-export-invalid-field",
2580
+ `waiver "${e.key}" needs a non-empty reason (why the route cannot render this kind).`,
2581
+ e.value.pos
2582
+ );
2583
+ }
2584
+ waivers[e.key] = e.value.value;
2585
+ }
2586
+ return waivers;
2587
+ }
2588
+ function readBooleanField(byKey, name) {
2589
+ const entry = byKey.get(name);
2590
+ if (!entry) return void 0;
2591
+ if (entry.value.kind !== "boolean") {
2592
+ throw new ExtractError(
2593
+ "uidex-export-invalid-field",
2594
+ `\`${name}\` must be a boolean.`,
2595
+ entry.value.pos
2596
+ );
2597
+ }
2598
+ return entry.value.value;
2599
+ }
1976
2600
  function readStringField(byKey, name) {
1977
2601
  const entry = byKey.get(name);
1978
2602
  if (!entry) return void 0;
@@ -2010,6 +2634,86 @@ function readStringArrayField(byKey, name) {
2010
2634
  }
2011
2635
  return { values, spans };
2012
2636
  }
2637
+ function readStatesField(byKey) {
2638
+ const entry = byKey.get("states");
2639
+ if (!entry) return void 0;
2640
+ if (entry.value.kind !== "array") {
2641
+ throw new ExtractError(
2642
+ "uidex-export-invalid-field",
2643
+ "`states` must be an array of state names or `{ name, acceptance?, description? }` objects.",
2644
+ entry.value.pos
2645
+ );
2646
+ }
2647
+ const states = [];
2648
+ const seen = /* @__PURE__ */ new Set();
2649
+ for (const item of entry.value.items) {
2650
+ let state;
2651
+ if (item.kind === "string") {
2652
+ state = { name: item.value, nameSpan: item.span };
2653
+ } else if (item.kind === "object") {
2654
+ const fieldByKey = /* @__PURE__ */ new Map();
2655
+ for (const e of item.entries) fieldByKey.set(e.key, e);
2656
+ for (const e of item.entries) {
2657
+ if (!STATE_FIELDS.has(e.key)) {
2658
+ throw new ExtractError(
2659
+ "uidex-export-invalid-field",
2660
+ `Unknown field "${e.key}" in a \`states\` entry. Allowed: ${Array.from(
2661
+ STATE_FIELDS
2662
+ ).sort().join(", ")}.`,
2663
+ e.value.pos
2664
+ );
2665
+ }
2666
+ }
2667
+ const nameEntry = fieldByKey.get("name");
2668
+ if (!nameEntry || nameEntry.value.kind !== "string") {
2669
+ throw new ExtractError(
2670
+ "uidex-export-invalid-field",
2671
+ "Each object `states` entry must have a string `name`.",
2672
+ item.pos
2673
+ );
2674
+ }
2675
+ state = { name: nameEntry.value.value, nameSpan: nameEntry.value.span };
2676
+ const kind = readStringField(fieldByKey, "kind");
2677
+ if (kind !== void 0) {
2678
+ if (!STATE_KINDS2.has(kind)) {
2679
+ throw new ExtractError(
2680
+ "uidex-export-invalid-field",
2681
+ `state \`kind\` must be one of ${[...STATE_KINDS2].join(", ")} (got "${kind}").`,
2682
+ fieldByKey.get("kind").value.pos
2683
+ );
2684
+ }
2685
+ state.kind = kind;
2686
+ }
2687
+ const acceptance = readStringArrayField(fieldByKey, "acceptance")?.values;
2688
+ if (acceptance) state.acceptance = acceptance;
2689
+ const description = readStringField(fieldByKey, "description");
2690
+ if (description) state.description = description;
2691
+ } else {
2692
+ throw new ExtractError(
2693
+ "uidex-export-invalid-field",
2694
+ "`states` items must be strings or `{ name, ... }` objects.",
2695
+ item.pos
2696
+ );
2697
+ }
2698
+ if (state.name.length === 0) {
2699
+ throw new ExtractError(
2700
+ "uidex-export-invalid-field",
2701
+ "A `states` entry has an empty `name`.",
2702
+ item.pos
2703
+ );
2704
+ }
2705
+ if (seen.has(state.name)) {
2706
+ throw new ExtractError(
2707
+ "uidex-export-duplicate-state",
2708
+ `Duplicate state "${state.name}" in \`states\`; each state name must be unique within the entity.`,
2709
+ item.pos
2710
+ );
2711
+ }
2712
+ seen.add(state.name);
2713
+ states.push(state);
2714
+ }
2715
+ return states;
2716
+ }
2013
2717
  function posAt(content, offset, p2) {
2014
2718
  const lineStart = content.lastIndexOf("\n", offset - 1) + 1;
2015
2719
  return { offset, line: p2.lineAt(offset), column: offset - lineStart + 1 };
@@ -2247,22 +2951,22 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
2247
2951
  if (!kind) continue;
2248
2952
  const value = attr.value;
2249
2953
  if (!value) continue;
2250
- let result2 = UNRESOLVED;
2954
+ let result = UNRESOLVED;
2251
2955
  let valueSpan;
2252
2956
  if (value.type === "Literal") {
2253
2957
  const v = staticString(value);
2254
- result2 = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
2255
- if (result2.resolved) valueSpan = { start: value.start, end: value.end };
2958
+ result = v !== null && v.length > 0 ? { resolved: true, ids: [v] } : UNRESOLVED;
2959
+ if (result.resolved) valueSpan = { start: value.start, end: value.end };
2256
2960
  } else if (value.type === "JSXExpressionContainer") {
2257
2961
  const expr = value.expression;
2258
2962
  if (expr && expr.type !== "JSXEmptyExpression") {
2259
- result2 = evalIdExpression(expr, consts);
2963
+ result = evalIdExpression(expr, consts);
2260
2964
  const inner = unwrapTsExpression(expr);
2261
- if (result2.resolved && staticString(inner) !== null) {
2965
+ if (result.resolved && staticString(inner) !== null) {
2262
2966
  valueSpan = { start: inner.start, end: inner.end };
2263
2967
  }
2264
2968
  }
2265
- if (!result2.resolved) {
2969
+ if (!result.resolved) {
2266
2970
  dynamicAttrs.push({
2267
2971
  kind,
2268
2972
  attrName: kind === "element" ? "data-uidex" : `data-uidex-${kind}`,
@@ -2271,15 +2975,15 @@ function collectElementAttrs(opening, consts, dynamicAttrs, lineAt) {
2271
2975
  continue;
2272
2976
  }
2273
2977
  }
2274
- if (!result2.resolved) continue;
2275
- for (const id of result2.ids) {
2978
+ if (!result.resolved) continue;
2979
+ for (const id of result.ids) {
2276
2980
  const resolved = {
2277
2981
  kind,
2278
2982
  id,
2279
2983
  start: attr.start,
2280
2984
  isPattern: id.includes("*"),
2281
2985
  // Only a single plain string literal is renameable in place.
2282
- ...result2.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
2986
+ ...result.ids.length === 1 && valueSpan ? { span: valueSpan } : {}
2283
2987
  };
2284
2988
  if (resolved.isPattern) patterns.push(resolved);
2285
2989
  else statics.push(resolved);
@@ -2633,7 +3337,12 @@ var BASE_EXCLUDES = [
2633
3337
  "**/dist/**",
2634
3338
  "**/build/**",
2635
3339
  "**/.next/**",
2636
- "**/*.gen.ts"
3340
+ // Exclude the scanner's own generated siblings so a scan never re-ingests its
3341
+ // output. Enumerated (not `*.gen.*`) so a hand-written file that merely contains
3342
+ // a `.gen.` segment, e.g. `foo.gen.helpers.ts`, is still scanned.
3343
+ "**/*.gen.ts",
3344
+ "**/*.gen.data.ts",
3345
+ "**/*.gen.locs.ts"
2637
3346
  ];
2638
3347
  var TEST_EXCLUDES = [
2639
3348
  "**/*.test.ts",
@@ -2782,6 +3491,18 @@ function buildMetaFromExport(exp) {
2782
3491
  if (exp.acceptance?.length) meta.acceptance = exp.acceptance;
2783
3492
  if (exp.features?.length) meta.features = exp.features;
2784
3493
  if (exp.widgets?.length) meta.widgets = exp.widgets;
3494
+ if (exp.states?.length) {
3495
+ meta.states = exp.states.map((s) => ({
3496
+ name: s.name,
3497
+ ...s.kind ? { kind: s.kind } : {},
3498
+ ...s.acceptance?.length ? { acceptance: s.acceptance } : {},
3499
+ ...s.description ? { description: s.description } : {}
3500
+ }));
3501
+ }
3502
+ if (exp.capture === false) meta.capture = false;
3503
+ if (exp.waivers && Object.keys(exp.waivers).length > 0) {
3504
+ meta.waivers = exp.waivers;
3505
+ }
2785
3506
  return Object.keys(meta).length > 0 ? meta : void 0;
2786
3507
  }
2787
3508
  function resolve3(ctx) {
@@ -3200,6 +3921,68 @@ function dedupe(arr) {
3200
3921
  }
3201
3922
 
3202
3923
  // src/scanner/scan/pipeline.ts
3924
+ var DEFAULT_STATES_MANIFEST = "uidex-states.json";
3925
+ var DEFAULT_COVERAGE_BASELINE = "uidex-coverage-baseline.json";
3926
+ function loadPageBaseline(configDir, config) {
3927
+ const rel = config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
3928
+ const abs = path9.resolve(configDir, rel);
3929
+ let raw;
3930
+ try {
3931
+ raw = fs6.readFileSync(abs, "utf8");
3932
+ } catch {
3933
+ return void 0;
3934
+ }
3935
+ try {
3936
+ const parsed = JSON.parse(raw);
3937
+ if (!parsed || !Array.isArray(parsed.uncapturedPages)) return void 0;
3938
+ const uncapturedPages = parsed.uncapturedPages.filter(
3939
+ (p2) => typeof p2 === "string"
3940
+ );
3941
+ let missingKinds;
3942
+ const mkRaw = parsed.missingKinds;
3943
+ if (mkRaw && typeof mkRaw === "object" && !Array.isArray(mkRaw)) {
3944
+ const mk = {};
3945
+ for (const [route, kinds] of Object.entries(mkRaw)) {
3946
+ if (Array.isArray(kinds)) {
3947
+ mk[route] = kinds.filter((k) => typeof k === "string");
3948
+ }
3949
+ }
3950
+ missingKinds = mk;
3951
+ }
3952
+ return { uncapturedPages, ...missingKinds ? { missingKinds } : {} };
3953
+ } catch {
3954
+ return void 0;
3955
+ }
3956
+ }
3957
+ function loadCapturedStates(configDir, config) {
3958
+ const rel = config.statesManifest ?? DEFAULT_STATES_MANIFEST;
3959
+ const abs = path9.resolve(configDir, rel);
3960
+ let raw;
3961
+ try {
3962
+ raw = fs6.readFileSync(abs, "utf8");
3963
+ } catch {
3964
+ return void 0;
3965
+ }
3966
+ try {
3967
+ const parsed = JSON.parse(raw);
3968
+ if (!parsed || !Array.isArray(parsed.captured)) return void 0;
3969
+ const captured = [];
3970
+ for (const entry of parsed.captured) {
3971
+ if (!entry || typeof entry !== "object") continue;
3972
+ const c = entry;
3973
+ if (typeof c.entity !== "string" || typeof c.state !== "string") continue;
3974
+ const rec = { entity: c.entity, state: c.state };
3975
+ if (typeof c.kind === "string") rec.kind = c.kind;
3976
+ if (typeof c.stateKind === "string") rec.stateKind = c.stateKind;
3977
+ if (typeof c.url === "string") rec.url = c.url;
3978
+ if (typeof c.route === "string") rec.route = c.route;
3979
+ captured.push(rec);
3980
+ }
3981
+ return { captured };
3982
+ } catch {
3983
+ return void 0;
3984
+ }
3985
+ }
3203
3986
  function runScan(opts = {}) {
3204
3987
  const cwd = opts.cwd ?? process.cwd();
3205
3988
  const configs = opts.configs ?? discover({ cwd });
@@ -3226,23 +4009,29 @@ function runOne(dc, opts) {
3226
4009
  flowFiles: extractedFlows
3227
4010
  });
3228
4011
  const gitContext = resolveGitContext({ cwd: configDir });
3229
- const generated = emit({
4012
+ const emitted = emit({
3230
4013
  registry: resolved.registry,
3231
4014
  gitContext
3232
4015
  });
3233
- const outputPath = path9.resolve(configDir, config.output);
3234
- const outputRel = config.output;
3235
- let existingOnDisk = null;
4016
+ const typesRel = config.output;
4017
+ const typesPath = path9.resolve(configDir, typesRel);
4018
+ const dataRel = dataOutputPath(config.output);
4019
+ const dataPath = path9.resolve(configDir, dataRel);
4020
+ const locsRel = locsOutputPath(config.output);
4021
+ const locsPath = path9.resolve(configDir, locsRel);
4022
+ let typesOnDisk = null;
4023
+ let dataOnDisk = null;
4024
+ let locsOnDisk = null;
3236
4025
  if (opts.check) {
3237
- try {
3238
- existingOnDisk = fs6.readFileSync(outputPath, "utf8");
3239
- } catch {
3240
- existingOnDisk = null;
3241
- }
4026
+ typesOnDisk = readOrNull(typesPath);
4027
+ dataOnDisk = readOrNull(dataPath);
4028
+ locsOnDisk = readOrNull(locsPath);
3242
4029
  }
3243
4030
  const hasExtractDiagnostics = [...extracted, ...extractedFlows].some(
3244
4031
  (ef) => (ef.diagnostics?.length ?? 0) > 0
3245
4032
  );
4033
+ const capturedStates = opts.capturedStates ?? loadCapturedStates(configDir, config);
4034
+ const pageBaseline = opts.pageBaseline ?? loadPageBaseline(configDir, config);
3246
4035
  let auditResult;
3247
4036
  if (opts.check || opts.lint || resolved.diagnostics.length > 0 || hasExtractDiagnostics) {
3248
4037
  auditResult = audit({
@@ -3254,9 +4043,20 @@ function runOne(dc, opts) {
3254
4043
  check: opts.check,
3255
4044
  lint: opts.lint,
3256
4045
  resolveDiagnostics: resolved.diagnostics,
3257
- generated,
3258
- existingOnDisk,
3259
- outputPath: outputRel
4046
+ // The entity array (and thus the meaningful drift) lives in the data file,
4047
+ // so the entity-aware gen-stale diff runs against it; the types file gets a
4048
+ // plain content compare (see audit()).
4049
+ generated: emitted.data,
4050
+ existingOnDisk: dataOnDisk,
4051
+ outputPath: dataRel,
4052
+ typesGenerated: emitted.types,
4053
+ typesOnDisk,
4054
+ typesOutputPath: typesRel,
4055
+ locsGenerated: emitted.locs,
4056
+ locsOnDisk,
4057
+ locsOutputPath: locsRel,
4058
+ capturedStates,
4059
+ pageBaseline
3260
4060
  });
3261
4061
  }
3262
4062
  return {
@@ -3265,13 +4065,38 @@ function runOne(dc, opts) {
3265
4065
  registry: resolved.registry,
3266
4066
  gitContext,
3267
4067
  audit: auditResult,
3268
- generated,
3269
- outputPath
4068
+ types: {
4069
+ generated: emitted.types,
4070
+ outputPath: typesPath,
4071
+ outputRel: typesRel
4072
+ },
4073
+ data: { generated: emitted.data, outputPath: dataPath, outputRel: dataRel },
4074
+ locs: { generated: emitted.locs, outputPath: locsPath, outputRel: locsRel },
4075
+ capturedStates
3270
4076
  };
3271
4077
  }
3272
- function writeScanResult(result2) {
3273
- fs6.mkdirSync(path9.dirname(result2.outputPath), { recursive: true });
3274
- fs6.writeFileSync(result2.outputPath, result2.generated, "utf8");
4078
+ function readOrNull(p2) {
4079
+ try {
4080
+ return fs6.readFileSync(p2, "utf8");
4081
+ } catch {
4082
+ return null;
4083
+ }
4084
+ }
4085
+ function writeArtifact(artifact) {
4086
+ if (readOrNull(artifact.outputPath) === artifact.generated) return false;
4087
+ fs6.mkdirSync(path9.dirname(artifact.outputPath), { recursive: true });
4088
+ fs6.writeFileSync(artifact.outputPath, artifact.generated, "utf8");
4089
+ return true;
4090
+ }
4091
+ function writeScanResult(result) {
4092
+ const wroteTypes = writeArtifact(result.types);
4093
+ const wroteData = writeArtifact(result.data);
4094
+ const wroteLocs = writeArtifact(result.locs);
4095
+ return wroteTypes || wroteData || wroteLocs;
4096
+ }
4097
+ function pageBaselinePath(result) {
4098
+ const rel = result.config.coverageBaseline ?? DEFAULT_COVERAGE_BASELINE;
4099
+ return path9.resolve(result.configDir, rel);
3275
4100
  }
3276
4101
 
3277
4102
  // src/scanner/scan/rename.ts
@@ -3411,7 +4236,7 @@ function renameEntity(opts) {
3411
4236
  ]
3412
4237
  };
3413
4238
  }
3414
- const result2 = applyFixes([
4239
+ const result = applyFixes([
3415
4240
  {
3416
4241
  code: "rename",
3417
4242
  severity: "info",
@@ -3422,8 +4247,8 @@ function renameEntity(opts) {
3422
4247
  }
3423
4248
  }
3424
4249
  ]);
3425
- if (result2.skipped.length > 0) {
3426
- errors.push(`Some edits were skipped: ${result2.skipped[0].reason}`);
4250
+ if (result.skipped.length > 0) {
4251
+ errors.push(`Some edits were skipped: ${result.skipped[0].reason}`);
3427
4252
  }
3428
4253
  for (const r of runScan({ cwd })) writeScanResult(r);
3429
4254
  return { edits: edits.length, manual, errors };
@@ -3522,10 +4347,10 @@ function parseArgs(args) {
3522
4347
  async function run(opts) {
3523
4348
  const cwd = opts.cwd ?? process.cwd();
3524
4349
  const { positional, flags } = parseArgs(opts.argv);
3525
- const command2 = positional[0] ?? "help";
4350
+ const command = positional[0] ?? "help";
3526
4351
  const writer = createWriter();
3527
4352
  try {
3528
- switch (command2) {
4353
+ switch (command) {
3529
4354
  case "help":
3530
4355
  case "--help":
3531
4356
  case "-h":
@@ -3540,16 +4365,16 @@ async function run(opts) {
3540
4365
  case "rename":
3541
4366
  return runRename(cwd, positional.slice(1), flags, writer);
3542
4367
  case "ai": {
3543
- const result2 = await runAiCommand({
4368
+ const result = await runAiCommand({
3544
4369
  cwd,
3545
4370
  argv: opts.argv.slice(1)
3546
4371
  });
3547
- if (result2.stdout) writer.out(result2.stdout.replace(/\n$/, ""));
3548
- if (result2.stderr) writer.err(result2.stderr.replace(/\n$/, ""));
3549
- return writer.result(result2.exitCode);
4372
+ if (result.stdout) writer.out(result.stdout.replace(/\n$/, ""));
4373
+ if (result.stderr) writer.err(result.stderr.replace(/\n$/, ""));
4374
+ return writer.result(result.exitCode);
3550
4375
  }
3551
4376
  default:
3552
- writer.err(`Unknown command: ${command2}`);
4377
+ writer.err(`Unknown command: ${command}`);
3553
4378
  writer.err(helpText2());
3554
4379
  return writer.result(1);
3555
4380
  }
@@ -3568,16 +4393,13 @@ function helpText2() {
3568
4393
  " scaffold <widget|page|feature> <id> Emit a Playwright spec from declared acceptance",
3569
4394
  " rename <element|widget|region> <old-id> <new-id> Rename an id everywhere (DOM attr, flows, exports)",
3570
4395
  " ai <install|uninstall|providers> Manage AI assistant integrations",
3571
- " api <METHOD> <PATH> Call the uidex API",
3572
- " api --list Show available API routes",
3573
- " api login Authenticate via browser",
3574
- " api login --token <tok> Store an auth token directly",
3575
4396
  "",
3576
4397
  "Flags:",
3577
4398
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
3578
4399
  " --lint Run lint diagnostics (missing annotations, scope leak, duplicate ids, coverage)",
3579
4400
  " --audit Equivalent to --check --lint (read-only)",
3580
4401
  " --fix Apply machine-generated fixes (add data-uidex to unannotated interactive elements, drop empty names), then rescan and write",
4402
+ " --update-baseline Regenerate the page-coverage baseline (uidex-coverage-baseline.json) from the current uncaptured routes",
3581
4403
  " --json Emit JSON diagnostics on stdout",
3582
4404
  " --force (scaffold) overwrite existing spec",
3583
4405
  ""
@@ -3597,7 +4419,7 @@ function runInit(cwd, w) {
3597
4419
  fs8.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf8");
3598
4420
  w.out(`Created ${configPath}`);
3599
4421
  const gitignorePath = path11.join(cwd, ".gitignore");
3600
- const entry = "*.gen.ts";
4422
+ const entry = "*.gen.*";
3601
4423
  if (fs8.existsSync(gitignorePath)) {
3602
4424
  const existing = fs8.readFileSync(gitignorePath, "utf8");
3603
4425
  const hasEntry = existing.split("\n").some((line) => line.trim() === entry);
@@ -3620,8 +4442,9 @@ function runInit(cwd, w) {
3620
4442
  }
3621
4443
  function runScanCommand(cwd, flags, w) {
3622
4444
  const fix = Boolean(flags.fix);
3623
- const check = !fix && Boolean(flags.check || flags.audit);
3624
- const lint = Boolean(flags.lint || flags.audit || fix);
4445
+ const updateBaseline = Boolean(flags["update-baseline"]);
4446
+ const check = !fix && !updateBaseline && Boolean(flags.check || flags.audit);
4447
+ const lint = Boolean(flags.lint || flags.audit || fix || updateBaseline);
3625
4448
  const asJson = Boolean(flags.json);
3626
4449
  let configs = discover({ cwd });
3627
4450
  if (configs.length === 0) {
@@ -3632,16 +4455,41 @@ function runScanCommand(cwd, flags, w) {
3632
4455
  let fixSkipped = [];
3633
4456
  if (fix) {
3634
4457
  const discovery = runScan({ cwd, check: true, lint: true, configs });
3635
- const result2 = applyFixes(
4458
+ const result = applyFixes(
3636
4459
  discovery.flatMap((r) => r.audit?.diagnostics ?? [])
3637
4460
  );
3638
- fixed = result2.applied;
3639
- fixSkipped = result2.skipped;
4461
+ fixed = result.applied;
4462
+ fixSkipped = result.skipped;
3640
4463
  configs = discover({ cwd });
3641
4464
  }
3642
4465
  const results = runScan({ cwd, check, lint, configs });
4466
+ const artifacts = results.flatMap((r) => [r.types, r.data, r.locs]);
4467
+ const wrote = /* @__PURE__ */ new Set();
3643
4468
  if (!check) {
3644
- for (const r of results) writeScanResult(r);
4469
+ for (const a of artifacts) {
4470
+ if (writeArtifact(a)) wrote.add(a);
4471
+ }
4472
+ }
4473
+ if (updateBaseline) {
4474
+ for (const r of results) {
4475
+ const manifest = r.capturedStates ?? { captured: [] };
4476
+ const pageBaseline = computePageBaseline(r.registry, manifest);
4477
+ const missingKinds = computeMissingKinds(r.registry, manifest);
4478
+ const baseline = {
4479
+ ...pageBaseline,
4480
+ ...Object.keys(missingKinds).length > 0 ? { missingKinds } : {}
4481
+ };
4482
+ const p2 = pageBaselinePath(r);
4483
+ fs8.writeFileSync(p2, JSON.stringify(baseline, null, 2) + "\n");
4484
+ const kindGaps = Object.values(missingKinds).reduce(
4485
+ (n, ks) => n + ks.length,
4486
+ 0
4487
+ );
4488
+ w.out(
4489
+ `Wrote ${p2} \u2014 ${baseline.uncapturedPages.length} uncaptured page(s), ${kindGaps} missing core-kind(s) baselined`
4490
+ );
4491
+ }
4492
+ return w.result(0);
3645
4493
  }
3646
4494
  const allDiagnostics = results.flatMap((r) => r.audit?.diagnostics ?? []);
3647
4495
  const summary = results.reduce(
@@ -3669,10 +4517,14 @@ function runScanCommand(cwd, flags, w) {
3669
4517
  );
3670
4518
  }
3671
4519
  for (const r of results) {
3672
- if (check) {
3673
- w.out(`Checked ${r.outputPath}`);
3674
- } else {
3675
- w.out(`Wrote ${r.outputPath}`);
4520
+ for (const art of [r.types, r.data, r.locs]) {
4521
+ if (check) {
4522
+ w.out(`Checked ${art.outputPath}`);
4523
+ } else if (wrote.has(art)) {
4524
+ w.out(`Wrote ${art.outputPath}`);
4525
+ } else {
4526
+ w.out(`Unchanged ${art.outputPath}`);
4527
+ }
3676
4528
  }
3677
4529
  for (const d of r.audit?.diagnostics ?? []) {
3678
4530
  const loc = d.file ? `${d.file}${d.line ? `:${d.line}` : ""}` : "";
@@ -3708,18 +4560,18 @@ function runScaffold(cwd, args, flags, w) {
3708
4560
  const entity = r.registry.get(scaffoldKind, id);
3709
4561
  if (!entity) continue;
3710
4562
  const outDir = path11.resolve(r.configDir, "e2e");
3711
- const result2 = scaffoldSpec({
4563
+ const result = scaffoldSpec({
3712
4564
  registry: r.registry,
3713
4565
  kind: scaffoldKind,
3714
4566
  id,
3715
4567
  outDir,
3716
4568
  force: Boolean(flags.force)
3717
4569
  });
3718
- if (result2.skipped) {
3719
- w.err(result2.reason ?? "skipped");
4570
+ if (result.skipped) {
4571
+ w.err(result.reason ?? "skipped");
3720
4572
  return w.result(1);
3721
4573
  }
3722
- w.out(`Wrote ${result2.outputPath}`);
4574
+ w.out(`Wrote ${result.outputPath}`);
3723
4575
  return w.result(0);
3724
4576
  }
3725
4577
  w.err(
@@ -3736,24 +4588,24 @@ function runRename(cwd, args, flags, w) {
3736
4588
  );
3737
4589
  return w.result(1);
3738
4590
  }
3739
- const result2 = renameEntity({
4591
+ const result = renameEntity({
3740
4592
  cwd,
3741
4593
  kind,
3742
4594
  oldId,
3743
4595
  newId,
3744
4596
  force: Boolean(flags.force)
3745
4597
  });
3746
- for (const e of result2.errors) w.err(e);
3747
- for (const m of result2.manual) {
4598
+ for (const e of result.errors) w.err(e);
4599
+ for (const m of result.manual) {
3748
4600
  w.err(`MANUAL ${m.file}:${m.line} \u2014 ${m.reason}`);
3749
4601
  }
3750
- if (result2.errors.length > 0) return w.result(1);
4602
+ if (result.errors.length > 0) return w.result(1);
3751
4603
  w.out(
3752
- `Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result2.edits} edit(s)); gen file regenerated`
4604
+ `Renamed ${kind} "${oldId}" \u2192 "${newId}" (${result.edits} edit(s)); gen file regenerated`
3753
4605
  );
3754
- if (result2.manual.length > 0) {
4606
+ if (result.manual.length > 0) {
3755
4607
  w.err(
3756
- `${result2.manual.length} occurrence(s) need manual follow-up (listed above)`
4608
+ `${result.manual.length} occurrence(s) need manual follow-up (listed above)`
3757
4609
  );
3758
4610
  return w.result(1);
3759
4611
  }
@@ -3775,883 +4627,14 @@ function createWriter() {
3775
4627
  };
3776
4628
  }
3777
4629
 
3778
- // src/scanner/cli/auth.ts
3779
- function createMemoryTokenStorage(initial) {
3780
- let token = initial ?? null;
3781
- return {
3782
- get: () => token,
3783
- set: (t) => {
3784
- token = t;
3785
- },
3786
- clear: () => {
3787
- token = null;
3788
- }
3789
- };
3790
- }
3791
- function createFileTokenStorage(options) {
3792
- const nodeFs = require("fs");
3793
- const nodePath = require("path");
3794
- const file = options.path;
3795
- function read() {
3796
- if (!nodeFs.existsSync(file)) return null;
3797
- try {
3798
- const raw = nodeFs.readFileSync(file, "utf8");
3799
- const parsed = JSON.parse(raw);
3800
- if (parsed && typeof parsed === "object") {
3801
- return parsed;
3802
- }
3803
- } catch {
3804
- return null;
3805
- }
3806
- return null;
3807
- }
3808
- return {
3809
- get: () => read()?.token ?? null,
3810
- set: (token) => {
3811
- nodeFs.mkdirSync(nodePath.dirname(file), { recursive: true });
3812
- nodeFs.writeFileSync(file, JSON.stringify({ token }, null, 2));
3813
- },
3814
- clear: () => {
3815
- if (nodeFs.existsSync(file)) nodeFs.unlinkSync(file);
3816
- }
3817
- };
3818
- }
3819
- function defaultTokenPath() {
3820
- const os = require("os");
3821
- const path12 = require("path");
3822
- return path12.join(os.homedir(), ".uidex", "cloud-token.json");
3823
- }
3824
-
3825
- // src/scanner/cli/http.ts
3826
- function createHttpClient(options) {
3827
- const baseUrl = options.baseUrl.replace(/\/$/, "");
3828
- async function request(path12, opts = {}) {
3829
- let url = `${baseUrl}${path12.startsWith("/") ? path12 : `/${path12}`}`;
3830
- if (opts.query) {
3831
- url += (url.includes("?") ? "&" : "?") + opts.query;
3832
- }
3833
- const headers = {
3834
- Accept: "application/json",
3835
- ...opts.headers
3836
- };
3837
- const token = options.token();
3838
- if (token) headers.Authorization = `Bearer ${token}`;
3839
- if (opts.body !== void 0) {
3840
- headers["Content-Type"] ??= "application/json";
3841
- }
3842
- const res = await fetch(url, {
3843
- method: opts.method ?? (opts.body ? "POST" : "GET"),
3844
- headers,
3845
- body: opts.body
3846
- });
3847
- const raw = await res.text();
3848
- let body = raw;
3849
- try {
3850
- body = JSON.parse(raw);
3851
- } catch {
3852
- }
3853
- return { status: res.status, body, raw };
3854
- }
3855
- return { request };
3856
- }
3857
-
3858
- // src/scanner/cli/json-highlight.ts
3859
- var RESET = "\x1B[0m";
3860
- var BOLD = "\x1B[1m";
3861
- var DIM = "\x1B[2m";
3862
- var RED = "\x1B[31m";
3863
- var CYAN = "\x1B[36m";
3864
- var GREEN = "\x1B[32m";
3865
- var YELLOW = "\x1B[33m";
3866
- var MAGENTA = "\x1B[35m";
3867
- function highlightJson(value, color) {
3868
- const json = JSON.stringify(value, null, 2);
3869
- if (!color) return json;
3870
- return json.replace(
3871
- /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
3872
- (match, key, str, literal, num) => {
3873
- if (key) return `${CYAN}${key}${RESET}:`;
3874
- if (str) return `${GREEN}${str}${RESET}`;
3875
- if (literal) return `${MAGENTA}${literal}${RESET}`;
3876
- if (num) return `${YELLOW}${num}${RESET}`;
3877
- return match;
3878
- }
3879
- );
3880
- }
3881
- function formatStatus(status, color) {
3882
- if (!color) return `HTTP ${status}`;
3883
- const code = status >= 400 ? RED : status >= 300 ? YELLOW : GREEN;
3884
- return `${code}HTTP ${status}${RESET}`;
3885
- }
3886
- function boldText(text, color) {
3887
- if (!color) return text;
3888
- return `${BOLD}${text}${RESET}`;
3889
- }
3890
- function dimText(text, color) {
3891
- if (!color) return text;
3892
- return `${DIM}${text}${RESET}`;
3893
- }
3894
-
3895
- // src/scanner/cli/api-routes.gen.ts
3896
- var API_ROUTES = [
3897
- {
3898
- "method": "POST",
3899
- "path": "/api/ingest",
3900
- "operationId": "submitReport",
3901
- "summary": "Submit feedback from the SDK.",
3902
- "tag": "Ingest",
3903
- "params": []
3904
- },
3905
- {
3906
- "method": "GET",
3907
- "path": "/api/ingest/config",
3908
- "operationId": "getIngestConfig",
3909
- "summary": "Get integration configuration for the project.",
3910
- "tag": "Ingest",
3911
- "params": []
3912
- },
3913
- {
3914
- "method": "GET",
3915
- "path": "/api/ingest/reports",
3916
- "operationId": "listIngestReports",
3917
- "summary": "List feedback for the project (SDK view).",
3918
- "tag": "Ingest",
3919
- "params": []
3920
- },
3921
- {
3922
- "method": "GET",
3923
- "path": "/api/ingest/pins",
3924
- "operationId": "listPins",
3925
- "summary": "List pins by route and/or entity.",
3926
- "tag": "Ingest",
3927
- "params": []
3928
- },
3929
- {
3930
- "method": "POST",
3931
- "path": "/api/ingest/pins/archive",
3932
- "operationId": "archivePin",
3933
- "summary": "Archive a pin.",
3934
- "tag": "Ingest",
3935
- "params": []
3936
- },
3937
- {
3938
- "method": "POST",
3939
- "path": "/api/cli-auth/authorize",
3940
- "operationId": "authorizeCli",
3941
- "summary": "Authorize a CLI session by delivering a token.",
3942
- "tag": "CLI Auth",
3943
- "params": []
3944
- },
3945
- {
3946
- "method": "GET",
3947
- "path": "/api/organizations",
3948
- "operationId": "listOrganizations",
3949
- "summary": "List organizations for the current user.",
3950
- "tag": "Organizations",
3951
- "params": []
3952
- },
3953
- {
3954
- "method": "POST",
3955
- "path": "/api/organizations",
3956
- "operationId": "createOrganization",
3957
- "summary": "Create an organization.",
3958
- "tag": "Organizations",
3959
- "params": []
3960
- },
3961
- {
3962
- "method": "POST",
3963
- "path": "/api/organizations/switch",
3964
- "operationId": "switchOrganization",
3965
- "summary": "Switch the active organization.",
3966
- "tag": "Organizations",
3967
- "params": []
3968
- },
3969
- {
3970
- "method": "GET",
3971
- "path": "/api/organizations/{orgId}",
3972
- "operationId": "getOrganization",
3973
- "summary": "Get organization details.",
3974
- "tag": "Organizations",
3975
- "params": [
3976
- "orgId"
3977
- ]
3978
- },
3979
- {
3980
- "method": "PATCH",
3981
- "path": "/api/organizations/{orgId}",
3982
- "operationId": "updateOrganization",
3983
- "summary": "Update an organization.",
3984
- "tag": "Organizations",
3985
- "params": [
3986
- "orgId"
3987
- ]
3988
- },
3989
- {
3990
- "method": "DELETE",
3991
- "path": "/api/organizations/{orgId}",
3992
- "operationId": "deleteOrganization",
3993
- "summary": "Delete an organization.",
3994
- "tag": "Organizations",
3995
- "params": [
3996
- "orgId"
3997
- ]
3998
- },
3999
- {
4000
- "method": "GET",
4001
- "path": "/api/organizations/{orgId}/members",
4002
- "operationId": "listMembers",
4003
- "summary": "List organization members.",
4004
- "tag": "Members",
4005
- "params": [
4006
- "orgId"
4007
- ]
4008
- },
4009
- {
4010
- "method": "DELETE",
4011
- "path": "/api/organizations/{orgId}/members",
4012
- "operationId": "removeMember",
4013
- "summary": "Remove a member from the organization.",
4014
- "tag": "Members",
4015
- "params": [
4016
- "orgId"
4017
- ]
4018
- },
4019
- {
4020
- "method": "PATCH",
4021
- "path": "/api/organizations/{orgId}/members/{memberId}",
4022
- "operationId": "updateMember",
4023
- "summary": "Update a member's role.",
4024
- "tag": "Members",
4025
- "params": [
4026
- "orgId",
4027
- "memberId"
4028
- ]
4029
- },
4030
- {
4031
- "method": "GET",
4032
- "path": "/api/organizations/{orgId}/invitations",
4033
- "operationId": "listInvitations",
4034
- "summary": "List organization invitations.",
4035
- "tag": "Invitations",
4036
- "params": [
4037
- "orgId"
4038
- ]
4039
- },
4040
- {
4041
- "method": "POST",
4042
- "path": "/api/organizations/{orgId}/invitations",
4043
- "operationId": "createInvitation",
4044
- "summary": "Create an invitation link.",
4045
- "tag": "Invitations",
4046
- "params": [
4047
- "orgId"
4048
- ]
4049
- },
4050
- {
4051
- "method": "DELETE",
4052
- "path": "/api/organizations/{orgId}/invitations/{inviteId}",
4053
- "operationId": "deleteInvitation",
4054
- "summary": "Revoke an invitation.",
4055
- "tag": "Invitations",
4056
- "params": [
4057
- "orgId",
4058
- "inviteId"
4059
- ]
4060
- },
4061
- {
4062
- "method": "GET",
4063
- "path": "/api/organizations/{orgId}/projects",
4064
- "operationId": "listProjects",
4065
- "summary": "List projects in an organization.",
4066
- "tag": "Projects",
4067
- "params": [
4068
- "orgId"
4069
- ]
4070
- },
4071
- {
4072
- "method": "POST",
4073
- "path": "/api/organizations/{orgId}/projects",
4074
- "operationId": "createProject",
4075
- "summary": "Create a project.",
4076
- "tag": "Projects",
4077
- "params": [
4078
- "orgId"
4079
- ]
4080
- },
4081
- {
4082
- "method": "GET",
4083
- "path": "/api/organizations/{orgId}/projects/{projectId}",
4084
- "operationId": "getProject",
4085
- "summary": "Get project details.",
4086
- "tag": "Projects",
4087
- "params": [
4088
- "orgId",
4089
- "projectId"
4090
- ]
4091
- },
4092
- {
4093
- "method": "PATCH",
4094
- "path": "/api/organizations/{orgId}/projects/{projectId}",
4095
- "operationId": "updateProject",
4096
- "summary": "Update a project.",
4097
- "tag": "Projects",
4098
- "params": [
4099
- "orgId",
4100
- "projectId"
4101
- ]
4102
- },
4103
- {
4104
- "method": "DELETE",
4105
- "path": "/api/organizations/{orgId}/projects/{projectId}",
4106
- "operationId": "deleteProject",
4107
- "summary": "Delete a project.",
4108
- "tag": "Projects",
4109
- "params": [
4110
- "orgId",
4111
- "projectId"
4112
- ]
4113
- },
4114
- {
4115
- "method": "GET",
4116
- "path": "/api/projects/resolve",
4117
- "operationId": "resolveProject",
4118
- "summary": "Resolve an API key to its project and organization.",
4119
- "tag": "Projects",
4120
- "params": []
4121
- },
4122
- {
4123
- "method": "GET",
4124
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
4125
- "operationId": "listApiKeys",
4126
- "summary": "List API keys for a project.",
4127
- "tag": "API Keys",
4128
- "params": [
4129
- "orgId",
4130
- "projectId"
4131
- ]
4132
- },
4133
- {
4134
- "method": "POST",
4135
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
4136
- "operationId": "createApiKey",
4137
- "summary": "Create an API key.",
4138
- "tag": "API Keys",
4139
- "params": [
4140
- "orgId",
4141
- "projectId"
4142
- ]
4143
- },
4144
- {
4145
- "method": "PATCH",
4146
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
4147
- "operationId": "revokeApiKey",
4148
- "summary": "Revoke an API key.",
4149
- "tag": "API Keys",
4150
- "params": [
4151
- "orgId",
4152
- "projectId",
4153
- "keyId"
4154
- ]
4155
- },
4156
- {
4157
- "method": "DELETE",
4158
- "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
4159
- "operationId": "deleteApiKey",
4160
- "summary": "Permanently delete an API key.",
4161
- "tag": "API Keys",
4162
- "params": [
4163
- "orgId",
4164
- "projectId",
4165
- "keyId"
4166
- ]
4167
- },
4168
- {
4169
- "method": "GET",
4170
- "path": "/api/organizations/{orgId}/projects/{projectId}/reports",
4171
- "operationId": "listProjectReports",
4172
- "summary": "List feedback for a project.",
4173
- "tag": "Reports",
4174
- "params": [
4175
- "orgId",
4176
- "projectId"
4177
- ]
4178
- },
4179
- {
4180
- "method": "GET",
4181
- "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
4182
- "operationId": "getReport",
4183
- "summary": "Get feedback details.",
4184
- "tag": "Reports",
4185
- "params": [
4186
- "orgId",
4187
- "projectId",
4188
- "reportId"
4189
- ]
4190
- },
4191
- {
4192
- "method": "PATCH",
4193
- "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
4194
- "operationId": "updateReport",
4195
- "summary": "Update feedback status, priority, or assignment.",
4196
- "tag": "Reports",
4197
- "params": [
4198
- "orgId",
4199
- "projectId",
4200
- "reportId"
4201
- ]
4202
- },
4203
- {
4204
- "method": "DELETE",
4205
- "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
4206
- "operationId": "deleteReport",
4207
- "summary": "Delete a feedback record.",
4208
- "tag": "Reports",
4209
- "params": [
4210
- "orgId",
4211
- "projectId",
4212
- "reportId"
4213
- ]
4214
- },
4215
- {
4216
- "method": "POST",
4217
- "path": "/api/organizations/{orgId}/projects/{projectId}/reports/bulk-status",
4218
- "operationId": "bulkUpdateReportStatus",
4219
- "summary": "Update status on multiple feedback records.",
4220
- "tag": "Reports",
4221
- "params": [
4222
- "orgId",
4223
- "projectId"
4224
- ]
4225
- },
4226
- {
4227
- "method": "GET",
4228
- "path": "/api/organizations/{orgId}/integrations",
4229
- "operationId": "listIntegrations",
4230
- "summary": "List integrations for an organization.",
4231
- "tag": "Integrations",
4232
- "params": [
4233
- "orgId"
4234
- ]
4235
- },
4236
- {
4237
- "method": "POST",
4238
- "path": "/api/organizations/{orgId}/integrations",
4239
- "operationId": "createIntegration",
4240
- "summary": "Create an integration.",
4241
- "tag": "Integrations",
4242
- "params": [
4243
- "orgId"
4244
- ]
4245
- },
4246
- {
4247
- "method": "GET",
4248
- "path": "/api/organizations/{orgId}/integrations/{integrationId}",
4249
- "operationId": "getIntegration",
4250
- "summary": "Get integration details.",
4251
- "tag": "Integrations",
4252
- "params": [
4253
- "orgId",
4254
- "integrationId"
4255
- ]
4256
- },
4257
- {
4258
- "method": "DELETE",
4259
- "path": "/api/organizations/{orgId}/integrations/{integrationId}",
4260
- "operationId": "deleteIntegration",
4261
- "summary": "Delete an integration.",
4262
- "tag": "Integrations",
4263
- "params": [
4264
- "orgId",
4265
- "integrationId"
4266
- ]
4267
- },
4268
- {
4269
- "method": "GET",
4270
- "path": "/api/organizations/{orgId}/integrations/{integrationId}/targets",
4271
- "operationId": "listIntegrationTargets",
4272
- "summary": "List available targets (e.g. Jira projects/boards).",
4273
- "tag": "Integrations",
4274
- "params": [
4275
- "orgId",
4276
- "integrationId"
4277
- ]
4278
- },
4279
- {
4280
- "method": "POST",
4281
- "path": "/api/organizations/{orgId}/integrations/{integrationId}/test",
4282
- "operationId": "testIntegration",
4283
- "summary": "Test integration connectivity.",
4284
- "tag": "Integrations",
4285
- "params": [
4286
- "orgId",
4287
- "integrationId"
4288
- ]
4289
- },
4290
- {
4291
- "method": "GET",
4292
- "path": "/api/organizations/{orgId}/external-issues",
4293
- "operationId": "listExternalIssues",
4294
- "summary": "List external issues, optionally filtered by feedback.",
4295
- "tag": "External Issues",
4296
- "params": [
4297
- "orgId"
4298
- ]
4299
- },
4300
- {
4301
- "method": "GET",
4302
- "path": "/api/organizations/{orgId}/issue-drafts",
4303
- "operationId": "listIssueDrafts",
4304
- "summary": "List issue drafts for a project.",
4305
- "tag": "Issue Drafts",
4306
- "params": [
4307
- "orgId"
4308
- ]
4309
- },
4310
- {
4311
- "method": "POST",
4312
- "path": "/api/organizations/{orgId}/issue-drafts",
4313
- "operationId": "createIssueDraft",
4314
- "summary": "Manually create an issue draft.",
4315
- "tag": "Issue Drafts",
4316
- "params": [
4317
- "orgId"
4318
- ]
4319
- },
4320
- {
4321
- "method": "PATCH",
4322
- "path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
4323
- "operationId": "updateIssueDraft",
4324
- "summary": "Update an issue draft.",
4325
- "tag": "Issue Drafts",
4326
- "params": [
4327
- "orgId",
4328
- "draftId"
4329
- ]
4330
- },
4331
- {
4332
- "method": "DELETE",
4333
- "path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
4334
- "operationId": "deleteIssueDraft",
4335
- "summary": "Delete an issue draft.",
4336
- "tag": "Issue Drafts",
4337
- "params": [
4338
- "orgId",
4339
- "draftId"
4340
- ]
4341
- },
4342
- {
4343
- "method": "POST",
4344
- "path": "/api/organizations/{orgId}/issue-drafts/{draftId}/submit",
4345
- "operationId": "submitIssueDraft",
4346
- "summary": "Submit a draft to the configured integration.",
4347
- "tag": "Issue Drafts",
4348
- "params": [
4349
- "orgId",
4350
- "draftId"
4351
- ]
4352
- },
4353
- {
4354
- "method": "POST",
4355
- "path": "/api/organizations/{orgId}/projects/{projectId}/triage",
4356
- "operationId": "runTriage",
4357
- "summary": "Run LLM-powered triage on untriaged feedback.",
4358
- "tag": "Triage",
4359
- "params": [
4360
- "orgId",
4361
- "projectId"
4362
- ]
4363
- },
4364
- {
4365
- "method": "GET",
4366
- "path": "/api/user/tokens",
4367
- "operationId": "listUserTokens",
4368
- "summary": "List personal access tokens.",
4369
- "tag": "User Tokens",
4370
- "params": []
4371
- },
4372
- {
4373
- "method": "POST",
4374
- "path": "/api/user/tokens",
4375
- "operationId": "createUserToken",
4376
- "summary": "Create a personal access token.",
4377
- "tag": "User Tokens",
4378
- "params": []
4379
- },
4380
- {
4381
- "method": "DELETE",
4382
- "path": "/api/user/tokens/{tokenId}",
4383
- "operationId": "deleteUserToken",
4384
- "summary": "Revoke and delete a personal access token.",
4385
- "tag": "User Tokens",
4386
- "params": [
4387
- "tokenId"
4388
- ]
4389
- }
4390
- ];
4391
-
4392
- // src/scanner/cli/api.ts
4393
- var METHODS = /* @__PURE__ */ new Set([
4394
- "GET",
4395
- "POST",
4396
- "PUT",
4397
- "PATCH",
4398
- "DELETE",
4399
- "HEAD",
4400
- "OPTIONS"
4401
- ]);
4402
- var HELP = `uidex api \u2014 call the uidex API
4403
-
4404
- Usage:
4405
- uidex api <METHOD> <PATH> [flags] Make an API request
4406
- uidex api --list [--tag <tag>] List available routes
4407
- uidex api login Authenticate via browser
4408
- uidex api login --token <tok> Store an auth token directly
4409
- uidex api status Check auth state
4410
-
4411
- Flags:
4412
- --body <json> Request body (JSON string)
4413
- --query <params> Query string (key=val&key2=val2)
4414
- --base-url <url> Override the API base URL
4415
- --token <tok> Use a specific token (instead of stored)
4416
- --raw Disable JSON pretty-printing
4417
- --no-color Disable colored output
4418
- --list List available API routes
4419
- --tag <tag> Filter --list by tag
4420
- --json Emit --list as JSON
4421
- `;
4422
- async function runApiCommand(opts) {
4423
- const { positional, flags } = parseArgs(opts.argv);
4424
- const stdout = [];
4425
- const stderr = [];
4426
- const color = opts.color ?? (flags["no-color"] ? false : process.stdout.isTTY ?? false);
4427
- const ctx = { flags, opts, color, stdout, stderr };
4428
- const sub = positional[0];
4429
- if (!sub && !flags.list) {
4430
- stdout.push(HELP);
4431
- return result(0, stdout, stderr);
4432
- }
4433
- if (flags.list) return listRoutes(ctx);
4434
- if (sub === "help" || sub === "--help" || sub === "-h") {
4435
- stdout.push(HELP);
4436
- return result(0, stdout, stderr);
4437
- }
4438
- if (sub === "login") return runLogin(ctx);
4439
- if (sub === "status") return runStatus(ctx);
4440
- const method = sub?.toUpperCase();
4441
- const path12 = positional[1];
4442
- if (!method || !METHODS.has(method)) {
4443
- stderr.push(`Unknown command or method: ${sub}`);
4444
- stderr.push(HELP);
4445
- return result(1, stdout, stderr);
4446
- }
4447
- if (!path12) {
4448
- stderr.push("Missing path. Usage: uidex api GET /api/organizations");
4449
- return result(1, stdout, stderr);
4450
- }
4451
- return runRequest(ctx, method, path12);
4452
- }
4453
- function listRoutes(ctx) {
4454
- const { flags, color, stdout, stderr } = ctx;
4455
- let routes = API_ROUTES;
4456
- const tagFilter = flags.tag;
4457
- if (typeof tagFilter === "string") {
4458
- routes = routes.filter(
4459
- (r) => r.tag.toLowerCase() === tagFilter.toLowerCase()
4460
- );
4461
- }
4462
- if (flags.json) {
4463
- stdout.push(JSON.stringify(routes, null, 2));
4464
- return result(0, stdout, stderr);
4465
- }
4466
- const grouped = /* @__PURE__ */ new Map();
4467
- for (const r of routes) {
4468
- const tag = r.tag || "Other";
4469
- let list = grouped.get(tag);
4470
- if (!list) {
4471
- list = [];
4472
- grouped.set(tag, list);
4473
- }
4474
- list.push(r);
4475
- }
4476
- const methodPad = 7;
4477
- for (const [tag, tagRoutes] of grouped) {
4478
- stdout.push("");
4479
- stdout.push(boldText(tag, color));
4480
- for (const r of tagRoutes) {
4481
- const m = r.method.padEnd(methodPad);
4482
- const desc = r.summary ? dimText(` ${r.operationId} \u2014 ${r.summary}`, color) : dimText(` ${r.operationId}`, color);
4483
- stdout.push(` ${m} ${r.path}${desc}`);
4484
- }
4485
- }
4486
- stdout.push("");
4487
- return result(0, stdout, stderr);
4488
- }
4489
- async function runLogin(ctx) {
4490
- const { flags, opts, stdout, stderr } = ctx;
4491
- const token = flags.token;
4492
- if (typeof token === "string" && token.length > 0) {
4493
- const storage = resolveTokenStorage(opts);
4494
- storage.set(token);
4495
- stdout.push("Token stored.");
4496
- return result(0, stdout, stderr);
4497
- }
4498
- return runBrowserLogin(ctx);
4499
- }
4500
- async function runBrowserLogin(ctx) {
4501
- const { flags, opts, color, stdout, stderr } = ctx;
4502
- const http = await import("http");
4503
- const crypto = await import("crypto");
4504
- const { exec } = await import("child_process");
4505
- const baseUrl = resolveBaseUrl(flags, opts);
4506
- const code = crypto.randomBytes(3).toString("hex").toUpperCase();
4507
- return new Promise((resolve7) => {
4508
- let settled = false;
4509
- const settle = (exitCode) => {
4510
- if (settled) return;
4511
- settled = true;
4512
- server.close();
4513
- clearTimeout(timeout);
4514
- resolve7(result(exitCode, stdout, stderr));
4515
- };
4516
- const timeout = setTimeout(() => {
4517
- stderr.push("Timed out waiting for browser authorization.");
4518
- settle(1);
4519
- }, 12e4);
4520
- const server = http.createServer((req, res) => {
4521
- if (!req.url?.startsWith("/callback")) {
4522
- res.writeHead(404);
4523
- res.end();
4524
- return;
4525
- }
4526
- const url = new URL(req.url, `http://127.0.0.1`);
4527
- const receivedToken = url.searchParams.get("token");
4528
- const receivedCode = url.searchParams.get("code");
4529
- if (receivedCode !== code) {
4530
- res.writeHead(400, { "Content-Type": "application/json" });
4531
- res.end(JSON.stringify({ error: "Code mismatch" }));
4532
- stderr.push("Received callback with mismatched code. Aborting.");
4533
- settle(1);
4534
- return;
4535
- }
4536
- if (!receivedToken) {
4537
- res.writeHead(400, { "Content-Type": "application/json" });
4538
- res.end(JSON.stringify({ error: "Missing token" }));
4539
- stderr.push("Received callback without token. Aborting.");
4540
- settle(1);
4541
- return;
4542
- }
4543
- res.writeHead(200, { "Content-Type": "application/json" });
4544
- res.end(JSON.stringify({ ok: true }));
4545
- const storage = resolveTokenStorage(opts);
4546
- storage.set(receivedToken);
4547
- stdout.push(dimText("Authenticated successfully.", color));
4548
- settle(0);
4549
- });
4550
- server.listen(0, "127.0.0.1", () => {
4551
- const addr = server.address();
4552
- if (!addr || typeof addr === "string") {
4553
- stderr.push("Failed to start local server.");
4554
- settle(1);
4555
- return;
4556
- }
4557
- const port = addr.port;
4558
- const authUrl = `${baseUrl}/cli-auth?code=${encodeURIComponent(code)}&port=${port}`;
4559
- stdout.push(dimText("Opening browser to authorize...", color));
4560
- stdout.push("");
4561
- stdout.push(` Confirmation code: ${boldText(code, color)}`);
4562
- stdout.push("");
4563
- stdout.push(
4564
- dimText("If the browser doesn't open, visit this URL manually:", color)
4565
- );
4566
- stdout.push(` ${authUrl}`);
4567
- stdout.push("");
4568
- process.stdout.write(result(0, stdout, stderr).stdout);
4569
- stdout.length = 0;
4570
- openBrowser(authUrl, exec);
4571
- });
4572
- server.on("error", (err2) => {
4573
- stderr.push(`Failed to start local server: ${err2.message}`);
4574
- settle(1);
4575
- });
4576
- });
4577
- }
4578
- function openBrowser(url, exec) {
4579
- const platform = process.platform;
4580
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
4581
- exec(`${cmd} ${JSON.stringify(url)}`);
4582
- }
4583
- function runStatus(ctx) {
4584
- const { flags, opts, stdout, stderr } = ctx;
4585
- const storage = resolveTokenStorage(opts);
4586
- const baseUrl = resolveBaseUrl(flags, opts);
4587
- const token = storage.get();
4588
- stdout.push(`baseUrl: ${baseUrl}`);
4589
- stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
4590
- return result(token ? 0 : 1, stdout, stderr);
4591
- }
4592
- async function runRequest(ctx, method, path12) {
4593
- const { flags, opts, color, stdout, stderr } = ctx;
4594
- const tokenFromFlag = flags.token;
4595
- const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
4596
- if (!token) {
4597
- stderr.push("Not authenticated. Run: uidex api login --token <value>");
4598
- return result(1, stdout, stderr);
4599
- }
4600
- const baseUrl = resolveBaseUrl(flags, opts);
4601
- const http = opts.http ?? createHttpClient({ baseUrl, token: () => token });
4602
- const bodyStr = typeof flags.body === "string" ? flags.body : void 0;
4603
- if (bodyStr !== void 0) {
4604
- try {
4605
- JSON.parse(bodyStr);
4606
- } catch {
4607
- stderr.push("Invalid JSON in --body");
4608
- return result(1, stdout, stderr);
4609
- }
4610
- }
4611
- const res = await http.request(path12, {
4612
- method,
4613
- body: bodyStr,
4614
- query: typeof flags.query === "string" ? flags.query : void 0
4615
- });
4616
- const isSuccess = res.status >= 200 && res.status < 300;
4617
- if (!isSuccess) {
4618
- stderr.push(formatStatus(res.status, color));
4619
- }
4620
- if (res.body !== void 0 && res.body !== null && res.body !== "") {
4621
- if (flags.raw) {
4622
- stdout.push(res.raw);
4623
- } else {
4624
- stdout.push(highlightJson(res.body, color));
4625
- }
4626
- }
4627
- return result(isSuccess ? 0 : 1, stdout, stderr);
4628
- }
4629
- function resolveTokenStorage(opts) {
4630
- if (opts.tokenStorage) return opts.tokenStorage;
4631
- const envToken = opts.env?.UIDEX_TOKEN;
4632
- if (envToken) return createMemoryTokenStorage(envToken);
4633
- return createFileTokenStorage({ path: defaultTokenPath() });
4634
- }
4635
- function resolveBaseUrl(flags, opts) {
4636
- return (typeof flags["base-url"] === "string" ? flags["base-url"] : void 0) ?? opts.baseUrl ?? opts.env?.UIDEX_API_URL ?? "https://app.uidex.dev";
4637
- }
4638
- function result(exitCode, stdout, stderr) {
4639
- return {
4640
- exitCode,
4641
- stdout: stdout.join("\n") + (stdout.length ? "\n" : ""),
4642
- stderr: stderr.join("\n") + (stderr.length ? "\n" : "")
4643
- };
4644
- }
4645
-
4646
4630
  // src/scanner/cli/cli.ts
4647
4631
  var argv = process.argv.slice(2);
4648
- var command = argv[0];
4649
- var resultP = command === "api" ? runApiCommand({ argv: argv.slice(1) }) : run({ argv });
4632
+ var resultP = run({ argv });
4650
4633
  resultP.then(
4651
- (result2) => {
4652
- if (result2.stdout) process.stdout.write(result2.stdout);
4653
- if (result2.stderr) process.stderr.write(result2.stderr);
4654
- process.exit(result2.exitCode);
4634
+ (result) => {
4635
+ if (result.stdout) process.stdout.write(result.stdout);
4636
+ if (result.stderr) process.stderr.write(result.stderr);
4637
+ process.exit(result.exitCode);
4655
4638
  },
4656
4639
  (error) => {
4657
4640
  process.stderr.write(