uidex 0.4.0 → 0.5.1

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 (42) hide show
  1. package/dist/cli/cli.cjs +1111 -87
  2. package/dist/cli/cli.cjs.map +1 -1
  3. package/dist/cloud/index.cjs +375 -72
  4. package/dist/cloud/index.cjs.map +1 -1
  5. package/dist/cloud/index.d.cts +82 -0
  6. package/dist/cloud/index.d.ts +82 -0
  7. package/dist/cloud/index.js +376 -71
  8. package/dist/cloud/index.js.map +1 -1
  9. package/dist/headless/index.cjs +623 -469
  10. package/dist/headless/index.cjs.map +1 -1
  11. package/dist/headless/index.d.cts +77 -75
  12. package/dist/headless/index.d.ts +77 -75
  13. package/dist/headless/index.js +627 -469
  14. package/dist/headless/index.js.map +1 -1
  15. package/dist/index.cjs +4258 -2884
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +275 -234
  18. package/dist/index.d.ts +275 -234
  19. package/dist/index.js +4280 -2890
  20. package/dist/index.js.map +1 -1
  21. package/dist/playwright/index.cjs +4 -4
  22. package/dist/playwright/index.cjs.map +1 -1
  23. package/dist/playwright/index.js +3 -3
  24. package/dist/playwright/index.js.map +1 -1
  25. package/dist/playwright/reporter.cjs +3 -3
  26. package/dist/playwright/reporter.cjs.map +1 -1
  27. package/dist/playwright/reporter.js +3 -3
  28. package/dist/playwright/reporter.js.map +1 -1
  29. package/dist/react/index.cjs +4299 -2906
  30. package/dist/react/index.cjs.map +1 -1
  31. package/dist/react/index.d.cts +206 -200
  32. package/dist/react/index.d.ts +206 -200
  33. package/dist/react/index.js +4339 -2926
  34. package/dist/react/index.js.map +1 -1
  35. package/dist/scan/index.cjs +201 -49
  36. package/dist/scan/index.cjs.map +1 -1
  37. package/dist/scan/index.d.cts +27 -1
  38. package/dist/scan/index.d.ts +27 -1
  39. package/dist/scan/index.js +200 -48
  40. package/dist/scan/index.js.map +1 -1
  41. package/package.json +8 -14
  42. package/templates/claude/api.md +110 -0
@@ -65,6 +65,26 @@ type EntityByKind<K extends EntityKind> = Extract<Entity, {
65
65
  kind: K;
66
66
  }>;
67
67
 
68
+ interface ReportRecord {
69
+ id: string;
70
+ entity?: string;
71
+ reporter?: {
72
+ id?: string;
73
+ email?: string;
74
+ name?: string;
75
+ };
76
+ title?: string;
77
+ body: string;
78
+ type: string;
79
+ severity: string;
80
+ status: string;
81
+ labels?: string[];
82
+ url: string;
83
+ route?: string;
84
+ pageTitle?: string;
85
+ screenshot?: string;
86
+ createdAt: string;
87
+ }
68
88
  interface Registry {
69
89
  add(entity: Entity): void;
70
90
  get<K extends EntityKind>(kind: K, id: string): EntityByKind<K> | undefined;
@@ -72,6 +92,11 @@ interface Registry {
72
92
  query(predicate: (entity: Entity) => boolean): Entity[];
73
93
  byScope(scope: Scope): Entity[];
74
94
  touchedBy(flowId: string): Entity[];
95
+ setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
96
+ getReports(kind: EntityKind, id: string): readonly ReportRecord[];
97
+ listReportKeys(): readonly string[];
98
+ archiveReport?: (reportId: string, reason?: string) => void | Promise<void>;
99
+ onReportsChange(cb: () => void): () => void;
75
100
  }
76
101
 
77
102
  interface SourceConfig {
@@ -130,7 +155,7 @@ interface Annotation {
130
155
  /** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
131
156
  ancestors?: AnnotationAncestor[];
132
157
  }
133
- type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "flow";
158
+ type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
134
159
  interface MetadataExport {
135
160
  source: "ts-export";
136
161
  kind: MetadataExportKind;
@@ -351,6 +376,7 @@ declare namespace Uidex {
351
376
  interface Feature<FeatureIds extends string = string> {
352
377
  feature: FeatureIds | false;
353
378
  name?: string;
379
+ features?: readonly FeatureIds[];
354
380
  acceptance?: readonly string[];
355
381
  description?: string;
356
382
  }
@@ -65,6 +65,26 @@ type EntityByKind<K extends EntityKind> = Extract<Entity, {
65
65
  kind: K;
66
66
  }>;
67
67
 
68
+ interface ReportRecord {
69
+ id: string;
70
+ entity?: string;
71
+ reporter?: {
72
+ id?: string;
73
+ email?: string;
74
+ name?: string;
75
+ };
76
+ title?: string;
77
+ body: string;
78
+ type: string;
79
+ severity: string;
80
+ status: string;
81
+ labels?: string[];
82
+ url: string;
83
+ route?: string;
84
+ pageTitle?: string;
85
+ screenshot?: string;
86
+ createdAt: string;
87
+ }
68
88
  interface Registry {
69
89
  add(entity: Entity): void;
70
90
  get<K extends EntityKind>(kind: K, id: string): EntityByKind<K> | undefined;
@@ -72,6 +92,11 @@ interface Registry {
72
92
  query(predicate: (entity: Entity) => boolean): Entity[];
73
93
  byScope(scope: Scope): Entity[];
74
94
  touchedBy(flowId: string): Entity[];
95
+ setReports(kind: EntityKind, id: string, reports: readonly ReportRecord[]): void;
96
+ getReports(kind: EntityKind, id: string): readonly ReportRecord[];
97
+ listReportKeys(): readonly string[];
98
+ archiveReport?: (reportId: string, reason?: string) => void | Promise<void>;
99
+ onReportsChange(cb: () => void): () => void;
75
100
  }
76
101
 
77
102
  interface SourceConfig {
@@ -130,7 +155,7 @@ interface Annotation {
130
155
  /** JSX ancestor chain, outermost to innermost. Only populated for DOM-attribute kinds. */
131
156
  ancestors?: AnnotationAncestor[];
132
157
  }
133
- type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "flow";
158
+ type MetadataExportKind = "page" | "feature" | "primitive" | "widget" | "region" | "flow";
134
159
  interface MetadataExport {
135
160
  source: "ts-export";
136
161
  kind: MetadataExportKind;
@@ -351,6 +376,7 @@ declare namespace Uidex {
351
376
  interface Feature<FeatureIds extends string = string> {
352
377
  feature: FeatureIds | false;
353
378
  name?: string;
379
+ features?: readonly FeatureIds[];
354
380
  acceptance?: readonly string[];
355
381
  description?: string;
356
382
  }
@@ -1,8 +1,8 @@
1
- // src/scan/discover.ts
1
+ // src/scanner/scan/discover.ts
2
2
  import * as fs from "fs";
3
3
  import * as path from "path";
4
4
 
5
- // src/scan/config.ts
5
+ // src/scanner/scan/config.ts
6
6
  var DEFAULT_TYPE_MODE = "strict";
7
7
  var WELL_KNOWN_FILES = {
8
8
  page: "uidex.page.ts",
@@ -166,7 +166,7 @@ var DEFAULT_CONVENTIONS = {
166
166
  regions: "landmarks"
167
167
  };
168
168
 
169
- // src/scan/discover.ts
169
+ // src/scanner/scan/discover.ts
170
170
  var CONFIG_FILENAME = ".uidex.json";
171
171
  var SKIP_DIRS = /* @__PURE__ */ new Set([
172
172
  "node_modules",
@@ -225,7 +225,7 @@ function discover(options = {}) {
225
225
  return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
226
226
  }
227
227
 
228
- // src/scan/walk.ts
228
+ // src/scanner/scan/walk.ts
229
229
  import * as fs2 from "fs";
230
230
  import * as path2 from "path";
231
231
  var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
@@ -350,12 +350,13 @@ function* walkDir(root, dir) {
350
350
  }
351
351
  }
352
352
 
353
- // src/scan/extract-uidex-export.ts
353
+ // src/scanner/scan/extract-uidex-export.ts
354
354
  var KIND_DISCRIMINATORS = [
355
355
  "page",
356
356
  "feature",
357
357
  "primitive",
358
358
  "widget",
359
+ "region",
359
360
  "flow",
360
361
  "notFlow"
361
362
  ];
@@ -368,15 +369,23 @@ var ALLOWED_FIELDS = {
368
369
  "acceptance",
369
370
  "description"
370
371
  ]),
371
- feature: /* @__PURE__ */ new Set(["feature", "name", "acceptance", "description"]),
372
+ feature: /* @__PURE__ */ new Set([
373
+ "feature",
374
+ "name",
375
+ "features",
376
+ "acceptance",
377
+ "description"
378
+ ]),
372
379
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
373
380
  widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
381
+ region: /* @__PURE__ */ new Set(["region", "name", "description"]),
374
382
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
375
383
  };
376
384
  var FALSEABLE = /* @__PURE__ */ new Set([
377
385
  "page",
378
386
  "feature",
379
- "primitive"
387
+ "primitive",
388
+ "region"
380
389
  ]);
381
390
  var ExtractError = class extends Error {
382
391
  code;
@@ -1104,7 +1113,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
1104
1113
  line: pos.line
1105
1114
  });
1106
1115
  }
1107
- const features = kind === "page" ? readStringArrayField(byKey, "features") : void 0;
1116
+ const features = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1108
1117
  const widgets = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1109
1118
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1110
1119
  const metadata = {
@@ -1199,7 +1208,7 @@ function posAt(content, offset) {
1199
1208
  return { offset, line, column: offset - lineStart + 1 };
1200
1209
  }
1201
1210
 
1202
- // src/scan/jsx-ancestry.ts
1211
+ // src/scanner/scan/jsx-ancestry.ts
1203
1212
  var DATA_ATTR_RE = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
1204
1213
  function parseDataAttrs(tagSource) {
1205
1214
  if (!tagSource.includes("data-uidex")) return [];
@@ -1388,7 +1397,7 @@ function findTagEnd(content, start) {
1388
1397
  return -1;
1389
1398
  }
1390
1399
 
1391
- // src/scan/extract.ts
1400
+ // src/scanner/scan/extract.ts
1392
1401
  var JSDOC_BLOCK = /\/\*\*([\s\S]*?)\*\//g;
1393
1402
  function lineAt(content, index) {
1394
1403
  let line = 1;
@@ -1491,10 +1500,10 @@ function extractOne(file) {
1491
1500
  return annotations;
1492
1501
  }
1493
1502
 
1494
- // src/scan/resolve.ts
1503
+ // src/scanner/scan/resolve.ts
1495
1504
  import * as path3 from "path";
1496
1505
 
1497
- // src/entities/types.ts
1506
+ // src/shared/entities/types.ts
1498
1507
  var ENTITY_KINDS = [
1499
1508
  "route",
1500
1509
  "page",
@@ -1527,7 +1536,7 @@ function assertEntityKind(kind) {
1527
1536
  if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
1528
1537
  }
1529
1538
 
1530
- // src/entities/registry.ts
1539
+ // src/shared/entities/registry.ts
1531
1540
  function emptyStore() {
1532
1541
  return {
1533
1542
  route: /* @__PURE__ */ new Map(),
@@ -1609,10 +1618,33 @@ function createRegistry() {
1609
1618
  return ids.has(entity.id);
1610
1619
  });
1611
1620
  };
1612
- return { add, get, list, query, byScope, touchedBy };
1621
+ const reports = /* @__PURE__ */ new Map();
1622
+ const reportsCbs = /* @__PURE__ */ new Set();
1623
+ const setReports = (kind, id, records) => {
1624
+ reports.set(`${kind}:${id}`, records);
1625
+ for (const cb of reportsCbs) cb();
1626
+ };
1627
+ const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
1628
+ const listReportKeys = () => Array.from(reports.keys());
1629
+ const onReportsChange = (cb) => {
1630
+ reportsCbs.add(cb);
1631
+ return () => reportsCbs.delete(cb);
1632
+ };
1633
+ return {
1634
+ add,
1635
+ get,
1636
+ list,
1637
+ query,
1638
+ byScope,
1639
+ touchedBy,
1640
+ setReports,
1641
+ getReports,
1642
+ listReportKeys,
1643
+ onReportsChange
1644
+ };
1613
1645
  }
1614
1646
 
1615
- // src/scan/routes.ts
1647
+ // src/scanner/scan/routes.ts
1616
1648
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
1617
1649
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
1618
1650
  var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
@@ -1678,7 +1710,7 @@ function pathToId(routePath) {
1678
1710
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1679
1711
  }
1680
1712
 
1681
- // src/scan/resolve.ts
1713
+ // src/scanner/scan/resolve.ts
1682
1714
  var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
1683
1715
  "element",
1684
1716
  "region",
@@ -1985,6 +2017,21 @@ function resolve2(ctx) {
1985
2017
  };
1986
2018
  registry.add(region);
1987
2019
  }
2020
+ for (const ef of ctx.extracted) {
2021
+ const exp = exportFor(ef.file.displayPath, "region");
2022
+ if (!exp) continue;
2023
+ if (exp.id === false) continue;
2024
+ if (typeof exp.id === "string" && !registry.get("region", exp.id)) {
2025
+ const meta = buildMetaFromExport(exp);
2026
+ const region = {
2027
+ kind: "region",
2028
+ id: exp.id,
2029
+ loc: { file: ef.file.displayPath, line: exp.loc.line },
2030
+ ...meta ? { meta } : {}
2031
+ };
2032
+ registry.add(region);
2033
+ }
2034
+ }
1988
2035
  const primitiveConventions = conventions.primitives;
1989
2036
  for (const ef of ctx.extracted) {
1990
2037
  const file = ef.file.displayPath;
@@ -2163,7 +2210,7 @@ function dedupe(arr) {
2163
2210
  return Array.from(new Set(arr));
2164
2211
  }
2165
2212
 
2166
- // src/scan/audit.ts
2213
+ // src/scanner/scan/audit.ts
2167
2214
  import * as path4 from "path";
2168
2215
  var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
2169
2216
  function audit(opts) {
@@ -2293,17 +2340,38 @@ function audit(opts) {
2293
2340
  }
2294
2341
  }
2295
2342
  if (lint) {
2343
+ const dynamicAttrRe = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*\{/g;
2296
2344
  for (const f of files) {
2297
- const lines = f.content.split("\n");
2298
- const candidateRe = /<(button|a|input|select|textarea)(?=[\s/>])([^>]*)>/g;
2299
2345
  let m;
2300
- while ((m = candidateRe.exec(f.content)) !== null) {
2301
- const attrs = m[2];
2302
- if (attrs && attrs.includes("data-uidex")) continue;
2303
- const idx = m.index;
2346
+ dynamicAttrRe.lastIndex = 0;
2347
+ while ((m = dynamicAttrRe.exec(f.content)) !== null) {
2348
+ const kind = m[1] ?? "element";
2304
2349
  let line = 1;
2305
- for (let i = 0; i < idx; i++) if (f.content[i] === "\n") line++;
2306
- void lines;
2350
+ for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
2351
+ const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
2352
+ diagnostics.push({
2353
+ code: "dynamic-attr",
2354
+ severity: "warning",
2355
+ message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
2356
+ file: f.displayPath,
2357
+ line,
2358
+ hint: dynamicAttrHint(kind)
2359
+ });
2360
+ }
2361
+ }
2362
+ }
2363
+ if (lint) {
2364
+ for (const f of files) {
2365
+ const tagRe = /<(button|a|input|select|textarea)(?=[\s/>])/g;
2366
+ let m;
2367
+ while ((m = tagRe.exec(f.content)) !== null) {
2368
+ const afterTag = m.index + m[0].length;
2369
+ const closeIdx = findJsxOpeningEnd(f.content, afterTag);
2370
+ if (closeIdx === -1) continue;
2371
+ const attrs = f.content.slice(afterTag, closeIdx);
2372
+ if (attrs.includes("data-uidex")) continue;
2373
+ let line = 1;
2374
+ for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
2307
2375
  diagnostics.push({
2308
2376
  code: "missing-element-annotation",
2309
2377
  severity: "info",
@@ -2318,6 +2386,15 @@ function audit(opts) {
2318
2386
  const primitives = registry.list("primitive");
2319
2387
  const byName = /* @__PURE__ */ new Map();
2320
2388
  for (const p2 of primitives) byName.set(p2.id, p2);
2389
+ const declaredFeatures = /* @__PURE__ */ new Map();
2390
+ for (const ef of extracted) {
2391
+ if (!ef.metadata) continue;
2392
+ for (const m of ef.metadata) {
2393
+ if (m.features && m.features.length > 0) {
2394
+ declaredFeatures.set(ef.file.displayPath, new Set(m.features));
2395
+ }
2396
+ }
2397
+ }
2321
2398
  for (const f of files) {
2322
2399
  const importRe = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
2323
2400
  let m;
@@ -2332,14 +2409,19 @@ function audit(opts) {
2332
2409
  if (!scope) continue;
2333
2410
  const [kind, id] = scope.split(":");
2334
2411
  const importerSegments = f.displayPath.split("/");
2335
- if (!importerSegments.includes(id) || !importerSegments.includes(kind + "s")) {
2336
- diagnostics.push({
2337
- code: "scope-leak",
2338
- severity: "warning",
2339
- message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
2340
- file: f.displayPath
2341
- });
2412
+ if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
2413
+ continue;
2414
+ }
2415
+ if (kind === "feature" && importerSegments.includes(id)) continue;
2416
+ if (kind === "feature" && declaredFeatures.get(f.displayPath)?.has(id)) {
2417
+ continue;
2342
2418
  }
2419
+ diagnostics.push({
2420
+ code: "scope-leak",
2421
+ severity: "warning",
2422
+ message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
2423
+ file: f.displayPath
2424
+ });
2343
2425
  }
2344
2426
  }
2345
2427
  }
@@ -2499,6 +2581,63 @@ function extractEntitiesArray(source) {
2499
2581
  }
2500
2582
  return null;
2501
2583
  }
2584
+ function findJsxOpeningEnd(src, start) {
2585
+ let i = start;
2586
+ while (i < src.length) {
2587
+ const ch = src[i];
2588
+ if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
2589
+ if (ch === '"' || ch === "'" || ch === "`") {
2590
+ i = skipString2(src, i);
2591
+ } else if (ch === "{") {
2592
+ i = skipBraces(src, i);
2593
+ } else {
2594
+ i++;
2595
+ }
2596
+ }
2597
+ return -1;
2598
+ }
2599
+ function skipString2(src, start) {
2600
+ const quote = src[start];
2601
+ let i = start + 1;
2602
+ while (i < src.length) {
2603
+ if (src[i] === "\\" && quote !== "`") {
2604
+ i += 2;
2605
+ continue;
2606
+ }
2607
+ if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
2608
+ i = skipBraces(src, i + 1);
2609
+ continue;
2610
+ }
2611
+ if (src[i] === quote) return i + 1;
2612
+ i++;
2613
+ }
2614
+ return i;
2615
+ }
2616
+ function skipBraces(src, start) {
2617
+ let depth = 1;
2618
+ let i = start + 1;
2619
+ while (i < src.length && depth > 0) {
2620
+ const ch = src[i];
2621
+ if (ch === "{") {
2622
+ depth++;
2623
+ i++;
2624
+ } else if (ch === "}") {
2625
+ depth--;
2626
+ i++;
2627
+ } else if (ch === '"' || ch === "'" || ch === "`") {
2628
+ i = skipString2(src, i);
2629
+ } else {
2630
+ i++;
2631
+ }
2632
+ }
2633
+ return i;
2634
+ }
2635
+ function dynamicAttrHint(kind) {
2636
+ if (kind === "region") {
2637
+ return `Use a string literal: \`data-uidex-region="id"\`, or declare the region via \`export const uidex = { region: "id" } as const satisfies Uidex.Region\` on the file that passes the region value`;
2638
+ }
2639
+ return `The scanner requires string-literal attribute values. If this component forwards the annotation via a prop, restructure so the caller provides the annotated element directly (e.g. via a slot or render prop) with a string-literal \`data-uidex\` attribute`;
2640
+ }
2502
2641
  function stableStringify(value) {
2503
2642
  return JSON.stringify(value, stableReplacer);
2504
2643
  }
@@ -2513,7 +2652,7 @@ function stableReplacer(_key, value) {
2513
2652
  return value;
2514
2653
  }
2515
2654
 
2516
- // src/scan/emit.ts
2655
+ // src/scanner/scan/emit.ts
2517
2656
  function sortById(arr) {
2518
2657
  return [...arr].sort((a, b) => a.id.localeCompare(b.id));
2519
2658
  }
@@ -2637,6 +2776,7 @@ function emit(opts) {
2637
2776
  lines.push(" export interface Feature {");
2638
2777
  lines.push(" feature: FeatureId | false");
2639
2778
  lines.push(" name?: string");
2779
+ lines.push(" features?: readonly FeatureId[]");
2640
2780
  lines.push(" acceptance?: readonly string[]");
2641
2781
  lines.push(" description?: string");
2642
2782
  lines.push(" }");
@@ -2651,6 +2791,11 @@ function emit(opts) {
2651
2791
  lines.push(" acceptance?: readonly string[]");
2652
2792
  lines.push(" description?: string");
2653
2793
  lines.push(" }");
2794
+ lines.push(" export interface Region {");
2795
+ lines.push(" region: RegionId | false");
2796
+ lines.push(" name?: string");
2797
+ lines.push(" description?: string");
2798
+ lines.push(" }");
2654
2799
  lines.push(" export interface Flow {");
2655
2800
  lines.push(" flow: FlowId");
2656
2801
  lines.push(" name?: string");
@@ -2693,7 +2838,7 @@ function emit(opts) {
2693
2838
  return lines.join("\n");
2694
2839
  }
2695
2840
 
2696
- // src/scan/git.ts
2841
+ // src/scanner/scan/git.ts
2697
2842
  import { execSync } from "child_process";
2698
2843
  function runGit(args, cwd) {
2699
2844
  try {
@@ -2725,7 +2870,7 @@ function parseGitHubRef(ref) {
2725
2870
  return m ? m[1] : null;
2726
2871
  }
2727
2872
 
2728
- // src/scan/scaffold.ts
2873
+ // src/scanner/scan/scaffold.ts
2729
2874
  import * as fs3 from "fs";
2730
2875
  import * as path5 from "path";
2731
2876
  function scaffoldWidgetSpec(opts) {
@@ -2788,7 +2933,7 @@ function renderSpec(args) {
2788
2933
  return lines.join("\n");
2789
2934
  }
2790
2935
 
2791
- // src/scan/pipeline.ts
2936
+ // src/scanner/scan/pipeline.ts
2792
2937
  import * as fs4 from "fs";
2793
2938
  import * as path6 from "path";
2794
2939
  function runScan(opts = {}) {
@@ -2862,18 +3007,18 @@ function writeScanResult(result) {
2862
3007
  fs4.writeFileSync(result.outputPath, result.generated, "utf8");
2863
3008
  }
2864
3009
 
2865
- // src/scan/cli.ts
3010
+ // src/scanner/scan/cli.ts
2866
3011
  import * as fs7 from "fs";
2867
3012
  import * as path9 from "path";
2868
3013
 
2869
- // src/scan/ai/index.ts
3014
+ // src/scanner/scan/ai/index.ts
2870
3015
  import * as p from "@clack/prompts";
2871
3016
 
2872
- // src/scan/ai/providers/claude.ts
3017
+ // src/scanner/scan/ai/providers/claude.ts
2873
3018
  import * as fs6 from "fs";
2874
3019
  import * as path8 from "path";
2875
3020
 
2876
- // src/scan/ai/templates.ts
3021
+ // src/scanner/scan/ai/templates.ts
2877
3022
  import * as fs5 from "fs";
2878
3023
  import * as path7 from "path";
2879
3024
  function templatePath(rel) {
@@ -2900,15 +3045,16 @@ function readTemplate(rel) {
2900
3045
  return fs5.readFileSync(templatePath(rel), "utf8");
2901
3046
  }
2902
3047
 
2903
- // src/scan/ai/providers/claude.ts
3048
+ // src/scanner/scan/ai/providers/claude.ts
2904
3049
  var CLAUDE_FILES = [
2905
3050
  { dest: ".claude/rules/uidex.md", template: "claude/rules.md" },
2906
- { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" }
3051
+ { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" },
3052
+ { dest: ".claude/commands/uidex/api.md", template: "claude/api.md" }
2907
3053
  ];
2908
3054
  var claudeProvider = {
2909
3055
  id: "claude",
2910
3056
  label: "Claude Code",
2911
- description: "Adds .claude/rules/uidex.md and the /uidex:audit slash command.",
3057
+ description: "Adds .claude/rules/uidex.md, /uidex:audit, and /uidex:api slash commands.",
2912
3058
  async install({ cwd, force }) {
2913
3059
  const changes = [];
2914
3060
  for (const file of CLAUDE_FILES) {
@@ -2956,13 +3102,13 @@ function cleanupEmpty(dir) {
2956
3102
  }
2957
3103
  }
2958
3104
 
2959
- // src/scan/ai/providers/index.ts
3105
+ // src/scanner/scan/ai/providers/index.ts
2960
3106
  var PROVIDERS = [claudeProvider];
2961
3107
  function getProvider(id) {
2962
3108
  return PROVIDERS.find((p2) => p2.id === id);
2963
3109
  }
2964
3110
 
2965
- // src/scan/ai/index.ts
3111
+ // src/scanner/scan/ai/index.ts
2966
3112
  async function runAiCommand(opts) {
2967
3113
  const { cwd, argv } = opts;
2968
3114
  const sub = argv[0];
@@ -3073,8 +3219,8 @@ function err(exitCode, stderr) {
3073
3219
  return { exitCode, stdout: "", stderr };
3074
3220
  }
3075
3221
 
3076
- // src/scan/cli.ts
3077
- function parseFlags2(args) {
3222
+ // src/scanner/cli/parse-args.ts
3223
+ function parseArgs(args) {
3078
3224
  const positional = [];
3079
3225
  const flags = {};
3080
3226
  for (let i = 0; i < args.length; i++) {
@@ -3098,9 +3244,11 @@ function parseFlags2(args) {
3098
3244
  }
3099
3245
  return { positional, flags };
3100
3246
  }
3247
+
3248
+ // src/scanner/scan/cli.ts
3101
3249
  async function run(opts) {
3102
3250
  const cwd = opts.cwd ?? process.cwd();
3103
- const { positional, flags } = parseFlags2(opts.argv);
3251
+ const { positional, flags } = parseArgs(opts.argv);
3104
3252
  const command = positional[0] ?? "help";
3105
3253
  const writer = createWriter();
3106
3254
  try {
@@ -3144,6 +3292,10 @@ function helpText2() {
3144
3292
  " scan [flags] Run the scanner pipeline",
3145
3293
  " scaffold widget <id> Emit a Playwright spec from a widget's acceptance",
3146
3294
  " ai <install|uninstall|providers> Manage AI assistant integrations",
3295
+ " api <METHOD> <PATH> Call the uidex API",
3296
+ " api --list Show available API routes",
3297
+ " api login Authenticate via browser",
3298
+ " api login --token <tok> Store an auth token directly",
3147
3299
  "",
3148
3300
  "Flags:",
3149
3301
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",