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
package/dist/cli/cli.cjs CHANGED
@@ -23,18 +23,18 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  mod
24
24
  ));
25
25
 
26
- // src/scan/cli.ts
26
+ // src/scanner/scan/cli.ts
27
27
  var fs7 = __toESM(require("fs"), 1);
28
28
  var path9 = __toESM(require("path"), 1);
29
29
 
30
- // src/scan/ai/index.ts
30
+ // src/scanner/scan/ai/index.ts
31
31
  var p = __toESM(require("@clack/prompts"), 1);
32
32
 
33
- // src/scan/ai/providers/claude.ts
33
+ // src/scanner/scan/ai/providers/claude.ts
34
34
  var fs2 = __toESM(require("fs"), 1);
35
35
  var path2 = __toESM(require("path"), 1);
36
36
 
37
- // src/scan/ai/templates.ts
37
+ // src/scanner/scan/ai/templates.ts
38
38
  var fs = __toESM(require("fs"), 1);
39
39
  var path = __toESM(require("path"), 1);
40
40
  function templatePath(rel) {
@@ -61,15 +61,16 @@ function readTemplate(rel) {
61
61
  return fs.readFileSync(templatePath(rel), "utf8");
62
62
  }
63
63
 
64
- // src/scan/ai/providers/claude.ts
64
+ // src/scanner/scan/ai/providers/claude.ts
65
65
  var CLAUDE_FILES = [
66
66
  { dest: ".claude/rules/uidex.md", template: "claude/rules.md" },
67
- { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" }
67
+ { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" },
68
+ { dest: ".claude/commands/uidex/api.md", template: "claude/api.md" }
68
69
  ];
69
70
  var claudeProvider = {
70
71
  id: "claude",
71
72
  label: "Claude Code",
72
- description: "Adds .claude/rules/uidex.md and the /uidex:audit slash command.",
73
+ description: "Adds .claude/rules/uidex.md, /uidex:audit, and /uidex:api slash commands.",
73
74
  async install({ cwd, force }) {
74
75
  const changes = [];
75
76
  for (const file of CLAUDE_FILES) {
@@ -117,16 +118,16 @@ function cleanupEmpty(dir) {
117
118
  }
118
119
  }
119
120
 
120
- // src/scan/ai/providers/index.ts
121
+ // src/scanner/scan/ai/providers/index.ts
121
122
  var PROVIDERS = [claudeProvider];
122
123
  function getProvider(id) {
123
124
  return PROVIDERS.find((p2) => p2.id === id);
124
125
  }
125
126
 
126
- // src/scan/ai/index.ts
127
+ // src/scanner/scan/ai/index.ts
127
128
  async function runAiCommand(opts) {
128
- const { cwd, argv } = opts;
129
- const sub = argv[0];
129
+ const { cwd, argv: argv2 } = opts;
130
+ const sub = argv2[0];
130
131
  if (!sub || sub === "--help" || sub === "-h" || sub === "help") {
131
132
  return out(0, helpText());
132
133
  }
@@ -141,18 +142,18 @@ async function runAiCommand(opts) {
141
142
 
142
143
  ${helpText()}`);
143
144
  }
144
- const flags = parseFlags(argv.slice(1));
145
+ const flags = parseFlags(argv2.slice(1));
145
146
  const provider = await selectProvider(opts, flags.provider);
146
147
  if (!provider) return out(0, "Cancelled.\n");
147
148
  if (sub === "install") {
148
- const result2 = await provider.install({
149
+ const result3 = await provider.install({
149
150
  cwd,
150
151
  force: flags.force === true
151
152
  });
152
- return out(0, formatChanges(provider, "Installed", result2));
153
+ return out(0, formatChanges(provider, "Installed", result3));
153
154
  }
154
- const result = await provider.uninstall({ cwd });
155
- return out(0, formatChanges(provider, "Uninstalled", result));
155
+ const result2 = await provider.uninstall({ cwd });
156
+ return out(0, formatChanges(provider, "Uninstalled", result2));
156
157
  }
157
158
  async function selectProvider(opts, explicit) {
158
159
  if (explicit) {
@@ -195,9 +196,9 @@ function parseFlags(args) {
195
196
  }
196
197
  return flags;
197
198
  }
198
- function formatChanges(provider, verb, result) {
199
+ function formatChanges(provider, verb, result2) {
199
200
  const lines = [`${verb} ${provider.label}:`];
200
- for (const c of result.changes) lines.push(` ${describe(c)}`);
201
+ for (const c of result2.changes) lines.push(` ${describe(c)}`);
201
202
  return lines.join("\n") + "\n";
202
203
  }
203
204
  function describe(c) {
@@ -234,11 +235,11 @@ function err(exitCode, stderr) {
234
235
  return { exitCode, stdout: "", stderr };
235
236
  }
236
237
 
237
- // src/scan/discover.ts
238
+ // src/scanner/scan/discover.ts
238
239
  var fs3 = __toESM(require("fs"), 1);
239
240
  var path3 = __toESM(require("path"), 1);
240
241
 
241
- // src/scan/config.ts
242
+ // src/scanner/scan/config.ts
242
243
  var DEFAULT_TYPE_MODE = "strict";
243
244
  var WELL_KNOWN_FILES = {
244
245
  page: "uidex.page.ts",
@@ -402,7 +403,7 @@ var DEFAULT_CONVENTIONS = {
402
403
  regions: "landmarks"
403
404
  };
404
405
 
405
- // src/scan/discover.ts
406
+ // src/scanner/scan/discover.ts
406
407
  var CONFIG_FILENAME = ".uidex.json";
407
408
  var SKIP_DIRS = /* @__PURE__ */ new Set([
408
409
  "node_modules",
@@ -461,14 +462,14 @@ function discover(options = {}) {
461
462
  return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
462
463
  }
463
464
 
464
- // src/scan/pipeline.ts
465
+ // src/scanner/scan/pipeline.ts
465
466
  var fs5 = __toESM(require("fs"), 1);
466
467
  var path7 = __toESM(require("path"), 1);
467
468
 
468
- // src/scan/audit.ts
469
+ // src/scanner/scan/audit.ts
469
470
  var path4 = __toESM(require("path"), 1);
470
471
 
471
- // src/entities/types.ts
472
+ // src/shared/entities/types.ts
472
473
  var ENTITY_KINDS = [
473
474
  "route",
474
475
  "page",
@@ -501,7 +502,7 @@ function assertEntityKind(kind) {
501
502
  if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
502
503
  }
503
504
 
504
- // src/entities/registry.ts
505
+ // src/shared/entities/registry.ts
505
506
  function emptyStore() {
506
507
  return {
507
508
  route: /* @__PURE__ */ new Map(),
@@ -565,11 +566,11 @@ function createRegistry() {
565
566
  };
566
567
  const query = (predicate) => {
567
568
  const flows = getFlows();
568
- const result = [];
569
+ const result2 = [];
569
570
  for (const entity of allEntities()) {
570
- if (predicate(entity)) result.push(freezeEntity(entity, flows));
571
+ if (predicate(entity)) result2.push(freezeEntity(entity, flows));
571
572
  }
572
- return result;
573
+ return result2;
573
574
  };
574
575
  const byScope = (scope) => query(
575
576
  (entity) => "scopes" in entity && Array.isArray(entity.scopes) && entity.scopes.includes(scope)
@@ -583,10 +584,33 @@ function createRegistry() {
583
584
  return ids.has(entity.id);
584
585
  });
585
586
  };
586
- return { add, get, list, query, byScope, touchedBy };
587
+ const reports = /* @__PURE__ */ new Map();
588
+ const reportsCbs = /* @__PURE__ */ new Set();
589
+ const setReports = (kind, id, records) => {
590
+ reports.set(`${kind}:${id}`, records);
591
+ for (const cb of reportsCbs) cb();
592
+ };
593
+ const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
594
+ const listReportKeys = () => Array.from(reports.keys());
595
+ const onReportsChange = (cb) => {
596
+ reportsCbs.add(cb);
597
+ return () => reportsCbs.delete(cb);
598
+ };
599
+ return {
600
+ add,
601
+ get,
602
+ list,
603
+ query,
604
+ byScope,
605
+ touchedBy,
606
+ setReports,
607
+ getReports,
608
+ listReportKeys,
609
+ onReportsChange
610
+ };
587
611
  }
588
612
 
589
- // src/scan/audit.ts
613
+ // src/scanner/scan/audit.ts
590
614
  var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
591
615
  function audit(opts) {
592
616
  const diagnostics = [];
@@ -714,18 +738,39 @@ function audit(opts) {
714
738
  }
715
739
  }
716
740
  }
741
+ if (lint) {
742
+ const dynamicAttrRe = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*\{/g;
743
+ for (const f of files) {
744
+ let m;
745
+ dynamicAttrRe.lastIndex = 0;
746
+ while ((m = dynamicAttrRe.exec(f.content)) !== null) {
747
+ const kind = m[1] ?? "element";
748
+ let line = 1;
749
+ for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
750
+ const attrName = m[1] ? `data-uidex-${m[1]}` : "data-uidex";
751
+ diagnostics.push({
752
+ code: "dynamic-attr",
753
+ severity: "warning",
754
+ message: `\`${attrName}={\u2026}\` uses a dynamic expression; the scanner cannot resolve the ${kind} id statically`,
755
+ file: f.displayPath,
756
+ line,
757
+ hint: dynamicAttrHint(kind)
758
+ });
759
+ }
760
+ }
761
+ }
717
762
  if (lint) {
718
763
  for (const f of files) {
719
- const lines = f.content.split("\n");
720
- const candidateRe = /<(button|a|input|select|textarea)(?=[\s/>])([^>]*)>/g;
764
+ const tagRe = /<(button|a|input|select|textarea)(?=[\s/>])/g;
721
765
  let m;
722
- while ((m = candidateRe.exec(f.content)) !== null) {
723
- const attrs = m[2];
724
- if (attrs && attrs.includes("data-uidex")) continue;
725
- const idx = m.index;
766
+ while ((m = tagRe.exec(f.content)) !== null) {
767
+ const afterTag = m.index + m[0].length;
768
+ const closeIdx = findJsxOpeningEnd(f.content, afterTag);
769
+ if (closeIdx === -1) continue;
770
+ const attrs = f.content.slice(afterTag, closeIdx);
771
+ if (attrs.includes("data-uidex")) continue;
726
772
  let line = 1;
727
- for (let i = 0; i < idx; i++) if (f.content[i] === "\n") line++;
728
- void lines;
773
+ for (let i = 0; i < m.index; i++) if (f.content[i] === "\n") line++;
729
774
  diagnostics.push({
730
775
  code: "missing-element-annotation",
731
776
  severity: "info",
@@ -740,6 +785,15 @@ function audit(opts) {
740
785
  const primitives = registry.list("primitive");
741
786
  const byName = /* @__PURE__ */ new Map();
742
787
  for (const p2 of primitives) byName.set(p2.id, p2);
788
+ const declaredFeatures = /* @__PURE__ */ new Map();
789
+ for (const ef of extracted) {
790
+ if (!ef.metadata) continue;
791
+ for (const m of ef.metadata) {
792
+ if (m.features && m.features.length > 0) {
793
+ declaredFeatures.set(ef.file.displayPath, new Set(m.features));
794
+ }
795
+ }
796
+ }
743
797
  for (const f of files) {
744
798
  const importRe = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
745
799
  let m;
@@ -754,14 +808,19 @@ function audit(opts) {
754
808
  if (!scope) continue;
755
809
  const [kind, id] = scope.split(":");
756
810
  const importerSegments = f.displayPath.split("/");
757
- if (!importerSegments.includes(id) || !importerSegments.includes(kind + "s")) {
758
- diagnostics.push({
759
- code: "scope-leak",
760
- severity: "warning",
761
- message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
762
- file: f.displayPath
763
- });
811
+ if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
812
+ continue;
813
+ }
814
+ if (kind === "feature" && importerSegments.includes(id)) continue;
815
+ if (kind === "feature" && declaredFeatures.get(f.displayPath)?.has(id)) {
816
+ continue;
764
817
  }
818
+ diagnostics.push({
819
+ code: "scope-leak",
820
+ severity: "warning",
821
+ message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
822
+ file: f.displayPath
823
+ });
765
824
  }
766
825
  }
767
826
  }
@@ -921,6 +980,63 @@ function extractEntitiesArray(source) {
921
980
  }
922
981
  return null;
923
982
  }
983
+ function findJsxOpeningEnd(src, start) {
984
+ let i = start;
985
+ while (i < src.length) {
986
+ const ch = src[i];
987
+ if (ch === ">" || ch === "/" && src[i + 1] === ">") return i;
988
+ if (ch === '"' || ch === "'" || ch === "`") {
989
+ i = skipString(src, i);
990
+ } else if (ch === "{") {
991
+ i = skipBraces(src, i);
992
+ } else {
993
+ i++;
994
+ }
995
+ }
996
+ return -1;
997
+ }
998
+ function skipString(src, start) {
999
+ const quote = src[start];
1000
+ let i = start + 1;
1001
+ while (i < src.length) {
1002
+ if (src[i] === "\\" && quote !== "`") {
1003
+ i += 2;
1004
+ continue;
1005
+ }
1006
+ if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
1007
+ i = skipBraces(src, i + 1);
1008
+ continue;
1009
+ }
1010
+ if (src[i] === quote) return i + 1;
1011
+ i++;
1012
+ }
1013
+ return i;
1014
+ }
1015
+ function skipBraces(src, start) {
1016
+ let depth = 1;
1017
+ let i = start + 1;
1018
+ while (i < src.length && depth > 0) {
1019
+ const ch = src[i];
1020
+ if (ch === "{") {
1021
+ depth++;
1022
+ i++;
1023
+ } else if (ch === "}") {
1024
+ depth--;
1025
+ i++;
1026
+ } else if (ch === '"' || ch === "'" || ch === "`") {
1027
+ i = skipString(src, i);
1028
+ } else {
1029
+ i++;
1030
+ }
1031
+ }
1032
+ return i;
1033
+ }
1034
+ function dynamicAttrHint(kind) {
1035
+ if (kind === "region") {
1036
+ 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`;
1037
+ }
1038
+ 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`;
1039
+ }
924
1040
  function stableStringify(value) {
925
1041
  return JSON.stringify(value, stableReplacer);
926
1042
  }
@@ -935,7 +1051,7 @@ function stableReplacer(_key, value) {
935
1051
  return value;
936
1052
  }
937
1053
 
938
- // src/scan/emit.ts
1054
+ // src/scanner/scan/emit.ts
939
1055
  function sortById(arr) {
940
1056
  return [...arr].sort((a, b) => a.id.localeCompare(b.id));
941
1057
  }
@@ -1059,6 +1175,7 @@ function emit(opts) {
1059
1175
  lines.push(" export interface Feature {");
1060
1176
  lines.push(" feature: FeatureId | false");
1061
1177
  lines.push(" name?: string");
1178
+ lines.push(" features?: readonly FeatureId[]");
1062
1179
  lines.push(" acceptance?: readonly string[]");
1063
1180
  lines.push(" description?: string");
1064
1181
  lines.push(" }");
@@ -1073,6 +1190,11 @@ function emit(opts) {
1073
1190
  lines.push(" acceptance?: readonly string[]");
1074
1191
  lines.push(" description?: string");
1075
1192
  lines.push(" }");
1193
+ lines.push(" export interface Region {");
1194
+ lines.push(" region: RegionId | false");
1195
+ lines.push(" name?: string");
1196
+ lines.push(" description?: string");
1197
+ lines.push(" }");
1076
1198
  lines.push(" export interface Flow {");
1077
1199
  lines.push(" flow: FlowId");
1078
1200
  lines.push(" name?: string");
@@ -1115,12 +1237,13 @@ function emit(opts) {
1115
1237
  return lines.join("\n");
1116
1238
  }
1117
1239
 
1118
- // src/scan/extract-uidex-export.ts
1240
+ // src/scanner/scan/extract-uidex-export.ts
1119
1241
  var KIND_DISCRIMINATORS = [
1120
1242
  "page",
1121
1243
  "feature",
1122
1244
  "primitive",
1123
1245
  "widget",
1246
+ "region",
1124
1247
  "flow",
1125
1248
  "notFlow"
1126
1249
  ];
@@ -1133,15 +1256,23 @@ var ALLOWED_FIELDS = {
1133
1256
  "acceptance",
1134
1257
  "description"
1135
1258
  ]),
1136
- feature: /* @__PURE__ */ new Set(["feature", "name", "acceptance", "description"]),
1259
+ feature: /* @__PURE__ */ new Set([
1260
+ "feature",
1261
+ "name",
1262
+ "features",
1263
+ "acceptance",
1264
+ "description"
1265
+ ]),
1137
1266
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
1138
1267
  widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
1268
+ region: /* @__PURE__ */ new Set(["region", "name", "description"]),
1139
1269
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
1140
1270
  };
1141
1271
  var FALSEABLE = /* @__PURE__ */ new Set([
1142
1272
  "page",
1143
1273
  "feature",
1144
- "primitive"
1274
+ "primitive",
1275
+ "region"
1145
1276
  ]);
1146
1277
  var ExtractError = class extends Error {
1147
1278
  code;
@@ -1869,7 +2000,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
1869
2000
  line: pos.line
1870
2001
  });
1871
2002
  }
1872
- const features = kind === "page" ? readStringArrayField(byKey, "features") : void 0;
2003
+ const features = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1873
2004
  const widgets = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1874
2005
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1875
2006
  const metadata = {
@@ -1964,7 +2095,7 @@ function posAt(content, offset) {
1964
2095
  return { offset, line, column: offset - lineStart + 1 };
1965
2096
  }
1966
2097
 
1967
- // src/scan/jsx-ancestry.ts
2098
+ // src/scanner/scan/jsx-ancestry.ts
1968
2099
  var DATA_ATTR_RE = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
1969
2100
  function parseDataAttrs(tagSource) {
1970
2101
  if (!tagSource.includes("data-uidex")) return [];
@@ -2008,7 +2139,7 @@ function collectJSXAncestry(content) {
2008
2139
  continue;
2009
2140
  }
2010
2141
  if (c === '"' || c === "'") {
2011
- const next = skipString(content, i, c);
2142
+ const next = skipString2(content, i, c);
2012
2143
  advanceLines(i, next);
2013
2144
  i = next;
2014
2145
  continue;
@@ -2068,7 +2199,7 @@ function collectJSXAncestry(content) {
2068
2199
  }
2069
2200
  return out2;
2070
2201
  }
2071
- function skipString(content, start, quote) {
2202
+ function skipString2(content, start, quote) {
2072
2203
  const N = content.length;
2073
2204
  let i = start + 1;
2074
2205
  while (i < N) {
@@ -2098,7 +2229,7 @@ function skipTemplate(content, start) {
2098
2229
  while (i < N && depth > 0) {
2099
2230
  const cj = content[i];
2100
2231
  if (cj === '"' || cj === "'") {
2101
- i = skipString(content, i, cj);
2232
+ i = skipString2(content, i, cj);
2102
2233
  continue;
2103
2234
  }
2104
2235
  if (cj === "`") {
@@ -2121,7 +2252,7 @@ function findTagEnd(content, start) {
2121
2252
  while (i < N) {
2122
2253
  const c = content[i];
2123
2254
  if (c === '"' || c === "'") {
2124
- i = skipString(content, i, c);
2255
+ i = skipString2(content, i, c);
2125
2256
  continue;
2126
2257
  }
2127
2258
  if (c === "`") {
@@ -2134,7 +2265,7 @@ function findTagEnd(content, start) {
2134
2265
  while (i < N && depth > 0) {
2135
2266
  const cj = content[i];
2136
2267
  if (cj === '"' || cj === "'") {
2137
- i = skipString(content, i, cj);
2268
+ i = skipString2(content, i, cj);
2138
2269
  continue;
2139
2270
  }
2140
2271
  if (cj === "`") {
@@ -2153,7 +2284,7 @@ function findTagEnd(content, start) {
2153
2284
  return -1;
2154
2285
  }
2155
2286
 
2156
- // src/scan/extract.ts
2287
+ // src/scanner/scan/extract.ts
2157
2288
  var JSDOC_BLOCK = /\/\*\*([\s\S]*?)\*\//g;
2158
2289
  function lineAt(content, index) {
2159
2290
  let line = 1;
@@ -2256,7 +2387,7 @@ function extractOne(file) {
2256
2387
  return annotations;
2257
2388
  }
2258
2389
 
2259
- // src/scan/git.ts
2390
+ // src/scanner/scan/git.ts
2260
2391
  var import_node_child_process = require("child_process");
2261
2392
  function runGit(args, cwd) {
2262
2393
  try {
@@ -2288,10 +2419,10 @@ function parseGitHubRef(ref) {
2288
2419
  return m ? m[1] : null;
2289
2420
  }
2290
2421
 
2291
- // src/scan/resolve.ts
2422
+ // src/scanner/scan/resolve.ts
2292
2423
  var path6 = __toESM(require("path"), 1);
2293
2424
 
2294
- // src/scan/routes.ts
2425
+ // src/scanner/scan/routes.ts
2295
2426
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
2296
2427
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
2297
2428
  var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
@@ -2357,7 +2488,7 @@ function pathToId(routePath) {
2357
2488
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2358
2489
  }
2359
2490
 
2360
- // src/scan/walk.ts
2491
+ // src/scanner/scan/walk.ts
2361
2492
  var fs4 = __toESM(require("fs"), 1);
2362
2493
  var path5 = __toESM(require("path"), 1);
2363
2494
  var DEFAULT_INCLUDES = ["**/*.{ts,tsx,js,jsx,mjs,cjs}"];
@@ -2482,7 +2613,7 @@ function* walkDir(root, dir) {
2482
2613
  }
2483
2614
  }
2484
2615
 
2485
- // src/scan/resolve.ts
2616
+ // src/scanner/scan/resolve.ts
2486
2617
  var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
2487
2618
  "element",
2488
2619
  "region",
@@ -2789,6 +2920,21 @@ function resolve3(ctx) {
2789
2920
  };
2790
2921
  registry.add(region);
2791
2922
  }
2923
+ for (const ef of ctx.extracted) {
2924
+ const exp = exportFor(ef.file.displayPath, "region");
2925
+ if (!exp) continue;
2926
+ if (exp.id === false) continue;
2927
+ if (typeof exp.id === "string" && !registry.get("region", exp.id)) {
2928
+ const meta = buildMetaFromExport(exp);
2929
+ const region = {
2930
+ kind: "region",
2931
+ id: exp.id,
2932
+ loc: { file: ef.file.displayPath, line: exp.loc.line },
2933
+ ...meta ? { meta } : {}
2934
+ };
2935
+ registry.add(region);
2936
+ }
2937
+ }
2792
2938
  const primitiveConventions = conventions.primitives;
2793
2939
  for (const ef of ctx.extracted) {
2794
2940
  const file = ef.file.displayPath;
@@ -2967,7 +3113,7 @@ function dedupe(arr) {
2967
3113
  return Array.from(new Set(arr));
2968
3114
  }
2969
3115
 
2970
- // src/scan/pipeline.ts
3116
+ // src/scanner/scan/pipeline.ts
2971
3117
  function runScan(opts = {}) {
2972
3118
  const cwd = opts.cwd ?? process.cwd();
2973
3119
  const configs = opts.configs ?? discover({ cwd });
@@ -3034,12 +3180,12 @@ function runOne(dc, opts) {
3034
3180
  outputPath
3035
3181
  };
3036
3182
  }
3037
- function writeScanResult(result) {
3038
- fs5.mkdirSync(path7.dirname(result.outputPath), { recursive: true });
3039
- fs5.writeFileSync(result.outputPath, result.generated, "utf8");
3183
+ function writeScanResult(result2) {
3184
+ fs5.mkdirSync(path7.dirname(result2.outputPath), { recursive: true });
3185
+ fs5.writeFileSync(result2.outputPath, result2.generated, "utf8");
3040
3186
  }
3041
3187
 
3042
- // src/scan/scaffold.ts
3188
+ // src/scanner/scan/scaffold.ts
3043
3189
  var fs6 = __toESM(require("fs"), 1);
3044
3190
  var path8 = __toESM(require("path"), 1);
3045
3191
  function scaffoldWidgetSpec(opts) {
@@ -3102,8 +3248,8 @@ function renderSpec(args) {
3102
3248
  return lines.join("\n");
3103
3249
  }
3104
3250
 
3105
- // src/scan/cli.ts
3106
- function parseFlags2(args) {
3251
+ // src/scanner/cli/parse-args.ts
3252
+ function parseArgs(args) {
3107
3253
  const positional = [];
3108
3254
  const flags = {};
3109
3255
  for (let i = 0; i < args.length; i++) {
@@ -3127,13 +3273,15 @@ function parseFlags2(args) {
3127
3273
  }
3128
3274
  return { positional, flags };
3129
3275
  }
3276
+
3277
+ // src/scanner/scan/cli.ts
3130
3278
  async function run(opts) {
3131
3279
  const cwd = opts.cwd ?? process.cwd();
3132
- const { positional, flags } = parseFlags2(opts.argv);
3133
- const command = positional[0] ?? "help";
3280
+ const { positional, flags } = parseArgs(opts.argv);
3281
+ const command2 = positional[0] ?? "help";
3134
3282
  const writer = createWriter();
3135
3283
  try {
3136
- switch (command) {
3284
+ switch (command2) {
3137
3285
  case "help":
3138
3286
  case "--help":
3139
3287
  case "-h":
@@ -3146,16 +3294,16 @@ async function run(opts) {
3146
3294
  case "scaffold":
3147
3295
  return runScaffold(cwd, positional.slice(1), flags, writer);
3148
3296
  case "ai": {
3149
- const result = await runAiCommand({
3297
+ const result2 = await runAiCommand({
3150
3298
  cwd,
3151
3299
  argv: opts.argv.slice(1)
3152
3300
  });
3153
- if (result.stdout) writer.out(result.stdout.replace(/\n$/, ""));
3154
- if (result.stderr) writer.err(result.stderr.replace(/\n$/, ""));
3155
- return writer.result(result.exitCode);
3301
+ if (result2.stdout) writer.out(result2.stdout.replace(/\n$/, ""));
3302
+ if (result2.stderr) writer.err(result2.stderr.replace(/\n$/, ""));
3303
+ return writer.result(result2.exitCode);
3156
3304
  }
3157
3305
  default:
3158
- writer.err(`Unknown command: ${command}`);
3306
+ writer.err(`Unknown command: ${command2}`);
3159
3307
  writer.err(helpText2());
3160
3308
  return writer.result(1);
3161
3309
  }
@@ -3173,6 +3321,10 @@ function helpText2() {
3173
3321
  " scan [flags] Run the scanner pipeline",
3174
3322
  " scaffold widget <id> Emit a Playwright spec from a widget's acceptance",
3175
3323
  " ai <install|uninstall|providers> Manage AI assistant integrations",
3324
+ " api <METHOD> <PATH> Call the uidex API",
3325
+ " api --list Show available API routes",
3326
+ " api login Authenticate via browser",
3327
+ " api login --token <tok> Store an auth token directly",
3176
3328
  "",
3177
3329
  "Flags:",
3178
3330
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",
@@ -3275,17 +3427,17 @@ function runScaffold(cwd, args, flags, w) {
3275
3427
  const widget = r.registry.get("widget", id);
3276
3428
  if (!widget) continue;
3277
3429
  const outDir = path9.resolve(r.configDir, "e2e");
3278
- const result = scaffoldWidgetSpec({
3430
+ const result2 = scaffoldWidgetSpec({
3279
3431
  registry: r.registry,
3280
3432
  widgetId: id,
3281
3433
  outDir,
3282
3434
  force: Boolean(flags.force)
3283
3435
  });
3284
- if (result.skipped) {
3285
- w.err(result.reason ?? "skipped");
3436
+ if (result2.skipped) {
3437
+ w.err(result2.reason ?? "skipped");
3286
3438
  return w.result(1);
3287
3439
  }
3288
- w.out(`Wrote ${result.outputPath}`);
3440
+ w.out(`Wrote ${result2.outputPath}`);
3289
3441
  return w.result(0);
3290
3442
  }
3291
3443
  w.err(`Widget "${id}" not found in registry`);
@@ -3307,12 +3459,884 @@ function createWriter() {
3307
3459
  };
3308
3460
  }
3309
3461
 
3310
- // src/cli/cli.ts
3311
- run({ argv: process.argv.slice(2) }).then(
3312
- (result) => {
3313
- if (result.stdout) process.stdout.write(result.stdout);
3314
- if (result.stderr) process.stderr.write(result.stderr);
3315
- process.exit(result.exitCode);
3462
+ // src/scanner/cli/auth.ts
3463
+ function createMemoryTokenStorage(initial) {
3464
+ let token = initial ?? null;
3465
+ return {
3466
+ get: () => token,
3467
+ set: (t) => {
3468
+ token = t;
3469
+ },
3470
+ clear: () => {
3471
+ token = null;
3472
+ }
3473
+ };
3474
+ }
3475
+ function createFileTokenStorage(options) {
3476
+ const nodeFs = require("fs");
3477
+ const nodePath = require("path");
3478
+ const file = options.path;
3479
+ function read() {
3480
+ if (!nodeFs.existsSync(file)) return null;
3481
+ try {
3482
+ const raw = nodeFs.readFileSync(file, "utf8");
3483
+ const parsed = JSON.parse(raw);
3484
+ if (parsed && typeof parsed === "object") {
3485
+ return parsed;
3486
+ }
3487
+ } catch {
3488
+ return null;
3489
+ }
3490
+ return null;
3491
+ }
3492
+ return {
3493
+ get: () => read()?.token ?? null,
3494
+ set: (token) => {
3495
+ nodeFs.mkdirSync(nodePath.dirname(file), { recursive: true });
3496
+ nodeFs.writeFileSync(file, JSON.stringify({ token }, null, 2));
3497
+ },
3498
+ clear: () => {
3499
+ if (nodeFs.existsSync(file)) nodeFs.unlinkSync(file);
3500
+ }
3501
+ };
3502
+ }
3503
+ function defaultTokenPath() {
3504
+ const os = require("os");
3505
+ const path10 = require("path");
3506
+ return path10.join(os.homedir(), ".uidex", "cloud-token.json");
3507
+ }
3508
+
3509
+ // src/scanner/cli/http.ts
3510
+ function createHttpClient(options) {
3511
+ const baseUrl = options.baseUrl.replace(/\/$/, "");
3512
+ async function request(path10, opts = {}) {
3513
+ let url = `${baseUrl}${path10.startsWith("/") ? path10 : `/${path10}`}`;
3514
+ if (opts.query) {
3515
+ url += (url.includes("?") ? "&" : "?") + opts.query;
3516
+ }
3517
+ const headers = {
3518
+ Accept: "application/json",
3519
+ ...opts.headers
3520
+ };
3521
+ const token = options.token();
3522
+ if (token) headers.Authorization = `Bearer ${token}`;
3523
+ if (opts.body !== void 0) {
3524
+ headers["Content-Type"] ??= "application/json";
3525
+ }
3526
+ const res = await fetch(url, {
3527
+ method: opts.method ?? (opts.body ? "POST" : "GET"),
3528
+ headers,
3529
+ body: opts.body
3530
+ });
3531
+ const raw = await res.text();
3532
+ let body = raw;
3533
+ try {
3534
+ body = JSON.parse(raw);
3535
+ } catch {
3536
+ }
3537
+ return { status: res.status, body, raw };
3538
+ }
3539
+ return { request };
3540
+ }
3541
+
3542
+ // src/scanner/cli/json-highlight.ts
3543
+ var RESET = "\x1B[0m";
3544
+ var BOLD = "\x1B[1m";
3545
+ var DIM = "\x1B[2m";
3546
+ var RED = "\x1B[31m";
3547
+ var CYAN = "\x1B[36m";
3548
+ var GREEN = "\x1B[32m";
3549
+ var YELLOW = "\x1B[33m";
3550
+ var MAGENTA = "\x1B[35m";
3551
+ function highlightJson(value, color) {
3552
+ const json = JSON.stringify(value, null, 2);
3553
+ if (!color) return json;
3554
+ return json.replace(
3555
+ /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g,
3556
+ (match, key, str, literal, num) => {
3557
+ if (key) return `${CYAN}${key}${RESET}:`;
3558
+ if (str) return `${GREEN}${str}${RESET}`;
3559
+ if (literal) return `${MAGENTA}${literal}${RESET}`;
3560
+ if (num) return `${YELLOW}${num}${RESET}`;
3561
+ return match;
3562
+ }
3563
+ );
3564
+ }
3565
+ function formatStatus(status, color) {
3566
+ if (!color) return `HTTP ${status}`;
3567
+ const code = status >= 400 ? RED : status >= 300 ? YELLOW : GREEN;
3568
+ return `${code}HTTP ${status}${RESET}`;
3569
+ }
3570
+ function boldText(text, color) {
3571
+ if (!color) return text;
3572
+ return `${BOLD}${text}${RESET}`;
3573
+ }
3574
+ function dimText(text, color) {
3575
+ if (!color) return text;
3576
+ return `${DIM}${text}${RESET}`;
3577
+ }
3578
+
3579
+ // src/scanner/cli/api-routes.gen.ts
3580
+ var API_ROUTES = [
3581
+ {
3582
+ "method": "POST",
3583
+ "path": "/api/ingest",
3584
+ "operationId": "submitReport",
3585
+ "summary": "Submit feedback from the SDK.",
3586
+ "tag": "Ingest",
3587
+ "params": []
3588
+ },
3589
+ {
3590
+ "method": "GET",
3591
+ "path": "/api/ingest/config",
3592
+ "operationId": "getIngestConfig",
3593
+ "summary": "Get integration configuration for the project.",
3594
+ "tag": "Ingest",
3595
+ "params": []
3596
+ },
3597
+ {
3598
+ "method": "GET",
3599
+ "path": "/api/ingest/reports",
3600
+ "operationId": "listIngestReports",
3601
+ "summary": "List feedback for the project (SDK view).",
3602
+ "tag": "Ingest",
3603
+ "params": []
3604
+ },
3605
+ {
3606
+ "method": "GET",
3607
+ "path": "/api/ingest/pins",
3608
+ "operationId": "listPins",
3609
+ "summary": "List pins by route and/or entity.",
3610
+ "tag": "Ingest",
3611
+ "params": []
3612
+ },
3613
+ {
3614
+ "method": "POST",
3615
+ "path": "/api/ingest/pins/archive",
3616
+ "operationId": "archivePin",
3617
+ "summary": "Archive a pin.",
3618
+ "tag": "Ingest",
3619
+ "params": []
3620
+ },
3621
+ {
3622
+ "method": "POST",
3623
+ "path": "/api/cli-auth/authorize",
3624
+ "operationId": "authorizeCli",
3625
+ "summary": "Authorize a CLI session by delivering a token.",
3626
+ "tag": "CLI Auth",
3627
+ "params": []
3628
+ },
3629
+ {
3630
+ "method": "GET",
3631
+ "path": "/api/organizations",
3632
+ "operationId": "listOrganizations",
3633
+ "summary": "List organizations for the current user.",
3634
+ "tag": "Organizations",
3635
+ "params": []
3636
+ },
3637
+ {
3638
+ "method": "POST",
3639
+ "path": "/api/organizations",
3640
+ "operationId": "createOrganization",
3641
+ "summary": "Create an organization.",
3642
+ "tag": "Organizations",
3643
+ "params": []
3644
+ },
3645
+ {
3646
+ "method": "POST",
3647
+ "path": "/api/organizations/switch",
3648
+ "operationId": "switchOrganization",
3649
+ "summary": "Switch the active organization.",
3650
+ "tag": "Organizations",
3651
+ "params": []
3652
+ },
3653
+ {
3654
+ "method": "GET",
3655
+ "path": "/api/organizations/{orgId}",
3656
+ "operationId": "getOrganization",
3657
+ "summary": "Get organization details.",
3658
+ "tag": "Organizations",
3659
+ "params": [
3660
+ "orgId"
3661
+ ]
3662
+ },
3663
+ {
3664
+ "method": "PATCH",
3665
+ "path": "/api/organizations/{orgId}",
3666
+ "operationId": "updateOrganization",
3667
+ "summary": "Update an organization.",
3668
+ "tag": "Organizations",
3669
+ "params": [
3670
+ "orgId"
3671
+ ]
3672
+ },
3673
+ {
3674
+ "method": "DELETE",
3675
+ "path": "/api/organizations/{orgId}",
3676
+ "operationId": "deleteOrganization",
3677
+ "summary": "Delete an organization.",
3678
+ "tag": "Organizations",
3679
+ "params": [
3680
+ "orgId"
3681
+ ]
3682
+ },
3683
+ {
3684
+ "method": "GET",
3685
+ "path": "/api/organizations/{orgId}/members",
3686
+ "operationId": "listMembers",
3687
+ "summary": "List organization members.",
3688
+ "tag": "Members",
3689
+ "params": [
3690
+ "orgId"
3691
+ ]
3692
+ },
3693
+ {
3694
+ "method": "DELETE",
3695
+ "path": "/api/organizations/{orgId}/members",
3696
+ "operationId": "removeMember",
3697
+ "summary": "Remove a member from the organization.",
3698
+ "tag": "Members",
3699
+ "params": [
3700
+ "orgId"
3701
+ ]
3702
+ },
3703
+ {
3704
+ "method": "PATCH",
3705
+ "path": "/api/organizations/{orgId}/members/{memberId}",
3706
+ "operationId": "updateMember",
3707
+ "summary": "Update a member's role.",
3708
+ "tag": "Members",
3709
+ "params": [
3710
+ "orgId",
3711
+ "memberId"
3712
+ ]
3713
+ },
3714
+ {
3715
+ "method": "GET",
3716
+ "path": "/api/organizations/{orgId}/invitations",
3717
+ "operationId": "listInvitations",
3718
+ "summary": "List organization invitations.",
3719
+ "tag": "Invitations",
3720
+ "params": [
3721
+ "orgId"
3722
+ ]
3723
+ },
3724
+ {
3725
+ "method": "POST",
3726
+ "path": "/api/organizations/{orgId}/invitations",
3727
+ "operationId": "createInvitation",
3728
+ "summary": "Create an invitation link.",
3729
+ "tag": "Invitations",
3730
+ "params": [
3731
+ "orgId"
3732
+ ]
3733
+ },
3734
+ {
3735
+ "method": "DELETE",
3736
+ "path": "/api/organizations/{orgId}/invitations/{inviteId}",
3737
+ "operationId": "deleteInvitation",
3738
+ "summary": "Revoke an invitation.",
3739
+ "tag": "Invitations",
3740
+ "params": [
3741
+ "orgId",
3742
+ "inviteId"
3743
+ ]
3744
+ },
3745
+ {
3746
+ "method": "GET",
3747
+ "path": "/api/organizations/{orgId}/projects",
3748
+ "operationId": "listProjects",
3749
+ "summary": "List projects in an organization.",
3750
+ "tag": "Projects",
3751
+ "params": [
3752
+ "orgId"
3753
+ ]
3754
+ },
3755
+ {
3756
+ "method": "POST",
3757
+ "path": "/api/organizations/{orgId}/projects",
3758
+ "operationId": "createProject",
3759
+ "summary": "Create a project.",
3760
+ "tag": "Projects",
3761
+ "params": [
3762
+ "orgId"
3763
+ ]
3764
+ },
3765
+ {
3766
+ "method": "GET",
3767
+ "path": "/api/organizations/{orgId}/projects/{projectId}",
3768
+ "operationId": "getProject",
3769
+ "summary": "Get project details.",
3770
+ "tag": "Projects",
3771
+ "params": [
3772
+ "orgId",
3773
+ "projectId"
3774
+ ]
3775
+ },
3776
+ {
3777
+ "method": "PATCH",
3778
+ "path": "/api/organizations/{orgId}/projects/{projectId}",
3779
+ "operationId": "updateProject",
3780
+ "summary": "Update a project.",
3781
+ "tag": "Projects",
3782
+ "params": [
3783
+ "orgId",
3784
+ "projectId"
3785
+ ]
3786
+ },
3787
+ {
3788
+ "method": "DELETE",
3789
+ "path": "/api/organizations/{orgId}/projects/{projectId}",
3790
+ "operationId": "deleteProject",
3791
+ "summary": "Delete a project.",
3792
+ "tag": "Projects",
3793
+ "params": [
3794
+ "orgId",
3795
+ "projectId"
3796
+ ]
3797
+ },
3798
+ {
3799
+ "method": "GET",
3800
+ "path": "/api/projects/resolve",
3801
+ "operationId": "resolveProject",
3802
+ "summary": "Resolve an API key to its project and organization.",
3803
+ "tag": "Projects",
3804
+ "params": []
3805
+ },
3806
+ {
3807
+ "method": "GET",
3808
+ "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
3809
+ "operationId": "listApiKeys",
3810
+ "summary": "List API keys for a project.",
3811
+ "tag": "API Keys",
3812
+ "params": [
3813
+ "orgId",
3814
+ "projectId"
3815
+ ]
3816
+ },
3817
+ {
3818
+ "method": "POST",
3819
+ "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys",
3820
+ "operationId": "createApiKey",
3821
+ "summary": "Create an API key.",
3822
+ "tag": "API Keys",
3823
+ "params": [
3824
+ "orgId",
3825
+ "projectId"
3826
+ ]
3827
+ },
3828
+ {
3829
+ "method": "PATCH",
3830
+ "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
3831
+ "operationId": "revokeApiKey",
3832
+ "summary": "Revoke an API key.",
3833
+ "tag": "API Keys",
3834
+ "params": [
3835
+ "orgId",
3836
+ "projectId",
3837
+ "keyId"
3838
+ ]
3839
+ },
3840
+ {
3841
+ "method": "DELETE",
3842
+ "path": "/api/organizations/{orgId}/projects/{projectId}/api-keys/{keyId}",
3843
+ "operationId": "deleteApiKey",
3844
+ "summary": "Permanently delete an API key.",
3845
+ "tag": "API Keys",
3846
+ "params": [
3847
+ "orgId",
3848
+ "projectId",
3849
+ "keyId"
3850
+ ]
3851
+ },
3852
+ {
3853
+ "method": "GET",
3854
+ "path": "/api/organizations/{orgId}/projects/{projectId}/reports",
3855
+ "operationId": "listProjectReports",
3856
+ "summary": "List feedback for a project.",
3857
+ "tag": "Reports",
3858
+ "params": [
3859
+ "orgId",
3860
+ "projectId"
3861
+ ]
3862
+ },
3863
+ {
3864
+ "method": "GET",
3865
+ "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
3866
+ "operationId": "getReport",
3867
+ "summary": "Get feedback details.",
3868
+ "tag": "Reports",
3869
+ "params": [
3870
+ "orgId",
3871
+ "projectId",
3872
+ "reportId"
3873
+ ]
3874
+ },
3875
+ {
3876
+ "method": "PATCH",
3877
+ "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
3878
+ "operationId": "updateReport",
3879
+ "summary": "Update feedback status, priority, or assignment.",
3880
+ "tag": "Reports",
3881
+ "params": [
3882
+ "orgId",
3883
+ "projectId",
3884
+ "reportId"
3885
+ ]
3886
+ },
3887
+ {
3888
+ "method": "DELETE",
3889
+ "path": "/api/organizations/{orgId}/projects/{projectId}/feedback/{reportId}",
3890
+ "operationId": "deleteReport",
3891
+ "summary": "Delete a feedback record.",
3892
+ "tag": "Reports",
3893
+ "params": [
3894
+ "orgId",
3895
+ "projectId",
3896
+ "reportId"
3897
+ ]
3898
+ },
3899
+ {
3900
+ "method": "POST",
3901
+ "path": "/api/organizations/{orgId}/projects/{projectId}/reports/archive",
3902
+ "operationId": "bulkArchiveReport",
3903
+ "summary": "Archive multiple feedback records.",
3904
+ "tag": "Reports",
3905
+ "params": [
3906
+ "orgId",
3907
+ "projectId"
3908
+ ]
3909
+ },
3910
+ {
3911
+ "method": "GET",
3912
+ "path": "/api/organizations/{orgId}/integrations",
3913
+ "operationId": "listIntegrations",
3914
+ "summary": "List integrations for an organization.",
3915
+ "tag": "Integrations",
3916
+ "params": [
3917
+ "orgId"
3918
+ ]
3919
+ },
3920
+ {
3921
+ "method": "POST",
3922
+ "path": "/api/organizations/{orgId}/integrations",
3923
+ "operationId": "createIntegration",
3924
+ "summary": "Create an integration.",
3925
+ "tag": "Integrations",
3926
+ "params": [
3927
+ "orgId"
3928
+ ]
3929
+ },
3930
+ {
3931
+ "method": "GET",
3932
+ "path": "/api/organizations/{orgId}/integrations/{integrationId}",
3933
+ "operationId": "getIntegration",
3934
+ "summary": "Get integration details.",
3935
+ "tag": "Integrations",
3936
+ "params": [
3937
+ "orgId",
3938
+ "integrationId"
3939
+ ]
3940
+ },
3941
+ {
3942
+ "method": "DELETE",
3943
+ "path": "/api/organizations/{orgId}/integrations/{integrationId}",
3944
+ "operationId": "deleteIntegration",
3945
+ "summary": "Delete an integration.",
3946
+ "tag": "Integrations",
3947
+ "params": [
3948
+ "orgId",
3949
+ "integrationId"
3950
+ ]
3951
+ },
3952
+ {
3953
+ "method": "GET",
3954
+ "path": "/api/organizations/{orgId}/integrations/{integrationId}/targets",
3955
+ "operationId": "listIntegrationTargets",
3956
+ "summary": "List available targets (e.g. Jira projects/boards).",
3957
+ "tag": "Integrations",
3958
+ "params": [
3959
+ "orgId",
3960
+ "integrationId"
3961
+ ]
3962
+ },
3963
+ {
3964
+ "method": "POST",
3965
+ "path": "/api/organizations/{orgId}/integrations/{integrationId}/test",
3966
+ "operationId": "testIntegration",
3967
+ "summary": "Test integration connectivity.",
3968
+ "tag": "Integrations",
3969
+ "params": [
3970
+ "orgId",
3971
+ "integrationId"
3972
+ ]
3973
+ },
3974
+ {
3975
+ "method": "GET",
3976
+ "path": "/api/organizations/{orgId}/external-issues",
3977
+ "operationId": "listExternalIssues",
3978
+ "summary": "List external issues, optionally filtered by feedback.",
3979
+ "tag": "External Issues",
3980
+ "params": [
3981
+ "orgId"
3982
+ ]
3983
+ },
3984
+ {
3985
+ "method": "GET",
3986
+ "path": "/api/organizations/{orgId}/issue-drafts",
3987
+ "operationId": "listIssueDrafts",
3988
+ "summary": "List issue drafts for a project.",
3989
+ "tag": "Issue Drafts",
3990
+ "params": [
3991
+ "orgId"
3992
+ ]
3993
+ },
3994
+ {
3995
+ "method": "POST",
3996
+ "path": "/api/organizations/{orgId}/issue-drafts",
3997
+ "operationId": "createIssueDraft",
3998
+ "summary": "Manually create an issue draft.",
3999
+ "tag": "Issue Drafts",
4000
+ "params": [
4001
+ "orgId"
4002
+ ]
4003
+ },
4004
+ {
4005
+ "method": "PATCH",
4006
+ "path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
4007
+ "operationId": "updateIssueDraft",
4008
+ "summary": "Update an issue draft.",
4009
+ "tag": "Issue Drafts",
4010
+ "params": [
4011
+ "orgId",
4012
+ "draftId"
4013
+ ]
4014
+ },
4015
+ {
4016
+ "method": "DELETE",
4017
+ "path": "/api/organizations/{orgId}/issue-drafts/{draftId}",
4018
+ "operationId": "deleteIssueDraft",
4019
+ "summary": "Delete an issue draft.",
4020
+ "tag": "Issue Drafts",
4021
+ "params": [
4022
+ "orgId",
4023
+ "draftId"
4024
+ ]
4025
+ },
4026
+ {
4027
+ "method": "POST",
4028
+ "path": "/api/organizations/{orgId}/issue-drafts/{draftId}/submit",
4029
+ "operationId": "submitIssueDraft",
4030
+ "summary": "Submit a draft to the configured integration.",
4031
+ "tag": "Issue Drafts",
4032
+ "params": [
4033
+ "orgId",
4034
+ "draftId"
4035
+ ]
4036
+ },
4037
+ {
4038
+ "method": "POST",
4039
+ "path": "/api/organizations/{orgId}/projects/{projectId}/triage",
4040
+ "operationId": "runTriage",
4041
+ "summary": "Run LLM-powered triage on untriaged feedback.",
4042
+ "tag": "Triage",
4043
+ "params": [
4044
+ "orgId",
4045
+ "projectId"
4046
+ ]
4047
+ },
4048
+ {
4049
+ "method": "GET",
4050
+ "path": "/api/user/tokens",
4051
+ "operationId": "listUserTokens",
4052
+ "summary": "List personal access tokens.",
4053
+ "tag": "User Tokens",
4054
+ "params": []
4055
+ },
4056
+ {
4057
+ "method": "POST",
4058
+ "path": "/api/user/tokens",
4059
+ "operationId": "createUserToken",
4060
+ "summary": "Create a personal access token.",
4061
+ "tag": "User Tokens",
4062
+ "params": []
4063
+ },
4064
+ {
4065
+ "method": "DELETE",
4066
+ "path": "/api/user/tokens/{tokenId}",
4067
+ "operationId": "deleteUserToken",
4068
+ "summary": "Revoke and delete a personal access token.",
4069
+ "tag": "User Tokens",
4070
+ "params": [
4071
+ "tokenId"
4072
+ ]
4073
+ }
4074
+ ];
4075
+
4076
+ // src/scanner/cli/api.ts
4077
+ var METHODS = /* @__PURE__ */ new Set([
4078
+ "GET",
4079
+ "POST",
4080
+ "PUT",
4081
+ "PATCH",
4082
+ "DELETE",
4083
+ "HEAD",
4084
+ "OPTIONS"
4085
+ ]);
4086
+ var HELP = `uidex api \u2014 call the uidex API
4087
+
4088
+ Usage:
4089
+ uidex api <METHOD> <PATH> [flags] Make an API request
4090
+ uidex api --list [--tag <tag>] List available routes
4091
+ uidex api login Authenticate via browser
4092
+ uidex api login --token <tok> Store an auth token directly
4093
+ uidex api status Check auth state
4094
+
4095
+ Flags:
4096
+ --body <json> Request body (JSON string)
4097
+ --query <params> Query string (key=val&key2=val2)
4098
+ --base-url <url> Override the API base URL
4099
+ --token <tok> Use a specific token (instead of stored)
4100
+ --raw Disable JSON pretty-printing
4101
+ --no-color Disable colored output
4102
+ --list List available API routes
4103
+ --tag <tag> Filter --list by tag
4104
+ --json Emit --list as JSON
4105
+ `;
4106
+ async function runApiCommand(opts) {
4107
+ const { positional, flags } = parseArgs(opts.argv);
4108
+ const stdout = [];
4109
+ const stderr = [];
4110
+ const color = opts.color ?? (flags["no-color"] ? false : process.stdout.isTTY ?? false);
4111
+ const ctx = { flags, opts, color, stdout, stderr };
4112
+ const sub = positional[0];
4113
+ if (!sub && !flags.list) {
4114
+ stdout.push(HELP);
4115
+ return result(0, stdout, stderr);
4116
+ }
4117
+ if (flags.list) return listRoutes(ctx);
4118
+ if (sub === "help" || sub === "--help" || sub === "-h") {
4119
+ stdout.push(HELP);
4120
+ return result(0, stdout, stderr);
4121
+ }
4122
+ if (sub === "login") return runLogin(ctx);
4123
+ if (sub === "status") return runStatus(ctx);
4124
+ const method = sub?.toUpperCase();
4125
+ const path10 = positional[1];
4126
+ if (!method || !METHODS.has(method)) {
4127
+ stderr.push(`Unknown command or method: ${sub}`);
4128
+ stderr.push(HELP);
4129
+ return result(1, stdout, stderr);
4130
+ }
4131
+ if (!path10) {
4132
+ stderr.push("Missing path. Usage: uidex api GET /api/organizations");
4133
+ return result(1, stdout, stderr);
4134
+ }
4135
+ return runRequest(ctx, method, path10);
4136
+ }
4137
+ function listRoutes(ctx) {
4138
+ const { flags, color, stdout, stderr } = ctx;
4139
+ let routes = API_ROUTES;
4140
+ const tagFilter = flags.tag;
4141
+ if (typeof tagFilter === "string") {
4142
+ routes = routes.filter(
4143
+ (r) => r.tag.toLowerCase() === tagFilter.toLowerCase()
4144
+ );
4145
+ }
4146
+ if (flags.json) {
4147
+ stdout.push(JSON.stringify(routes, null, 2));
4148
+ return result(0, stdout, stderr);
4149
+ }
4150
+ const grouped = /* @__PURE__ */ new Map();
4151
+ for (const r of routes) {
4152
+ const tag = r.tag || "Other";
4153
+ let list = grouped.get(tag);
4154
+ if (!list) {
4155
+ list = [];
4156
+ grouped.set(tag, list);
4157
+ }
4158
+ list.push(r);
4159
+ }
4160
+ const methodPad = 7;
4161
+ for (const [tag, tagRoutes] of grouped) {
4162
+ stdout.push("");
4163
+ stdout.push(boldText(tag, color));
4164
+ for (const r of tagRoutes) {
4165
+ const m = r.method.padEnd(methodPad);
4166
+ const desc = r.summary ? dimText(` ${r.operationId} \u2014 ${r.summary}`, color) : dimText(` ${r.operationId}`, color);
4167
+ stdout.push(` ${m} ${r.path}${desc}`);
4168
+ }
4169
+ }
4170
+ stdout.push("");
4171
+ return result(0, stdout, stderr);
4172
+ }
4173
+ async function runLogin(ctx) {
4174
+ const { flags, opts, color, stdout, stderr } = ctx;
4175
+ const token = flags.token;
4176
+ if (typeof token === "string" && token.length > 0) {
4177
+ const storage = resolveTokenStorage(opts);
4178
+ storage.set(token);
4179
+ stdout.push("Token stored.");
4180
+ return result(0, stdout, stderr);
4181
+ }
4182
+ return runBrowserLogin(ctx);
4183
+ }
4184
+ async function runBrowserLogin(ctx) {
4185
+ const { flags, opts, color, stdout, stderr } = ctx;
4186
+ const http = await import("http");
4187
+ const crypto = await import("crypto");
4188
+ const { exec } = await import("child_process");
4189
+ const baseUrl = resolveBaseUrl(flags, opts);
4190
+ const code = crypto.randomBytes(3).toString("hex").toUpperCase();
4191
+ return new Promise((resolve7) => {
4192
+ let settled = false;
4193
+ const settle = (exitCode) => {
4194
+ if (settled) return;
4195
+ settled = true;
4196
+ server.close();
4197
+ clearTimeout(timeout);
4198
+ resolve7(result(exitCode, stdout, stderr));
4199
+ };
4200
+ const timeout = setTimeout(() => {
4201
+ stderr.push("Timed out waiting for browser authorization.");
4202
+ settle(1);
4203
+ }, 12e4);
4204
+ const server = http.createServer((req, res) => {
4205
+ if (!req.url?.startsWith("/callback")) {
4206
+ res.writeHead(404);
4207
+ res.end();
4208
+ return;
4209
+ }
4210
+ const url = new URL(req.url, `http://127.0.0.1`);
4211
+ const receivedToken = url.searchParams.get("token");
4212
+ const receivedCode = url.searchParams.get("code");
4213
+ if (receivedCode !== code) {
4214
+ res.writeHead(400, { "Content-Type": "application/json" });
4215
+ res.end(JSON.stringify({ error: "Code mismatch" }));
4216
+ stderr.push("Received callback with mismatched code. Aborting.");
4217
+ settle(1);
4218
+ return;
4219
+ }
4220
+ if (!receivedToken) {
4221
+ res.writeHead(400, { "Content-Type": "application/json" });
4222
+ res.end(JSON.stringify({ error: "Missing token" }));
4223
+ stderr.push("Received callback without token. Aborting.");
4224
+ settle(1);
4225
+ return;
4226
+ }
4227
+ res.writeHead(200, { "Content-Type": "application/json" });
4228
+ res.end(JSON.stringify({ ok: true }));
4229
+ const storage = resolveTokenStorage(opts);
4230
+ storage.set(receivedToken);
4231
+ stdout.push(dimText("Authenticated successfully.", color));
4232
+ settle(0);
4233
+ });
4234
+ server.listen(0, "127.0.0.1", () => {
4235
+ const addr = server.address();
4236
+ if (!addr || typeof addr === "string") {
4237
+ stderr.push("Failed to start local server.");
4238
+ settle(1);
4239
+ return;
4240
+ }
4241
+ const port = addr.port;
4242
+ const authUrl = `${baseUrl}/cli-auth?code=${encodeURIComponent(code)}&port=${port}`;
4243
+ stdout.push(dimText("Opening browser to authorize...", color));
4244
+ stdout.push("");
4245
+ stdout.push(` Confirmation code: ${boldText(code, color)}`);
4246
+ stdout.push("");
4247
+ stdout.push(
4248
+ dimText("If the browser doesn't open, visit this URL manually:", color)
4249
+ );
4250
+ stdout.push(` ${authUrl}`);
4251
+ stdout.push("");
4252
+ process.stdout.write(result(0, stdout, stderr).stdout);
4253
+ stdout.length = 0;
4254
+ openBrowser(authUrl);
4255
+ });
4256
+ server.on("error", (err2) => {
4257
+ stderr.push(`Failed to start local server: ${err2.message}`);
4258
+ settle(1);
4259
+ });
4260
+ });
4261
+ }
4262
+ function openBrowser(url) {
4263
+ const { exec } = require("child_process");
4264
+ const platform = process.platform;
4265
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
4266
+ exec(`${cmd} ${JSON.stringify(url)}`);
4267
+ }
4268
+ function runStatus(ctx) {
4269
+ const { flags, opts, stdout, stderr } = ctx;
4270
+ const storage = resolveTokenStorage(opts);
4271
+ const baseUrl = resolveBaseUrl(flags, opts);
4272
+ const token = storage.get();
4273
+ stdout.push(`baseUrl: ${baseUrl}`);
4274
+ stdout.push(`auth: ${token ? "authenticated" : "not authenticated"}`);
4275
+ return result(token ? 0 : 1, stdout, stderr);
4276
+ }
4277
+ async function runRequest(ctx, method, path10) {
4278
+ const { flags, opts, color, stdout, stderr } = ctx;
4279
+ const tokenFromFlag = flags.token;
4280
+ const token = typeof tokenFromFlag === "string" ? tokenFromFlag : resolveTokenStorage(opts).get();
4281
+ if (!token) {
4282
+ stderr.push("Not authenticated. Run: uidex api login --token <value>");
4283
+ return result(1, stdout, stderr);
4284
+ }
4285
+ const baseUrl = resolveBaseUrl(flags, opts);
4286
+ const http = opts.http ?? createHttpClient({ baseUrl, token: () => token });
4287
+ const bodyStr = typeof flags.body === "string" ? flags.body : void 0;
4288
+ if (bodyStr !== void 0) {
4289
+ try {
4290
+ JSON.parse(bodyStr);
4291
+ } catch {
4292
+ stderr.push("Invalid JSON in --body");
4293
+ return result(1, stdout, stderr);
4294
+ }
4295
+ }
4296
+ const res = await http.request(path10, {
4297
+ method,
4298
+ body: bodyStr,
4299
+ query: typeof flags.query === "string" ? flags.query : void 0
4300
+ });
4301
+ const isSuccess = res.status >= 200 && res.status < 300;
4302
+ if (!isSuccess) {
4303
+ stderr.push(formatStatus(res.status, color));
4304
+ }
4305
+ if (res.body !== void 0 && res.body !== null && res.body !== "") {
4306
+ if (flags.raw) {
4307
+ stdout.push(res.raw);
4308
+ } else {
4309
+ stdout.push(highlightJson(res.body, color));
4310
+ }
4311
+ }
4312
+ return result(isSuccess ? 0 : 1, stdout, stderr);
4313
+ }
4314
+ function resolveTokenStorage(opts) {
4315
+ if (opts.tokenStorage) return opts.tokenStorage;
4316
+ const envToken = opts.env?.UIDEX_TOKEN;
4317
+ if (envToken) return createMemoryTokenStorage(envToken);
4318
+ return createFileTokenStorage({ path: defaultTokenPath() });
4319
+ }
4320
+ function resolveBaseUrl(flags, opts) {
4321
+ return (typeof flags["base-url"] === "string" ? flags["base-url"] : void 0) ?? opts.baseUrl ?? opts.env?.UIDEX_API_URL ?? "https://app.uidex.dev";
4322
+ }
4323
+ function result(exitCode, stdout, stderr) {
4324
+ return {
4325
+ exitCode,
4326
+ stdout: stdout.join("\n") + (stdout.length ? "\n" : ""),
4327
+ stderr: stderr.join("\n") + (stderr.length ? "\n" : "")
4328
+ };
4329
+ }
4330
+
4331
+ // src/scanner/cli/cli.ts
4332
+ var argv = process.argv.slice(2);
4333
+ var command = argv[0];
4334
+ var resultP = command === "api" ? runApiCommand({ argv: argv.slice(1) }) : run({ argv });
4335
+ resultP.then(
4336
+ (result2) => {
4337
+ if (result2.stdout) process.stdout.write(result2.stdout);
4338
+ if (result2.stderr) process.stderr.write(result2.stderr);
4339
+ process.exit(result2.exitCode);
3316
4340
  },
3317
4341
  (error) => {
3318
4342
  process.stderr.write(