uidex 0.4.0 → 0.5.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 (42) hide show
  1. package/dist/cli/cli.cjs +996 -73
  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 +620 -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 +624 -469
  14. package/dist/headless/index.js.map +1 -1
  15. package/dist/index.cjs +4255 -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 +4277 -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 +4298 -2908
  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 +4334 -2924
  34. package/dist/react/index.js.map +1 -1
  35. package/dist/scan/index.cjs +91 -40
  36. package/dist/scan/index.cjs.map +1 -1
  37. package/dist/scan/index.d.cts +26 -0
  38. package/dist/scan/index.d.ts +26 -0
  39. package/dist/scan/index.js +90 -39
  40. package/dist/scan/index.js.map +1 -1
  41. package/package.json +23 -22
  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 {
@@ -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 {
@@ -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,7 +350,7 @@ 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",
@@ -368,7 +368,13 @@ var ALLOWED_FIELDS = {
368
368
  "acceptance",
369
369
  "description"
370
370
  ]),
371
- feature: /* @__PURE__ */ new Set(["feature", "name", "acceptance", "description"]),
371
+ feature: /* @__PURE__ */ new Set([
372
+ "feature",
373
+ "name",
374
+ "features",
375
+ "acceptance",
376
+ "description"
377
+ ]),
372
378
  primitive: /* @__PURE__ */ new Set(["primitive", "name", "description"]),
373
379
  widget: /* @__PURE__ */ new Set(["widget", "name", "acceptance", "description"]),
374
380
  flow: /* @__PURE__ */ new Set(["flow", "notFlow", "name", "description"])
@@ -1104,7 +1110,7 @@ function buildMetadata(value, file, headerPos, diagnostics) {
1104
1110
  line: pos.line
1105
1111
  });
1106
1112
  }
1107
- const features = kind === "page" ? readStringArrayField(byKey, "features") : void 0;
1113
+ const features = kind === "page" || kind === "feature" ? readStringArrayField(byKey, "features") : void 0;
1108
1114
  const widgets = kind === "page" ? readStringArrayField(byKey, "widgets") : void 0;
1109
1115
  const notFlow = kind === "flow" && discriminator === "notFlow" ? true : void 0;
1110
1116
  const metadata = {
@@ -1199,7 +1205,7 @@ function posAt(content, offset) {
1199
1205
  return { offset, line, column: offset - lineStart + 1 };
1200
1206
  }
1201
1207
 
1202
- // src/scan/jsx-ancestry.ts
1208
+ // src/scanner/scan/jsx-ancestry.ts
1203
1209
  var DATA_ATTR_RE = /\bdata-uidex(?:-(region|widget|primitive))?\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
1204
1210
  function parseDataAttrs(tagSource) {
1205
1211
  if (!tagSource.includes("data-uidex")) return [];
@@ -1388,7 +1394,7 @@ function findTagEnd(content, start) {
1388
1394
  return -1;
1389
1395
  }
1390
1396
 
1391
- // src/scan/extract.ts
1397
+ // src/scanner/scan/extract.ts
1392
1398
  var JSDOC_BLOCK = /\/\*\*([\s\S]*?)\*\//g;
1393
1399
  function lineAt(content, index) {
1394
1400
  let line = 1;
@@ -1491,10 +1497,10 @@ function extractOne(file) {
1491
1497
  return annotations;
1492
1498
  }
1493
1499
 
1494
- // src/scan/resolve.ts
1500
+ // src/scanner/scan/resolve.ts
1495
1501
  import * as path3 from "path";
1496
1502
 
1497
- // src/entities/types.ts
1503
+ // src/shared/entities/types.ts
1498
1504
  var ENTITY_KINDS = [
1499
1505
  "route",
1500
1506
  "page",
@@ -1527,7 +1533,7 @@ function assertEntityKind(kind) {
1527
1533
  if (!KIND_SET.has(kind)) throw new UnknownEntityKindError(kind);
1528
1534
  }
1529
1535
 
1530
- // src/entities/registry.ts
1536
+ // src/shared/entities/registry.ts
1531
1537
  function emptyStore() {
1532
1538
  return {
1533
1539
  route: /* @__PURE__ */ new Map(),
@@ -1609,10 +1615,33 @@ function createRegistry() {
1609
1615
  return ids.has(entity.id);
1610
1616
  });
1611
1617
  };
1612
- return { add, get, list, query, byScope, touchedBy };
1618
+ const reports = /* @__PURE__ */ new Map();
1619
+ const reportsCbs = /* @__PURE__ */ new Set();
1620
+ const setReports = (kind, id, records) => {
1621
+ reports.set(`${kind}:${id}`, records);
1622
+ for (const cb of reportsCbs) cb();
1623
+ };
1624
+ const getReports = (kind, id) => reports.get(`${kind}:${id}`) ?? [];
1625
+ const listReportKeys = () => Array.from(reports.keys());
1626
+ const onReportsChange = (cb) => {
1627
+ reportsCbs.add(cb);
1628
+ return () => reportsCbs.delete(cb);
1629
+ };
1630
+ return {
1631
+ add,
1632
+ get,
1633
+ list,
1634
+ query,
1635
+ byScope,
1636
+ touchedBy,
1637
+ setReports,
1638
+ getReports,
1639
+ listReportKeys,
1640
+ onReportsChange
1641
+ };
1613
1642
  }
1614
1643
 
1615
- // src/scan/routes.ts
1644
+ // src/scanner/scan/routes.ts
1616
1645
  var PAGE_BASENAME = /^page\.(tsx|ts|jsx|js|mjs|cjs)$/;
1617
1646
  var PAGES_ROUTER_BASENAME = /\.(tsx|ts|jsx|js|mjs|cjs)$/;
1618
1647
  var ROUTE_BASENAME = /^route\.(tsx|ts|jsx|js|mjs|cjs)$/;
@@ -1678,7 +1707,7 @@ function pathToId(routePath) {
1678
1707
  return routePath.replace(/^\/+/, "").replace(/\[\.{3}([^\]]+)\]/g, "$1").replace(/\[([^\]]+)\]/g, "$1").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1679
1708
  }
1680
1709
 
1681
- // src/scan/resolve.ts
1710
+ // src/scanner/scan/resolve.ts
1682
1711
  var DOM_ATTR_KINDS = /* @__PURE__ */ new Set([
1683
1712
  "element",
1684
1713
  "region",
@@ -2163,7 +2192,7 @@ function dedupe(arr) {
2163
2192
  return Array.from(new Set(arr));
2164
2193
  }
2165
2194
 
2166
- // src/scan/audit.ts
2195
+ // src/scanner/scan/audit.ts
2167
2196
  import * as path4 from "path";
2168
2197
  var MARKER_FILENAMES = ["UIDEX_PAGE.md", "UIDEX_FEATURE.md"];
2169
2198
  function audit(opts) {
@@ -2318,6 +2347,15 @@ function audit(opts) {
2318
2347
  const primitives = registry.list("primitive");
2319
2348
  const byName = /* @__PURE__ */ new Map();
2320
2349
  for (const p2 of primitives) byName.set(p2.id, p2);
2350
+ const declaredFeatures = /* @__PURE__ */ new Map();
2351
+ for (const ef of extracted) {
2352
+ if (!ef.metadata) continue;
2353
+ for (const m of ef.metadata) {
2354
+ if (m.features && m.features.length > 0) {
2355
+ declaredFeatures.set(ef.file.displayPath, new Set(m.features));
2356
+ }
2357
+ }
2358
+ }
2321
2359
  for (const f of files) {
2322
2360
  const importRe = /import\s+(?:[^'"]+)\s+from\s+['"]([^'"]+)['"]/g;
2323
2361
  let m;
@@ -2332,14 +2370,19 @@ function audit(opts) {
2332
2370
  if (!scope) continue;
2333
2371
  const [kind, id] = scope.split(":");
2334
2372
  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
- });
2373
+ if (importerSegments.includes(id) && importerSegments.includes(kind + "s")) {
2374
+ continue;
2375
+ }
2376
+ if (kind === "feature" && importerSegments.includes(id)) continue;
2377
+ if (kind === "feature" && declaredFeatures.get(f.displayPath)?.has(id)) {
2378
+ continue;
2342
2379
  }
2380
+ diagnostics.push({
2381
+ code: "scope-leak",
2382
+ severity: "warning",
2383
+ message: `Primitive "${primitive.id}" is scoped to ${scope} but is imported from ${f.displayPath}`,
2384
+ file: f.displayPath
2385
+ });
2343
2386
  }
2344
2387
  }
2345
2388
  }
@@ -2513,7 +2556,7 @@ function stableReplacer(_key, value) {
2513
2556
  return value;
2514
2557
  }
2515
2558
 
2516
- // src/scan/emit.ts
2559
+ // src/scanner/scan/emit.ts
2517
2560
  function sortById(arr) {
2518
2561
  return [...arr].sort((a, b) => a.id.localeCompare(b.id));
2519
2562
  }
@@ -2637,6 +2680,7 @@ function emit(opts) {
2637
2680
  lines.push(" export interface Feature {");
2638
2681
  lines.push(" feature: FeatureId | false");
2639
2682
  lines.push(" name?: string");
2683
+ lines.push(" features?: readonly FeatureId[]");
2640
2684
  lines.push(" acceptance?: readonly string[]");
2641
2685
  lines.push(" description?: string");
2642
2686
  lines.push(" }");
@@ -2693,7 +2737,7 @@ function emit(opts) {
2693
2737
  return lines.join("\n");
2694
2738
  }
2695
2739
 
2696
- // src/scan/git.ts
2740
+ // src/scanner/scan/git.ts
2697
2741
  import { execSync } from "child_process";
2698
2742
  function runGit(args, cwd) {
2699
2743
  try {
@@ -2725,7 +2769,7 @@ function parseGitHubRef(ref) {
2725
2769
  return m ? m[1] : null;
2726
2770
  }
2727
2771
 
2728
- // src/scan/scaffold.ts
2772
+ // src/scanner/scan/scaffold.ts
2729
2773
  import * as fs3 from "fs";
2730
2774
  import * as path5 from "path";
2731
2775
  function scaffoldWidgetSpec(opts) {
@@ -2788,7 +2832,7 @@ function renderSpec(args) {
2788
2832
  return lines.join("\n");
2789
2833
  }
2790
2834
 
2791
- // src/scan/pipeline.ts
2835
+ // src/scanner/scan/pipeline.ts
2792
2836
  import * as fs4 from "fs";
2793
2837
  import * as path6 from "path";
2794
2838
  function runScan(opts = {}) {
@@ -2862,18 +2906,18 @@ function writeScanResult(result) {
2862
2906
  fs4.writeFileSync(result.outputPath, result.generated, "utf8");
2863
2907
  }
2864
2908
 
2865
- // src/scan/cli.ts
2909
+ // src/scanner/scan/cli.ts
2866
2910
  import * as fs7 from "fs";
2867
2911
  import * as path9 from "path";
2868
2912
 
2869
- // src/scan/ai/index.ts
2913
+ // src/scanner/scan/ai/index.ts
2870
2914
  import * as p from "@clack/prompts";
2871
2915
 
2872
- // src/scan/ai/providers/claude.ts
2916
+ // src/scanner/scan/ai/providers/claude.ts
2873
2917
  import * as fs6 from "fs";
2874
2918
  import * as path8 from "path";
2875
2919
 
2876
- // src/scan/ai/templates.ts
2920
+ // src/scanner/scan/ai/templates.ts
2877
2921
  import * as fs5 from "fs";
2878
2922
  import * as path7 from "path";
2879
2923
  function templatePath(rel) {
@@ -2900,15 +2944,16 @@ function readTemplate(rel) {
2900
2944
  return fs5.readFileSync(templatePath(rel), "utf8");
2901
2945
  }
2902
2946
 
2903
- // src/scan/ai/providers/claude.ts
2947
+ // src/scanner/scan/ai/providers/claude.ts
2904
2948
  var CLAUDE_FILES = [
2905
2949
  { dest: ".claude/rules/uidex.md", template: "claude/rules.md" },
2906
- { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" }
2950
+ { dest: ".claude/commands/uidex/audit.md", template: "claude/audit.md" },
2951
+ { dest: ".claude/commands/uidex/api.md", template: "claude/api.md" }
2907
2952
  ];
2908
2953
  var claudeProvider = {
2909
2954
  id: "claude",
2910
2955
  label: "Claude Code",
2911
- description: "Adds .claude/rules/uidex.md and the /uidex:audit slash command.",
2956
+ description: "Adds .claude/rules/uidex.md, /uidex:audit, and /uidex:api slash commands.",
2912
2957
  async install({ cwd, force }) {
2913
2958
  const changes = [];
2914
2959
  for (const file of CLAUDE_FILES) {
@@ -2956,13 +3001,13 @@ function cleanupEmpty(dir) {
2956
3001
  }
2957
3002
  }
2958
3003
 
2959
- // src/scan/ai/providers/index.ts
3004
+ // src/scanner/scan/ai/providers/index.ts
2960
3005
  var PROVIDERS = [claudeProvider];
2961
3006
  function getProvider(id) {
2962
3007
  return PROVIDERS.find((p2) => p2.id === id);
2963
3008
  }
2964
3009
 
2965
- // src/scan/ai/index.ts
3010
+ // src/scanner/scan/ai/index.ts
2966
3011
  async function runAiCommand(opts) {
2967
3012
  const { cwd, argv } = opts;
2968
3013
  const sub = argv[0];
@@ -3073,8 +3118,8 @@ function err(exitCode, stderr) {
3073
3118
  return { exitCode, stdout: "", stderr };
3074
3119
  }
3075
3120
 
3076
- // src/scan/cli.ts
3077
- function parseFlags2(args) {
3121
+ // src/scanner/cli/parse-args.ts
3122
+ function parseArgs(args) {
3078
3123
  const positional = [];
3079
3124
  const flags = {};
3080
3125
  for (let i = 0; i < args.length; i++) {
@@ -3098,9 +3143,11 @@ function parseFlags2(args) {
3098
3143
  }
3099
3144
  return { positional, flags };
3100
3145
  }
3146
+
3147
+ // src/scanner/scan/cli.ts
3101
3148
  async function run(opts) {
3102
3149
  const cwd = opts.cwd ?? process.cwd();
3103
- const { positional, flags } = parseFlags2(opts.argv);
3150
+ const { positional, flags } = parseArgs(opts.argv);
3104
3151
  const command = positional[0] ?? "help";
3105
3152
  const writer = createWriter();
3106
3153
  try {
@@ -3144,6 +3191,10 @@ function helpText2() {
3144
3191
  " scan [flags] Run the scanner pipeline",
3145
3192
  " scaffold widget <id> Emit a Playwright spec from a widget's acceptance",
3146
3193
  " ai <install|uninstall|providers> Manage AI assistant integrations",
3194
+ " api <METHOD> <PATH> Call the uidex API",
3195
+ " api --list Show available API routes",
3196
+ " api login Authenticate via browser",
3197
+ " api login --token <tok> Store an auth token directly",
3147
3198
  "",
3148
3199
  "Flags:",
3149
3200
  " --check Verify the on-disk gen file matches a fresh scan; exit non-zero on drift (read-only)",