uidex 0.2.4 → 0.4.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 (65) hide show
  1. package/README.md +253 -353
  2. package/dist/cli/cli.cjs +3324 -0
  3. package/dist/cli/cli.cjs.map +1 -0
  4. package/dist/cloud/index.cjs +169 -0
  5. package/dist/cloud/index.cjs.map +1 -0
  6. package/dist/cloud/index.js +140 -0
  7. package/dist/cloud/index.js.map +1 -0
  8. package/dist/headless/index.cjs +4143 -0
  9. package/dist/headless/index.cjs.map +1 -0
  10. package/dist/headless/index.d.cts +220 -0
  11. package/dist/headless/index.d.ts +220 -0
  12. package/dist/headless/index.js +4130 -0
  13. package/dist/headless/index.js.map +1 -0
  14. package/dist/index.cjs +8704 -9883
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +968 -146
  17. package/dist/index.d.ts +968 -146
  18. package/dist/index.js +8327 -9492
  19. package/dist/index.js.map +1 -1
  20. package/dist/playwright/index.cjs +164 -24
  21. package/dist/playwright/index.cjs.map +1 -1
  22. package/dist/playwright/index.d.cts +30 -53
  23. package/dist/playwright/index.d.ts +30 -53
  24. package/dist/playwright/index.js +148 -21
  25. package/dist/playwright/index.js.map +1 -1
  26. package/dist/playwright/reporter.cjs +62 -28
  27. package/dist/playwright/reporter.cjs.map +1 -1
  28. package/dist/playwright/reporter.d.cts +24 -12
  29. package/dist/playwright/reporter.d.ts +24 -12
  30. package/dist/playwright/reporter.js +62 -28
  31. package/dist/playwright/reporter.js.map +1 -1
  32. package/dist/react/index.cjs +8706 -9883
  33. package/dist/react/index.cjs.map +1 -1
  34. package/dist/react/index.d.cts +720 -146
  35. package/dist/react/index.d.ts +720 -146
  36. package/dist/react/index.js +8518 -9629
  37. package/dist/react/index.js.map +1 -1
  38. package/dist/scan/index.cjs +3360 -0
  39. package/dist/scan/index.cjs.map +1 -0
  40. package/dist/scan/index.d.cts +378 -0
  41. package/dist/scan/index.d.ts +378 -0
  42. package/dist/scan/index.js +3303 -0
  43. package/dist/scan/index.js.map +1 -0
  44. package/package.json +67 -60
  45. package/templates/claude/audit.md +43 -0
  46. package/templates/claude/rules.md +227 -0
  47. package/claude/audit-command.md +0 -46
  48. package/claude/rules.md +0 -167
  49. package/dist/api/index.cjs +0 -254
  50. package/dist/api/index.cjs.map +0 -1
  51. package/dist/api/index.d.cts +0 -236
  52. package/dist/api/index.d.ts +0 -236
  53. package/dist/api/index.js +0 -226
  54. package/dist/api/index.js.map +0 -1
  55. package/dist/core/index.cjs +0 -11045
  56. package/dist/core/index.cjs.map +0 -1
  57. package/dist/core/index.d.cts +0 -424
  58. package/dist/core/index.d.ts +0 -424
  59. package/dist/core/index.global.js +0 -66516
  60. package/dist/core/index.global.js.map +0 -1
  61. package/dist/core/index.js +0 -10995
  62. package/dist/core/index.js.map +0 -1
  63. package/dist/core/style.css +0 -1529
  64. package/dist/scripts/cli.cjs +0 -3904
  65. package/uidex.schema.json +0 -93
@@ -1,3904 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- var __create = Object.create;
4
- var __defProp = Object.defineProperty;
5
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
- var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __getProtoOf = Object.getPrototypeOf;
8
- var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __esm = (fn, res) => function __init() {
10
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
- };
12
- var __export = (target, all) => {
13
- for (var name in all)
14
- __defProp(target, name, { get: all[name], enumerable: true });
15
- };
16
- var __copyProps = (to, from, except, desc) => {
17
- if (from && typeof from === "object" || typeof from === "function") {
18
- for (let key of __getOwnPropNames(from))
19
- if (!__hasOwnProp.call(to, key) && key !== except)
20
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
- }
22
- return to;
23
- };
24
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
- // If the importer is in node compatibility mode or this is not an ESM
26
- // file that has been converted to a CommonJS file using a Babel-
27
- // compatible transform (i.e. "__esModule" has not been set), then set
28
- // "default" to the CommonJS "module.exports" for node compatibility.
29
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
- mod
31
- ));
32
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
-
34
- // scripts/cli-utils.ts
35
- var cli_utils_exports = {};
36
- __export(cli_utils_exports, {
37
- colors: () => colors,
38
- error: () => error,
39
- formatDate: () => formatDate,
40
- formatError: () => formatError,
41
- heading: () => heading,
42
- info: () => info,
43
- pad: () => pad,
44
- parseFlags: () => parseFlags,
45
- success: () => success,
46
- truncate: () => truncate,
47
- warn: () => warn
48
- });
49
- function success(message) {
50
- console.log(`${colors.green}\u2713${colors.reset} ${message}`);
51
- }
52
- function info(message) {
53
- console.log(`${colors.blue}\u2139${colors.reset} ${message}`);
54
- }
55
- function warn(message) {
56
- console.log(`${colors.yellow}\u26A0${colors.reset} ${message}`);
57
- }
58
- function error(message) {
59
- console.log(`${colors.red}\u2717${colors.reset} ${message}`);
60
- }
61
- function heading(message) {
62
- console.log(`
63
- ${colors.bold}${colors.cyan}${message}${colors.reset}
64
- `);
65
- }
66
- function parseFlags(args2) {
67
- const flags = {};
68
- for (const arg of args2) {
69
- if (arg.startsWith("--")) {
70
- const eq = arg.indexOf("=");
71
- if (eq !== -1) {
72
- flags[arg.slice(2, eq)] = arg.slice(eq + 1);
73
- }
74
- }
75
- }
76
- return flags;
77
- }
78
- function truncate(str, len) {
79
- if (str.length <= len) return str;
80
- return str.slice(0, len - 1) + "\u2026";
81
- }
82
- function pad(str, len) {
83
- return str.padEnd(len).slice(0, len);
84
- }
85
- function formatDate(dateStr) {
86
- const d = new Date(dateStr);
87
- return d.toLocaleDateString("en-US", {
88
- month: "short",
89
- day: "numeric",
90
- year: "numeric"
91
- });
92
- }
93
- function formatError(err) {
94
- return err instanceof Error ? err.message : String(err);
95
- }
96
- var colors;
97
- var init_cli_utils = __esm({
98
- "scripts/cli-utils.ts"() {
99
- "use strict";
100
- colors = {
101
- reset: "\x1B[0m",
102
- bold: "\x1B[1m",
103
- dim: "\x1B[2m",
104
- green: "\x1B[32m",
105
- yellow: "\x1B[33m",
106
- blue: "\x1B[34m",
107
- cyan: "\x1B[36m",
108
- red: "\x1B[31m"
109
- };
110
- }
111
- });
112
-
113
- // scripts/config-discovery.ts
114
- function discoverConfigs(from) {
115
- const startDir = from ?? process.cwd();
116
- const results = [];
117
- const queue = [[startDir, 0]];
118
- while (queue.length > 0) {
119
- const [dir, depth] = queue.shift();
120
- let entries;
121
- try {
122
- entries = fs2.readdirSync(dir, { withFileTypes: true });
123
- } catch {
124
- continue;
125
- }
126
- for (const entry of entries) {
127
- if (!entry.isDirectory()) continue;
128
- if (SKIP_DIRS.has(entry.name)) continue;
129
- const childDir = path2.join(dir, entry.name);
130
- const candidatePath = path2.join(childDir, CONFIG_FILENAME);
131
- if (fs2.existsSync(candidatePath)) {
132
- results.push({
133
- configPath: candidatePath,
134
- configDir: childDir
135
- });
136
- continue;
137
- }
138
- if (depth + 1 < MAX_DEPTH) {
139
- queue.push([childDir, depth + 1]);
140
- }
141
- }
142
- }
143
- return results.sort((a, b) => a.configPath.localeCompare(b.configPath));
144
- }
145
- function resolveConfigs(from) {
146
- const startDir = from ?? process.cwd();
147
- if (cachedResult && cachedResult.from === startDir) {
148
- return cachedResult.configs;
149
- }
150
- const localConfig = path2.join(startDir, CONFIG_FILENAME);
151
- let configs;
152
- if (fs2.existsSync(localConfig)) {
153
- configs = [{
154
- configPath: localConfig,
155
- configDir: startDir
156
- }];
157
- } else {
158
- configs = discoverConfigs(startDir);
159
- }
160
- cachedResult = { from: startDir, configs };
161
- return configs;
162
- }
163
- var fs2, path2, SKIP_DIRS, CONFIG_FILENAME, MAX_DEPTH, cachedResult;
164
- var init_config_discovery = __esm({
165
- "scripts/config-discovery.ts"() {
166
- "use strict";
167
- fs2 = __toESM(require("fs"), 1);
168
- path2 = __toESM(require("path"), 1);
169
- SKIP_DIRS = /* @__PURE__ */ new Set([
170
- "node_modules",
171
- ".git",
172
- "dist",
173
- ".next",
174
- "build",
175
- ".workshop",
176
- ".workshop-archive"
177
- ]);
178
- CONFIG_FILENAME = ".uidex.json";
179
- MAX_DEPTH = 4;
180
- cachedResult = null;
181
- }
182
- });
183
-
184
- // scripts/scanner-utils.ts
185
- function parseFrontmatter(content) {
186
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
187
- if (!match) {
188
- return { frontmatter: {}, body: content };
189
- }
190
- const raw = match[1];
191
- const body = match[2];
192
- const frontmatter = {};
193
- const lines = raw.split("\n");
194
- let inComponents = false;
195
- const components = [];
196
- for (const line of lines) {
197
- const rootMatch = line.match(/^root:\s+(.+)$/);
198
- if (rootMatch) {
199
- frontmatter.root = rootMatch[1].trim();
200
- inComponents = false;
201
- continue;
202
- }
203
- const descMatch = line.match(/^description:\s+(.+)$/);
204
- if (descMatch) {
205
- frontmatter.description = descMatch[1].trim();
206
- inComponents = false;
207
- continue;
208
- }
209
- if (/^components:\s*$/.test(line.trimEnd())) {
210
- inComponents = true;
211
- continue;
212
- }
213
- if (inComponents) {
214
- const itemMatch = line.match(/^\s+-\s+(.+)$/);
215
- if (itemMatch) {
216
- components.push(itemMatch[1].trim());
217
- } else if (line.trim() !== "") {
218
- inComponents = false;
219
- }
220
- }
221
- }
222
- if (components.length > 0) {
223
- frontmatter.components = components;
224
- }
225
- return { frontmatter, body };
226
- }
227
- function extractJSDocBlocks(content) {
228
- const blocks = [];
229
- const blockRegex = /\/\*\*[\s\S]*?\*\//g;
230
- let match;
231
- while ((match = blockRegex.exec(content)) !== null) {
232
- const blockContent = match[0];
233
- const uidexMatch = blockContent.match(/@uidex\s+(\S+)/);
234
- if (!uidexMatch) continue;
235
- const id = uidexMatch[1];
236
- const description = extractJSDocDescription(blockContent, id);
237
- if (!description) continue;
238
- const blockStart = match.index;
239
- const blockEnd = blockStart + blockContent.length;
240
- const textBeforeEnd = content.substring(0, blockEnd);
241
- const endLine = textBeforeEnd.split("\n").length;
242
- blocks.push({ id, description, endLine });
243
- }
244
- return blocks;
245
- }
246
- function extractJSDocDescription(blockContent, id) {
247
- const singleLineMatch = blockContent.match(
248
- new RegExp(`@uidex\\s+${escapeRegex(id)}\\s+-\\s+(.+?)\\s*\\*\\/`)
249
- );
250
- if (singleLineMatch) {
251
- return singleLineMatch[1].trim();
252
- }
253
- const lines = blockContent.split("\n");
254
- const descriptionLines = [];
255
- let foundUidex = false;
256
- for (const line of lines) {
257
- if (line.includes(`@uidex`) && line.includes(id)) {
258
- foundUidex = true;
259
- const inlineMatch = line.match(new RegExp(`@uidex\\s+${escapeRegex(id)}\\s+(.+)`));
260
- if (inlineMatch) {
261
- const inline = inlineMatch[1].replace(/\*\/$/, "").trim();
262
- if (inline && !inline.startsWith("-")) {
263
- descriptionLines.push(inline);
264
- }
265
- }
266
- continue;
267
- }
268
- if (foundUidex) {
269
- if (line.includes("@") && /^\s*\*\s*@/.test(line)) {
270
- break;
271
- }
272
- if (line.includes("*/")) {
273
- break;
274
- }
275
- const contentMatch = line.match(/^\s*\*\s*(.*)$/);
276
- if (contentMatch) {
277
- const content = contentMatch[1].trim();
278
- if (content) {
279
- descriptionLines.push(content);
280
- }
281
- }
282
- }
283
- }
284
- return descriptionLines.join(" ").trim();
285
- }
286
- function formatRoute(dir) {
287
- const stripped = dir.replace(/^src\/app\/?/, "").replace(/^src\/pages\/?/, "").replace(/^app\/?/, "").replace(/^pages\/?/, "");
288
- return "/" + stripped;
289
- }
290
- function parseAcceptanceCriteria(content) {
291
- const lines = content.split("\n");
292
- const criteria = [];
293
- let inAcceptance = false;
294
- for (const line of lines) {
295
- if (/^##\s+Acceptance/.test(line)) {
296
- inAcceptance = true;
297
- continue;
298
- }
299
- if (inAcceptance) {
300
- if (/^##\s+/.test(line)) break;
301
- const match = line.match(/^-\s+(?:\[[ x]\]\s*)?(.+)$/);
302
- if (match) criteria.push(match[1].trim());
303
- }
304
- }
305
- return criteria;
306
- }
307
- function normalizeAcceptanceCriteria(content) {
308
- const lines = content.split("\n");
309
- let inAcceptance = false;
310
- for (let i = 0; i < lines.length; i++) {
311
- if (/^##\s+Acceptance/.test(lines[i])) {
312
- inAcceptance = true;
313
- continue;
314
- }
315
- if (inAcceptance) {
316
- if (/^##\s+/.test(lines[i])) {
317
- inAcceptance = false;
318
- continue;
319
- }
320
- if (/^-\s+\[[ x]\]/.test(lines[i])) continue;
321
- const match = lines[i].match(/^-\s+(.+)$/);
322
- if (match) {
323
- lines[i] = `- [ ] ${match[1]}`;
324
- }
325
- }
326
- }
327
- return lines.join("\n");
328
- }
329
- function parseMarkdownTitle(content) {
330
- const match = content.match(/^#\s+(.+)$/m);
331
- return match ? match[1].trim() : null;
332
- }
333
- function parseBlockquoteDescription(content) {
334
- const lines = content.split("\n");
335
- let pastTitle = false;
336
- const descLines = [];
337
- for (const line of lines) {
338
- if (!pastTitle) {
339
- if (/^#\s+/.test(line)) pastTitle = true;
340
- continue;
341
- }
342
- if (descLines.length === 0 && line.trim() === "") continue;
343
- if (line.startsWith("> ")) {
344
- descLines.push(line.slice(2));
345
- } else if (line === ">") {
346
- descLines.push("");
347
- } else {
348
- break;
349
- }
350
- }
351
- return descLines.length > 0 ? descLines.join(" ").trim() : null;
352
- }
353
- function escapeRegex(str) {
354
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
355
- }
356
- function extractComponents(content) {
357
- const results = [];
358
- const jsDocBlocks = extractJSDocBlocks(content);
359
- const componentRegex = /data-uidex(?!-block)(?!-primitive)\s*=\s*(?:"([^"]+)"|'([^']+)'|\{["'`]([^"'`]+)["'`]\})/g;
360
- const blockRegex = /data-uidex-block\s*=\s*(?:"([^"]+)"|'([^']+)'|\{["'`]([^"'`]+)["'`]\})/g;
361
- const primitiveRegex = /data-uidex-primitive\s*=\s*(?:"([^"]+)"|'([^']+)'|\{["'`]([^"'`]+)["'`]\})/g;
362
- const lines = content.split("\n");
363
- lines.forEach((line, index) => {
364
- const lineNumber = index + 1;
365
- let match;
366
- componentRegex.lastIndex = 0;
367
- while ((match = componentRegex.exec(line)) !== null) {
368
- const id = match[1] || match[2] || match[3];
369
- if (id) {
370
- const matchingBlocks = jsDocBlocks.filter(
371
- (block) => block.id === id && block.endLine <= lineNumber && lineNumber - block.endLine <= 5
372
- );
373
- const matchingBlock = matchingBlocks.length > 0 ? matchingBlocks.reduce(
374
- (closest, block) => block.endLine > closest.endLine ? block : closest
375
- ) : void 0;
376
- results.push({
377
- id,
378
- line: lineNumber,
379
- kind: "component",
380
- ...matchingBlock?.description ? { doc: matchingBlock.description } : {}
381
- });
382
- }
383
- }
384
- blockRegex.lastIndex = 0;
385
- while ((match = blockRegex.exec(line)) !== null) {
386
- const id = match[1] || match[2] || match[3];
387
- if (id) {
388
- results.push({
389
- id,
390
- line: lineNumber,
391
- kind: "block"
392
- });
393
- }
394
- }
395
- primitiveRegex.lastIndex = 0;
396
- while ((match = primitiveRegex.exec(line)) !== null) {
397
- const id = match[1] || match[2] || match[3];
398
- if (id) {
399
- results.push({
400
- id,
401
- line: lineNumber,
402
- kind: "primitive"
403
- });
404
- }
405
- }
406
- });
407
- return results;
408
- }
409
- var UIDEX_PAGE_FILENAME, UIDEX_FEATURE_FILENAME;
410
- var init_scanner_utils = __esm({
411
- "scripts/scanner-utils.ts"() {
412
- "use strict";
413
- UIDEX_PAGE_FILENAME = "UIDEX_PAGE.md";
414
- UIDEX_FEATURE_FILENAME = "UIDEX_FEATURE.md";
415
- }
416
- });
417
-
418
- // scripts/primitives.ts
419
- function toPosix(p) {
420
- return p.replace(/\\/g, "/");
421
- }
422
- function resolvePrimitiveScope(filePath, sourceRoot) {
423
- const absFile = path3.resolve(sourceRoot, filePath);
424
- let dir = path3.dirname(absFile);
425
- const absRoot = path3.resolve(sourceRoot);
426
- while (dir.length >= absRoot.length) {
427
- if (fs3.existsSync(path3.join(dir, UIDEX_FEATURE_FILENAME))) {
428
- return `feature:${path3.basename(dir)}`;
429
- }
430
- if (fs3.existsSync(path3.join(dir, UIDEX_PAGE_FILENAME))) {
431
- return `page:${path3.basename(dir)}`;
432
- }
433
- const parent = path3.dirname(dir);
434
- if (parent === dir) break;
435
- dir = parent;
436
- }
437
- return "global";
438
- }
439
- function buildPrimitivesFromAnnotations(extracted) {
440
- return extracted.map((p) => ({
441
- name: p.name,
442
- filePath: p.outputPath,
443
- absolutePath: p.absolutePath,
444
- line: p.line,
445
- scope: resolvePrimitiveScope(p.relativePath, p.rootDir),
446
- composes: [],
447
- usedBy: [],
448
- kind: "primitive"
449
- }));
450
- }
451
- var fs3, path3;
452
- var init_primitives = __esm({
453
- "scripts/primitives.ts"() {
454
- "use strict";
455
- fs3 = __toESM(require("fs"), 1);
456
- path3 = __toESM(require("path"), 1);
457
- init_scanner_utils();
458
- }
459
- });
460
-
461
- // scripts/provenance.ts
462
- function extractImports(content) {
463
- const specifiers = [];
464
- const re = /import\s+(?:[\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
465
- let m;
466
- while ((m = re.exec(content)) !== null) {
467
- specifiers.push(m[1]);
468
- }
469
- return specifiers;
470
- }
471
- function resolveAlias(specifier, tsconfigPaths, absoluteBaseUrl) {
472
- for (const [pattern, mappings] of Object.entries(tsconfigPaths)) {
473
- const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\*/g, "(.*)");
474
- const re = new RegExp(`^${escaped}$`);
475
- const match = specifier.match(re);
476
- if (match) {
477
- for (const mapping of mappings) {
478
- const resolved = mapping.replace("*", match[1] ?? "");
479
- return path4.resolve(absoluteBaseUrl, resolved);
480
- }
481
- }
482
- }
483
- return null;
484
- }
485
- function resolveSpecifier(specifier, fromAbsolutePath, tsconfigPaths, absoluteBaseUrl) {
486
- let resolved = null;
487
- if (specifier.startsWith(".")) {
488
- const fromDir = path4.dirname(fromAbsolutePath);
489
- resolved = path4.resolve(fromDir, specifier);
490
- } else if (tsconfigPaths && absoluteBaseUrl) {
491
- const aliased = resolveAlias(specifier, tsconfigPaths, absoluteBaseUrl);
492
- if (aliased) {
493
- resolved = aliased;
494
- }
495
- }
496
- if (!resolved) return null;
497
- const extensions = [".tsx", ".ts", ".jsx", ".js"];
498
- for (const ext of extensions) {
499
- const candidate = resolved + ext;
500
- if (fs4.existsSync(candidate)) {
501
- return toPosix(candidate);
502
- }
503
- }
504
- if (fs4.existsSync(resolved)) {
505
- return toPosix(resolved);
506
- }
507
- return null;
508
- }
509
- function buildPrimitiveIndex(primitives) {
510
- const index = /* @__PURE__ */ new Map();
511
- for (const p of primitives) {
512
- const existing = index.get(p.absolutePath);
513
- if (existing) {
514
- existing.push(p);
515
- } else {
516
- index.set(p.absolutePath, [p]);
517
- }
518
- }
519
- return index;
520
- }
521
- function computeProvenance(files, primitives, tsconfigPaths, absoluteBaseUrl) {
522
- const primitiveIndex = buildPrimitiveIndex(primitives);
523
- const fileScopeSets = /* @__PURE__ */ new Map();
524
- const usedByMap = /* @__PURE__ */ new Map();
525
- const composesMap = /* @__PURE__ */ new Map();
526
- let provenanceLinks = 0;
527
- for (const file of files) {
528
- const imports = extractImports(file.content);
529
- const fileScopes = /* @__PURE__ */ new Set();
530
- for (const specifier of imports) {
531
- const resolved = resolveSpecifier(
532
- specifier,
533
- file.absolutePath,
534
- tsconfigPaths,
535
- absoluteBaseUrl
536
- );
537
- if (!resolved) continue;
538
- const matchedPrimitives = primitiveIndex.get(resolved);
539
- if (!matchedPrimitives) continue;
540
- provenanceLinks++;
541
- for (const prim of matchedPrimitives) {
542
- fileScopes.add(prim.scope);
543
- const consumerPrimitives = primitiveIndex.get(file.absolutePath);
544
- if (consumerPrimitives) {
545
- for (const consumerPrim of consumerPrimitives) {
546
- if (consumerPrim.absolutePath !== prim.absolutePath) {
547
- let composes = composesMap.get(consumerPrim.absolutePath);
548
- if (!composes) {
549
- composes = /* @__PURE__ */ new Set();
550
- composesMap.set(consumerPrim.absolutePath, composes);
551
- }
552
- composes.add(prim.filePath);
553
- }
554
- }
555
- } else {
556
- const isInScope = isFileInScope(file.outputPath, prim.scope);
557
- if (prim.scope === "global" || isInScope) {
558
- let usedBy = usedByMap.get(prim.absolutePath);
559
- if (!usedBy) {
560
- usedBy = /* @__PURE__ */ new Set();
561
- usedByMap.set(prim.absolutePath, usedBy);
562
- }
563
- usedBy.add(file.outputPath);
564
- }
565
- }
566
- }
567
- }
568
- if (fileScopes.size > 0) {
569
- fileScopeSets.set(file.outputPath, fileScopes);
570
- }
571
- }
572
- const updatedPrimitives = primitives.map((p) => ({
573
- ...p,
574
- composes: [...composesMap.get(p.absolutePath) ?? []].sort(),
575
- usedBy: [...usedByMap.get(p.absolutePath) ?? []].sort()
576
- }));
577
- return { primitives: updatedPrimitives, fileScopeSets, provenanceLinks };
578
- }
579
- function isFileInScope(filePath, scope) {
580
- if (scope === "global") return true;
581
- const colonIdx = scope.indexOf(":");
582
- if (colonIdx === -1) return false;
583
- const scopeName = scope.substring(colonIdx + 1);
584
- const segments = filePath.split("/");
585
- return segments.includes(scopeName);
586
- }
587
- function stripJsonComments(input) {
588
- let out = "";
589
- let inString = false;
590
- let i = 0;
591
- while (i < input.length) {
592
- const ch = input[i];
593
- const next = input[i + 1];
594
- if (inString) {
595
- if (ch === "\\" && next !== void 0) {
596
- out += ch + next;
597
- i += 2;
598
- continue;
599
- }
600
- if (ch === '"') inString = false;
601
- out += ch;
602
- i++;
603
- continue;
604
- }
605
- if (ch === '"') {
606
- inString = true;
607
- out += ch;
608
- i++;
609
- continue;
610
- }
611
- if (ch === "/" && next === "/") {
612
- while (i < input.length && input[i] !== "\n") i++;
613
- continue;
614
- }
615
- if (ch === "/" && next === "*") {
616
- i += 2;
617
- while (i < input.length - 1 && !(input[i] === "*" && input[i + 1] === "/")) i++;
618
- i += 2;
619
- continue;
620
- }
621
- out += ch;
622
- i++;
623
- }
624
- return out;
625
- }
626
- function loadTsconfigPaths(configDir) {
627
- let dir = path4.resolve(configDir);
628
- let tsconfigPath = null;
629
- while (true) {
630
- const candidate = path4.join(dir, "tsconfig.json");
631
- if (fs4.existsSync(candidate)) {
632
- tsconfigPath = candidate;
633
- break;
634
- }
635
- const parent = path4.dirname(dir);
636
- if (parent === dir) break;
637
- dir = parent;
638
- }
639
- if (!tsconfigPath) return {};
640
- try {
641
- const content = fs4.readFileSync(tsconfigPath, "utf-8");
642
- const stripped = stripJsonComments(content);
643
- const parsed = JSON.parse(stripped);
644
- const paths = parsed.compilerOptions?.paths;
645
- const baseUrl = parsed.compilerOptions?.baseUrl ?? ".";
646
- const absoluteBaseUrl = toPosix(
647
- path4.resolve(path4.dirname(tsconfigPath), baseUrl)
648
- );
649
- return { paths, absoluteBaseUrl };
650
- } catch {
651
- return {};
652
- }
653
- }
654
- var fs4, path4;
655
- var init_provenance = __esm({
656
- "scripts/provenance.ts"() {
657
- "use strict";
658
- fs4 = __toESM(require("fs"), 1);
659
- path4 = __toESM(require("path"), 1);
660
- init_primitives();
661
- }
662
- });
663
-
664
- // scripts/scope-leak.ts
665
- function checkScopeLeaks(files, primitives, tsconfigPaths, absoluteBaseUrl) {
666
- if (primitives.length === 0) {
667
- return { errors: [], barrelWarning: false };
668
- }
669
- const primitiveIndex = buildPrimitiveIndex(primitives);
670
- const errors = [];
671
- let anyProvenanceLinks = false;
672
- for (const file of files) {
673
- const content = fs5.readFileSync(file.fullPath, "utf-8");
674
- const lines = content.split("\n");
675
- const imports = extractImports(content);
676
- for (const specifier of imports) {
677
- const resolved = resolveSpecifier(
678
- specifier,
679
- file.absolutePath,
680
- tsconfigPaths,
681
- absoluteBaseUrl
682
- );
683
- if (!resolved) continue;
684
- const matched = primitiveIndex.get(resolved);
685
- if (!matched) continue;
686
- anyProvenanceLinks = true;
687
- for (const prim of matched) {
688
- if (prim.scope === "global") continue;
689
- if (!isFileInScope(file.outputPath, prim.scope)) {
690
- const importLine = lines.findIndex(
691
- (l) => l.includes(specifier) && /import\s/.test(l)
692
- );
693
- errors.push({
694
- consumer: file.outputPath,
695
- line: importLine >= 0 ? importLine + 1 : 1,
696
- primitiveName: prim.name,
697
- primitiveScope: prim.scope,
698
- category: "scope-leak"
699
- });
700
- }
701
- }
702
- }
703
- }
704
- const barrelWarning = primitives.length > 0 && !anyProvenanceLinks;
705
- return { errors, barrelWarning };
706
- }
707
- function runScopeLeakCheck(configDir, files, primitives) {
708
- const tsconfig = loadTsconfigPaths(configDir);
709
- const scopeLeakFiles = files.map((f) => ({
710
- absolutePath: toPosix(f.fullPath),
711
- outputPath: f.outputPath,
712
- fullPath: f.fullPath
713
- }));
714
- return checkScopeLeaks(
715
- scopeLeakFiles,
716
- primitives,
717
- tsconfig.paths,
718
- tsconfig.absoluteBaseUrl
719
- );
720
- }
721
- function printScopeLeakReport(result) {
722
- if (result.errors.length > 0) {
723
- console.error(`
724
- Scope-leak violations (${result.errors.length}):`);
725
- for (const err of result.errors) {
726
- console.error(
727
- ` ${err.consumer}:${err.line}: "${err.primitiveName}" belongs to ${err.primitiveScope}, not accessible from this file`
728
- );
729
- }
730
- console.error("");
731
- }
732
- if (result.barrelWarning) {
733
- console.warn(
734
- "\nNote: primitives detected but zero provenance links found. If you import primitives"
735
- );
736
- console.warn(
737
- " through barrel exports (index.ts), import directly for scope tracking to work."
738
- );
739
- console.warn("");
740
- }
741
- }
742
- var fs5;
743
- var init_scope_leak = __esm({
744
- "scripts/scope-leak.ts"() {
745
- "use strict";
746
- fs5 = __toESM(require("fs"), 1);
747
- init_provenance();
748
- init_primitives();
749
- }
750
- });
751
-
752
- // scripts/lint.ts
753
- var lint_exports = {};
754
- __export(lint_exports, {
755
- isNonVisual: () => isNonVisual,
756
- isPresentational: () => isPresentational,
757
- lintFile: () => lintFile,
758
- printLintJson: () => printLintJson,
759
- printLintReport: () => printLintReport,
760
- runLint: () => runLint
761
- });
762
- function hasAnnotation(lines, lineIndex, attr) {
763
- const start = Math.max(0, lineIndex - 3);
764
- const end = Math.min(lines.length, lineIndex + 10);
765
- const window = lines.slice(start, end).join("\n");
766
- if (attr === "data-uidex") {
767
- return /data-uidex(?!-block)/.test(window);
768
- }
769
- return window.includes(attr);
770
- }
771
- function extractTextContent(line) {
772
- const match = line.match(/>([^<]+)</);
773
- return match ? match[1].trim() || null : null;
774
- }
775
- function getSurroundingCode(lines, lineIndex) {
776
- const start = Math.max(0, lineIndex - 2);
777
- const end = Math.min(lines.length, lineIndex + 3);
778
- return lines.slice(start, end).join("\n");
779
- }
780
- function extractComponentName(content) {
781
- const match = content.match(
782
- /export\s+(?:default\s+)?function\s+([A-Z]\w*)/
783
- );
784
- return match ? match[1] : null;
785
- }
786
- function extractJsxTags(content) {
787
- JSX_TAG_RE.lastIndex = 0;
788
- const tags = [];
789
- let match;
790
- while ((match = JSX_TAG_RE.exec(content)) !== null) {
791
- tags.push(match[1]);
792
- }
793
- return tags;
794
- }
795
- function isNonVisual(filePath, content) {
796
- JSX_TAG_RE.lastIndex = 0;
797
- if (!JSX_TAG_RE.test(content)) {
798
- return { skip: true, reason: "no JSX elements" };
799
- }
800
- if (content.includes("createContext(") || content.includes("createContext<")) {
801
- const tags = extractJsxTags(content);
802
- if (tags.every((tag) => PROVIDER_TAG_RE.test(tag))) {
803
- return { skip: true, reason: "context provider" };
804
- }
805
- }
806
- if (CHILDREN_ONLY_RE.test(content)) {
807
- const tags = extractJsxTags(content);
808
- if (tags.length <= 1) {
809
- return { skip: true, reason: "children-only wrapper" };
810
- }
811
- }
812
- if (!content.includes("data-uidex")) {
813
- const basename4 = path5.basename(filePath);
814
- const tags = extractJsxTags(content);
815
- const hasHtmlElements = tags.some((tag) => HTML_ELEMENT_RE.test(tag));
816
- if (HOOK_FILE_RE.test(basename4) && !hasHtmlElements) {
817
- return { skip: true, reason: "hook filename" };
818
- }
819
- if (CONTEXT_PROVIDER_FILE_RE.test(basename4) && !hasHtmlElements) {
820
- return { skip: true, reason: "context/provider filename" };
821
- }
822
- }
823
- return { skip: false };
824
- }
825
- function isPresentational(filePath, content) {
826
- if (PAGE_FILE_RE.test(filePath)) return false;
827
- const componentName = content.match(
828
- /export\s+(?:default\s+)?function\s+([A-Z]\w*)/
829
- );
830
- if (!componentName) return false;
831
- if (DATA_HOOK_RE.test(content)) return false;
832
- if (FETCH_CALL_RE.test(content)) return false;
833
- if (SERVER_ACTION_RE.test(content)) return false;
834
- const tags = extractJsxTags(content);
835
- const htmlTags = tags.filter((t) => HTML_ELEMENT_RE.test(t));
836
- if (htmlTags.length === 0) return false;
837
- const composedTags = tags.filter((t) => !HTML_ELEMENT_RE.test(t));
838
- const UTILITY_TAG_RE = /^(Slot|Fragment|Spinner|Icon\w*|Svg\w*|Suspense|ErrorBoundary)$/;
839
- const appLevelTags = composedTags.filter((t) => !UTILITY_TAG_RE.test(t));
840
- if (appLevelTags.length > htmlTags.length) return false;
841
- const hasPassthrough = CHILDREN_PROP_RE.test(content) || CLASSNAME_PROP_RE.test(content) || SPREAD_PROPS_RE.test(content);
842
- if (!hasPassthrough) return false;
843
- return true;
844
- }
845
- function buildElementRegex(elements) {
846
- const escaped = elements.map(escapeRegex);
847
- return new RegExp(`<(${escaped.join("|")})(?:\\s|>|\\/|$)`, "g");
848
- }
849
- function toKebab(name) {
850
- return name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z])([A-Z][a-z])/g, "$1-$2").toLowerCase();
851
- }
852
- function lintFile(filePath, content, lintConfig) {
853
- const componentName = extractComponentName(content);
854
- if (PRIMITIVE_ATTR_RE.test(content)) {
855
- return {
856
- file: filePath,
857
- componentName,
858
- hasRootBlock: false,
859
- existingIds: [],
860
- violations: [],
861
- skipped: true,
862
- skipReason: "primitive definition"
863
- };
864
- }
865
- if (isPresentational(filePath, content)) {
866
- const kebab = toKebab(componentName ?? path5.basename(filePath, path5.extname(filePath)));
867
- return {
868
- file: filePath,
869
- componentName,
870
- hasRootBlock: false,
871
- existingIds: [],
872
- violations: [{
873
- line: 1,
874
- tier: "root",
875
- element: "root",
876
- textContent: null,
877
- surroundingCode: content.split("\n").slice(0, 5).join("\n"),
878
- suggestedType: "data-uidex-primitive"
879
- }],
880
- suggestPrimitive: true
881
- };
882
- }
883
- const autoSkip = isNonVisual(filePath, content);
884
- if (autoSkip.skip) {
885
- return {
886
- file: filePath,
887
- componentName,
888
- hasRootBlock: false,
889
- existingIds: [],
890
- violations: [],
891
- skipped: true,
892
- skipReason: autoSkip.reason
893
- };
894
- }
895
- const existingIds = extractComponents(content).map((r) => r.id);
896
- const lines = content.split("\n");
897
- const violations = [];
898
- const hasRootBlock = content.includes("data-uidex-block");
899
- if (!hasRootBlock) {
900
- violations.push({
901
- line: 1,
902
- tier: "root",
903
- element: "root",
904
- textContent: null,
905
- surroundingCode: getSurroundingCode(lines, 0),
906
- suggestedType: "data-uidex-block"
907
- });
908
- }
909
- const regionRegex = buildElementRegex(lintConfig.regionElements);
910
- for (let i = 0; i < lines.length; i++) {
911
- regionRegex.lastIndex = 0;
912
- let match;
913
- while ((match = regionRegex.exec(lines[i])) !== null) {
914
- if (!hasAnnotation(lines, i, "data-uidex-block")) {
915
- violations.push({
916
- line: i + 1,
917
- tier: "region",
918
- element: match[1],
919
- textContent: extractTextContent(lines[i]),
920
- surroundingCode: getSurroundingCode(lines, i),
921
- suggestedType: "data-uidex-block"
922
- });
923
- }
924
- }
925
- }
926
- const interactiveRegex = buildElementRegex(
927
- lintConfig.interactiveElements
928
- );
929
- const tagViolationLines = /* @__PURE__ */ new Set();
930
- for (let i = 0; i < lines.length; i++) {
931
- interactiveRegex.lastIndex = 0;
932
- let match;
933
- while ((match = interactiveRegex.exec(lines[i])) !== null) {
934
- if (!hasAnnotation(lines, i, "data-uidex")) {
935
- tagViolationLines.add(i);
936
- violations.push({
937
- line: i + 1,
938
- tier: "interactive",
939
- element: match[1],
940
- textContent: extractTextContent(lines[i]),
941
- surroundingCode: getSurroundingCode(lines, i),
942
- suggestedType: "data-uidex"
943
- });
944
- }
945
- }
946
- }
947
- for (let i = 0; i < lines.length; i++) {
948
- if (tagViolationLines.has(i)) continue;
949
- ROLE_REGEX.lastIndex = 0;
950
- let match;
951
- while ((match = ROLE_REGEX.exec(lines[i])) !== null) {
952
- if (!hasAnnotation(lines, i, "data-uidex")) {
953
- violations.push({
954
- line: i + 1,
955
- tier: "interactive",
956
- element: `role="${match[1]}"`,
957
- textContent: extractTextContent(lines[i]),
958
- surroundingCode: getSurroundingCode(lines, i),
959
- suggestedType: "data-uidex"
960
- });
961
- }
962
- }
963
- }
964
- const presentational = violations.length > 0 && isPresentational(filePath, content);
965
- if (presentational) {
966
- for (const v of violations) {
967
- v.suggestedType = "data-uidex-primitive";
968
- }
969
- }
970
- return {
971
- file: filePath,
972
- componentName,
973
- hasRootBlock,
974
- existingIds,
975
- violations,
976
- ...presentational ? { suggestPrimitive: true } : {}
977
- };
978
- }
979
- function runLint(configDir, options) {
980
- const scannerConfig = loadConfig({ silent: true, configDir });
981
- const lintConfig = loadLintConfig(configDir);
982
- const verbose = options?.verbose ?? false;
983
- const skipRegexes = compilePatterns(lintConfig.skipPaths);
984
- const allFiles = [];
985
- for (const source of scannerConfig.sources) {
986
- const sourceResult = findFilesForSource(source, scannerConfig.exclude, scannerConfig.configDir);
987
- for (const file of sourceResult.files) {
988
- allFiles.push({
989
- outputPath: file.outputPath,
990
- fullPath: file.fullPath
991
- });
992
- }
993
- }
994
- const results = [];
995
- const skippedFiles = [];
996
- for (const file of allFiles) {
997
- if (matchesPatterns(file.outputPath, skipRegexes)) {
998
- if (verbose) {
999
- const idx = skipRegexes.findIndex((re) => re.test(file.outputPath));
1000
- skippedFiles.push({
1001
- file: file.outputPath,
1002
- reason: `skip path: ${lintConfig.skipPaths[idx] ?? "unknown"}`
1003
- });
1004
- }
1005
- continue;
1006
- }
1007
- const content = fs6.readFileSync(file.fullPath, "utf-8");
1008
- const result = lintFile(file.outputPath, content, lintConfig);
1009
- if (result.skipped) {
1010
- if (verbose) {
1011
- skippedFiles.push({
1012
- file: file.outputPath,
1013
- reason: result.skipReason ?? "unknown"
1014
- });
1015
- }
1016
- continue;
1017
- }
1018
- if (result.violations.length > 0) {
1019
- results.push(result);
1020
- }
1021
- }
1022
- const stats = {
1023
- totalFiles: allFiles.length,
1024
- filesWithViolations: results.length,
1025
- totalViolations: results.reduce(
1026
- (sum, r) => sum + r.violations.length,
1027
- 0
1028
- ),
1029
- byTier: {
1030
- root: results.reduce(
1031
- (sum, r) => sum + r.violations.filter((v) => v.tier === "root").length,
1032
- 0
1033
- ),
1034
- region: results.reduce(
1035
- (sum, r) => sum + r.violations.filter((v) => v.tier === "region").length,
1036
- 0
1037
- ),
1038
- interactive: results.reduce(
1039
- (sum, r) => sum + r.violations.filter((v) => v.tier === "interactive").length,
1040
- 0
1041
- )
1042
- }
1043
- };
1044
- if (verbose) {
1045
- stats.autoSkipped = skippedFiles.length;
1046
- }
1047
- return {
1048
- files: results,
1049
- stats,
1050
- ...verbose ? { skipped: skippedFiles } : {}
1051
- };
1052
- }
1053
- function printLintReport(result) {
1054
- if (result.stats.totalViolations === 0) {
1055
- success(
1056
- `All ${result.stats.totalFiles} files pass lint checks`
1057
- );
1058
- } else {
1059
- console.error("");
1060
- warn(
1061
- `Found ${result.stats.totalViolations} missing annotations in ${result.stats.filesWithViolations} files`
1062
- );
1063
- console.error("");
1064
- for (const file of result.files) {
1065
- const name = file.componentName ? `${file.file} (${file.componentName})` : file.file;
1066
- info(name);
1067
- if (file.suggestPrimitive) {
1068
- console.error(` \u2192 presentational component \u2014 add data-uidex-primitive="${toKebab(file.componentName ?? path5.basename(file.file, path5.extname(file.file)))}" to root element`);
1069
- }
1070
- for (const v of file.violations) {
1071
- const label = v.tier === "root" ? "missing root data-uidex-block" : v.tier === "region" ? `<${v.element}> missing data-uidex-block` : v.element.startsWith("role=") ? `${v.element} missing data-uidex` : `<${v.element}>${v.textContent ? v.textContent : ""}${v.textContent ? `</${v.element}>` : ""} missing data-uidex`;
1072
- console.error(` line ${v.line}: ${label} (suggest: ${v.suggestedType})`);
1073
- }
1074
- console.error("");
1075
- }
1076
- console.error(
1077
- ` Breakdown: ${result.stats.byTier.root} root, ${result.stats.byTier.region} region, ${result.stats.byTier.interactive} interactive`
1078
- );
1079
- console.error("");
1080
- }
1081
- if (result.skipped && result.skipped.length > 0) {
1082
- console.error(" Auto-skipped files:");
1083
- for (const s of result.skipped) {
1084
- console.error(` ${s.file} (${s.reason})`);
1085
- }
1086
- console.error("");
1087
- }
1088
- }
1089
- function printLintJson(result) {
1090
- console.log(JSON.stringify(result, null, 2));
1091
- }
1092
- var fs6, path5, PRIMITIVE_ATTR_RE, JSX_TAG_RE, PROVIDER_TAG_RE, CHILDREN_ONLY_RE, HOOK_FILE_RE, CONTEXT_PROVIDER_FILE_RE, HTML_ELEMENT_RE, DATA_HOOK_RE, FETCH_CALL_RE, SERVER_ACTION_RE, PAGE_FILE_RE, SPREAD_PROPS_RE, CHILDREN_PROP_RE, CLASSNAME_PROP_RE, ROLE_REGEX;
1093
- var init_lint = __esm({
1094
- "scripts/lint.ts"() {
1095
- "use strict";
1096
- fs6 = __toESM(require("fs"), 1);
1097
- path5 = __toESM(require("path"), 1);
1098
- init_scan();
1099
- init_scanner_utils();
1100
- init_cli_utils();
1101
- PRIMITIVE_ATTR_RE = /data-uidex-primitive\s*=/;
1102
- JSX_TAG_RE = /(?:^|[^a-zA-Z0-9_])<([A-Z][a-zA-Z0-9.]*|[a-z][a-z0-9]*)[\s>\/]/gm;
1103
- PROVIDER_TAG_RE = /^([A-Z][a-zA-Z0-9]*\.Provider|[A-Z][a-zA-Z0-9]*Provider)$/;
1104
- CHILDREN_ONLY_RE = /return\s*\(?\s*<(\w[\w.]*)(?:\s[^>]*)?>[\s\n]*\{children\}[\s\n]*<\/\1>\s*\)?/;
1105
- HOOK_FILE_RE = /^use[A-Z].*\.[jt]sx$/;
1106
- CONTEXT_PROVIDER_FILE_RE = /context|provider/i;
1107
- HTML_ELEMENT_RE = /^[a-z]/;
1108
- DATA_HOOK_RE = /\b(useQuery|useMutation|useSuspenseQuery|useInfiniteQuery|useSWR|useSWRMutation)\s*[<(]/;
1109
- FETCH_CALL_RE = /\bfetch\s*\(/;
1110
- SERVER_ACTION_RE = /\buse server\b/;
1111
- PAGE_FILE_RE = /(?:^|[\\/])(?:page|layout|template|loading|error|not-found)\.[jt]sx?$/;
1112
- SPREAD_PROPS_RE = /\.\.\.\s*(?:props|rest)\b/;
1113
- CHILDREN_PROP_RE = /\bchildren\b/;
1114
- CLASSNAME_PROP_RE = /\bclassName\b/;
1115
- ROLE_REGEX = /role=["'](button|link|tab)["']/g;
1116
- }
1117
- });
1118
-
1119
- // scripts/scan.ts
1120
- function readConfigFile(configDir) {
1121
- const configPath = path6.resolve(configDir ?? process.cwd(), CONFIG_FILENAME);
1122
- try {
1123
- return JSON.parse(fs7.readFileSync(configPath, "utf-8"));
1124
- } catch {
1125
- return null;
1126
- }
1127
- }
1128
- function loadConfig(options) {
1129
- const dir = options?.configDir ?? process.cwd();
1130
- const raw = readConfigFile(dir);
1131
- const log2 = options?.silent ? () => {
1132
- } : console.log.bind(console);
1133
- if (!raw) {
1134
- log2("No .uidex.json found, using defaults");
1135
- return { ...DEFAULT_CONFIG, configDir: dir };
1136
- }
1137
- if (!raw.scanner) {
1138
- log2("No scanner config in .uidex.json, using defaults");
1139
- return { ...DEFAULT_CONFIG, configDir: dir };
1140
- }
1141
- const scanner = raw.scanner;
1142
- let sources;
1143
- if (scanner.sources?.length) {
1144
- sources = scanner.sources.map((source) => ({
1145
- rootDir: source.rootDir,
1146
- include: source.include,
1147
- exclude: source.exclude,
1148
- prefix: source.prefix
1149
- }));
1150
- } else if (scanner.rootDir) {
1151
- sources = [{
1152
- rootDir: scanner.rootDir,
1153
- include: scanner.include ?? ["**/*.tsx", "**/*.jsx"]
1154
- }];
1155
- } else {
1156
- sources = DEFAULT_CONFIG.sources;
1157
- }
1158
- return {
1159
- sources,
1160
- exclude: scanner.exclude ?? DEFAULT_CONFIG.exclude,
1161
- outputPath: scanner.outputPath ?? DEFAULT_CONFIG.outputPath,
1162
- configDir: dir
1163
- };
1164
- }
1165
- function loadLintConfig(configDir) {
1166
- const raw = readConfigFile(configDir);
1167
- if (!raw?.lint) {
1168
- return DEFAULT_LINT_CONFIG;
1169
- }
1170
- const useDefaults = raw.lint.skipPathDefaults !== false;
1171
- const userPaths = raw.lint.skipPaths ?? [];
1172
- const skipPaths = useDefaults ? [...DEFAULT_SKIP_PATHS, ...userPaths] : userPaths;
1173
- return {
1174
- interactiveElements: raw.lint.interactiveElements ?? DEFAULT_LINT_CONFIG.interactiveElements,
1175
- regionElements: raw.lint.regionElements ?? DEFAULT_LINT_CONFIG.regionElements,
1176
- skipPaths,
1177
- skipPathDefaults: useDefaults
1178
- };
1179
- }
1180
- function globToRegex(glob) {
1181
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}\//g, "(?:.*/)?").replace(/{{GLOBSTAR}}/g, ".*");
1182
- return new RegExp(`^${escaped}$`);
1183
- }
1184
- function compilePatterns(patterns) {
1185
- return patterns.map(globToRegex);
1186
- }
1187
- function matchesPatterns(filePath, patterns) {
1188
- const normalizedPath = filePath.replace(/\\/g, "/");
1189
- return patterns.some((regex) => regex.test(normalizedPath));
1190
- }
1191
- function parentDir(filePath) {
1192
- const i = filePath.lastIndexOf("/");
1193
- return i === -1 ? "." : filePath.substring(0, i);
1194
- }
1195
- function walkDir(dir, baseDir = dir) {
1196
- const result = { files: [], pageDocs: /* @__PURE__ */ new Map(), featureDocs: /* @__PURE__ */ new Map(), routeDirs: /* @__PURE__ */ new Set() };
1197
- if (!fs7.existsSync(dir)) {
1198
- return result;
1199
- }
1200
- const entries = fs7.readdirSync(dir, { withFileTypes: true });
1201
- for (const entry of entries) {
1202
- const fullPath = path6.join(dir, entry.name);
1203
- if (entry.isDirectory()) {
1204
- if (entry.name === "node_modules") continue;
1205
- const sub = walkDir(fullPath, baseDir);
1206
- result.files.push(...sub.files);
1207
- for (const [k, v] of sub.pageDocs) {
1208
- result.pageDocs.set(k, v);
1209
- }
1210
- for (const [k, v] of sub.featureDocs) {
1211
- result.featureDocs.set(k, v);
1212
- }
1213
- for (const d of sub.routeDirs) {
1214
- result.routeDirs.add(d);
1215
- }
1216
- } else if (entry.isFile()) {
1217
- const relativeDir = path6.relative(baseDir, dir).replace(/\\/g, "/") || ".";
1218
- if (entry.name === UIDEX_PAGE_FILENAME || entry.name === UIDEX_FEATURE_FILENAME) {
1219
- const content = fs7.readFileSync(fullPath, "utf-8");
1220
- if (entry.name === UIDEX_PAGE_FILENAME) {
1221
- result.pageDocs.set(relativeDir, content);
1222
- } else {
1223
- result.featureDocs.set(relativeDir, content);
1224
- }
1225
- } else {
1226
- const relativePath = path6.relative(baseDir, fullPath).replace(/\\/g, "/");
1227
- result.files.push(relativePath);
1228
- if (/^page\.[tjm]sx?$/.test(entry.name)) {
1229
- result.routeDirs.add(relativeDir);
1230
- }
1231
- }
1232
- }
1233
- }
1234
- return result;
1235
- }
1236
- function findFilesForSource(source, globalExclude, configDir = process.cwd()) {
1237
- const rootDir = path6.resolve(configDir, source.rootDir);
1238
- const walkResult = walkDir(rootDir);
1239
- const includeRegexes = compilePatterns(source.include);
1240
- const excludeRegexes = compilePatterns([...globalExclude, ...source.exclude ?? []]);
1241
- const files = walkResult.files.filter((file) => {
1242
- const matchesInclude = matchesPatterns(file, includeRegexes);
1243
- const matchesExclude = matchesPatterns(file, excludeRegexes);
1244
- return matchesInclude && !matchesExclude;
1245
- }).map((relativePath) => {
1246
- const outputPath = source.prefix ? `${source.prefix}/${relativePath}` : relativePath;
1247
- return {
1248
- relativePath,
1249
- fullPath: path6.join(rootDir, relativePath),
1250
- outputPath
1251
- };
1252
- });
1253
- function applyPrefix(docs) {
1254
- const result = /* @__PURE__ */ new Map();
1255
- for (const [dir, content] of docs) {
1256
- const outputDir = source.prefix ? `${source.prefix}/${dir}` : dir;
1257
- result.set(outputDir, content);
1258
- }
1259
- return result;
1260
- }
1261
- const scannedDirs = new Set(files.map((f) => parentDir(f.relativePath)));
1262
- const routeDirs = /* @__PURE__ */ new Set();
1263
- for (const dir of walkResult.routeDirs) {
1264
- const hasScannedFiles = [...scannedDirs].some(
1265
- (sDir) => sDir === dir || sDir.startsWith(dir + "/")
1266
- );
1267
- if (hasScannedFiles) {
1268
- routeDirs.add(source.prefix ? `${source.prefix}/${dir}` : dir);
1269
- }
1270
- }
1271
- return {
1272
- files,
1273
- pageDocs: applyPrefix(walkResult.pageDocs),
1274
- featureDocs: applyPrefix(walkResult.featureDocs),
1275
- routeDirs
1276
- };
1277
- }
1278
- function buildPages(docs, sourceComponentIds, sourceRootDir) {
1279
- if (docs.size === 0) return [];
1280
- const docDirs = [...docs.keys()].sort((a, b) => b.length - a.length);
1281
- const pageComponentSets = /* @__PURE__ */ new Map();
1282
- for (const dir of docDirs) {
1283
- pageComponentSets.set(dir, /* @__PURE__ */ new Set());
1284
- }
1285
- for (const [id, filePaths] of sourceComponentIds) {
1286
- for (const filePath of filePaths) {
1287
- const fileDir = parentDir(filePath);
1288
- const nearestDir = docDirs.find(
1289
- (dir) => dir === "." || fileDir === dir || fileDir.startsWith(dir + "/")
1290
- );
1291
- if (nearestDir) {
1292
- pageComponentSets.get(nearestDir).add(id);
1293
- }
1294
- }
1295
- }
1296
- return docDirs.map((dir) => {
1297
- const fullDir = dir === "." ? sourceRootDir : `${sourceRootDir}/${dir}`;
1298
- const doc = docs.get(dir);
1299
- const ids = pageComponentSets.get(dir);
1300
- for (const id of doc.explicitComponents) {
1301
- ids.add(id);
1302
- }
1303
- return {
1304
- dir: fullDir,
1305
- content: doc.body,
1306
- componentIds: [...ids].sort(),
1307
- ...doc.rootId ? { rootId: doc.rootId } : {},
1308
- ...doc.description ? { description: doc.description } : {}
1309
- };
1310
- });
1311
- }
1312
- function buildFeatures(featureDocs, sourceComponentIds, sourceRootDir) {
1313
- if (featureDocs.size === 0) return [];
1314
- const docDirs = [...featureDocs.keys()].sort((a, b) => b.length - a.length);
1315
- const featureComponentSets = /* @__PURE__ */ new Map();
1316
- for (const dir of docDirs) {
1317
- featureComponentSets.set(dir, /* @__PURE__ */ new Set());
1318
- }
1319
- for (const [id, filePaths] of sourceComponentIds) {
1320
- for (const filePath of filePaths) {
1321
- const fileDir = parentDir(filePath);
1322
- const nearestDir = docDirs.find(
1323
- (dir) => dir === "." || fileDir === dir || fileDir.startsWith(dir + "/")
1324
- );
1325
- if (nearestDir) {
1326
- featureComponentSets.get(nearestDir).add(id);
1327
- }
1328
- }
1329
- }
1330
- return docDirs.map((dir) => {
1331
- const fullDir = dir === "." ? sourceRootDir : `${sourceRootDir}/${dir}`;
1332
- const doc = featureDocs.get(dir);
1333
- const ids = featureComponentSets.get(dir);
1334
- for (const id of doc.explicitComponents) {
1335
- ids.add(id);
1336
- }
1337
- return {
1338
- dir: fullDir,
1339
- content: doc.body,
1340
- componentIds: [...ids].sort(),
1341
- ...doc.description ? { description: doc.description } : {}
1342
- };
1343
- });
1344
- }
1345
- function detectGitContext() {
1346
- const branchEnvVars = [
1347
- "GITHUB_HEAD_REF",
1348
- "GITHUB_REF_NAME",
1349
- "VERCEL_GIT_COMMIT_REF",
1350
- "CF_PAGES_BRANCH",
1351
- "RAILWAY_GIT_BRANCH",
1352
- "NETLIFY_BRANCH",
1353
- "CI_COMMIT_BRANCH",
1354
- "CIRCLE_BRANCH",
1355
- "BITBUCKET_BRANCH"
1356
- ];
1357
- let branch;
1358
- for (const envVar of branchEnvVars) {
1359
- const val = process.env[envVar];
1360
- if (val) {
1361
- branch = val;
1362
- break;
1363
- }
1364
- }
1365
- const commitEnvVars = [
1366
- "GITHUB_SHA",
1367
- "VERCEL_GIT_COMMIT_SHA",
1368
- "CF_PAGES_COMMIT_SHA",
1369
- "RAILWAY_GIT_COMMIT_SHA",
1370
- "COMMIT_REF",
1371
- "CI_COMMIT_SHA",
1372
- "CIRCLE_SHA1",
1373
- "BITBUCKET_COMMIT"
1374
- ];
1375
- let commit;
1376
- for (const envVar of commitEnvVars) {
1377
- const val = process.env[envVar];
1378
- if (val) {
1379
- commit = val;
1380
- break;
1381
- }
1382
- }
1383
- if (!branch) {
1384
- try {
1385
- branch = (0, import_child_process.execSync)("git rev-parse --abbrev-ref HEAD", {
1386
- encoding: "utf-8",
1387
- stdio: ["pipe", "pipe", "pipe"]
1388
- }).trim();
1389
- if (branch === "HEAD") branch = void 0;
1390
- } catch {
1391
- }
1392
- }
1393
- if (!commit) {
1394
- try {
1395
- commit = (0, import_child_process.execSync)("git rev-parse HEAD", {
1396
- encoding: "utf-8",
1397
- stdio: ["pipe", "pipe", "pipe"]
1398
- }).trim();
1399
- } catch {
1400
- }
1401
- }
1402
- return { branch, commit };
1403
- }
1404
- function generateTestOutput(components, pages, features) {
1405
- const sortedIds = Object.keys(components).sort();
1406
- const idsArrayStr = sortedIds.map((id) => `"${id}"`).join(", ");
1407
- const pageRoutes = pages.map((p) => ({ ...p, route: formatRoute(p.dir) }));
1408
- const routesStr = pageRoutes.length > 0 ? `
1409
- export const routes = {
1410
- ${pageRoutes.map((p) => ` "${p.dir}": "${p.route}"`).join(",\n")}
1411
- } as const;
1412
-
1413
- export type Route = typeof routes[keyof typeof routes];
1414
- ` : "";
1415
- const pagesStr = pageRoutes.length > 0 ? `
1416
- export const pages = [
1417
- ${pageRoutes.map((p) => {
1418
- const ids = p.componentIds.map((id) => `"${id}"`).join(", ");
1419
- const rootPart = p.rootId ? `, rootId: "${p.rootId}"` : "";
1420
- return ` { dir: "${p.dir}", route: "${p.route}", componentIds: [${ids}] as const${rootPart} }`;
1421
- }).join(",\n")}
1422
- ] as const;
1423
- ` : "";
1424
- const featuresStr = features.length > 0 ? `
1425
- export const features = [
1426
- ${features.map((f) => {
1427
- const ids = f.componentIds.map((id) => `"${id}"`).join(", ");
1428
- return ` { dir: "${f.dir}", componentIds: [${ids}] as const }`;
1429
- }).join(",\n")}
1430
- ] as const;
1431
- ` : "";
1432
- return `// Auto-generated by uidex scanner \u2014 safe for test imports (no side effects)
1433
- // Do not edit this file manually
1434
-
1435
- export const componentIds = [${idsArrayStr}] as const;
1436
-
1437
- export type ComponentId = typeof componentIds[number];
1438
- ${routesStr}${pagesStr}${featuresStr}`;
1439
- }
1440
- function generateOutput(components, pages, features, gitContext, uiComponents) {
1441
- const sortedIds = Object.keys(components).sort();
1442
- const entriesStr = sortedIds.map((id) => {
1443
- const locations = components[id].map((loc) => {
1444
- const parts = [
1445
- `filePath: "${loc.filePath}"`,
1446
- `line: ${loc.line}`,
1447
- `kind: "${loc.kind}"`
1448
- ];
1449
- if (loc.doc) {
1450
- const escapedDoc = loc.doc.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
1451
- parts.push(`doc: "${escapedDoc}"`);
1452
- }
1453
- if (loc.scopes && loc.scopes.length > 0) {
1454
- const scopesList = loc.scopes.map((s) => `"${s}"`).join(", ");
1455
- parts.push(`scopes: [${scopesList}]`);
1456
- }
1457
- return `{ ${parts.join(", ")} }`;
1458
- }).join(", ");
1459
- return ` "${id}": [${locations}]`;
1460
- }).join(",\n");
1461
- const idsArrayStr = sortedIds.map((id) => `"${id}"`).join(", ");
1462
- function serializeDocEntries(entries) {
1463
- return entries.map((entry) => {
1464
- const escaped = entry.content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
1465
- const ids = entry.componentIds.map((id) => `"${id}"`).join(", ");
1466
- const rootPart = entry.rootId ? `, rootId: "${entry.rootId}"` : "";
1467
- const descPart = entry.description ? `, description: "${entry.description.replace(/"/g, '\\"')}"` : "";
1468
- return ` { dir: "${entry.dir}", content: \`${escaped}\`, componentIds: [${ids}]${rootPart}${descPart} }`;
1469
- }).join(",\n");
1470
- }
1471
- const pagesStr = pages.length > 0 ? `
1472
- export const pages = [
1473
- ${serializeDocEntries(pages)}
1474
- ];
1475
- ` : "";
1476
- const featuresStr = features.length > 0 ? `
1477
- export const features = [
1478
- ${serializeDocEntries(features)}
1479
- ];
1480
- ` : "";
1481
- const hasPages = pages.length > 0;
1482
- const hasFeatures = features.length > 0;
1483
- const hasGitContext = gitContext?.branch != null;
1484
- const hasUiComponents = (uiComponents ?? []).length > 0;
1485
- const uiComponentsStr = hasUiComponents ? `
1486
- export const uiComponents = [
1487
- ${(uiComponents ?? []).sort((a, b) => a.filePath.localeCompare(b.filePath)).map((p) => {
1488
- const composesStr = p.composes.map((c) => `"${c}"`).join(", ");
1489
- const usedByStr = p.usedBy.map((u) => `"${u}"`).join(", ");
1490
- return ` { name: "${p.name}", filePath: "${p.filePath}", scope: "${p.scope}", composes: [${composesStr}], usedBy: [${usedByStr}], kind: "primitive" as const }`;
1491
- }).join(",\n")}
1492
- ];
1493
- ` : "";
1494
- const importParts = ["registerComponents"];
1495
- if (hasPages) importParts.push("registerPages");
1496
- if (hasFeatures) importParts.push("registerFeatures");
1497
- if (hasGitContext) importParts.push("registerGitContext");
1498
- importParts.push("createUidexDevtools");
1499
- const typeParts = ["UidexMap"];
1500
- if (hasUiComponents) typeParts.push("PrimitiveEntry");
1501
- const imports = `import { ${importParts.join(", ")} } from 'uidex';
1502
- import type { ${typeParts.join(", ")} } from 'uidex';`;
1503
- const regParts = ["registerComponents(components);"];
1504
- if (hasPages) regParts.push("registerPages(pages);");
1505
- if (hasFeatures) regParts.push("registerFeatures(features);");
1506
- if (hasGitContext) {
1507
- const commitPart = gitContext.commit ? `, commit: "${gitContext.commit}"` : "";
1508
- regParts.push(`registerGitContext({ branch: "${gitContext.branch}"${commitPart} });`);
1509
- }
1510
- const registrations = `// Auto-register
1511
- ${regParts.join("\n")}
1512
- `;
1513
- const factoryArgs = ["components"];
1514
- if (hasPages) factoryArgs.push("pages");
1515
- if (hasFeatures) factoryArgs.push("features");
1516
- if (hasUiComponents) factoryArgs.push("uiComponents");
1517
- const defaultExport = `export default createUidexDevtools({ ${factoryArgs.join(", ")} });
1518
- `;
1519
- return `"use client";
1520
- // Auto-generated by uidex scanner
1521
- // Do not edit this file manually
1522
-
1523
- ${imports}
1524
-
1525
- export const components = {
1526
- ${entriesStr}
1527
- } satisfies UidexMap;
1528
-
1529
- export const componentIds = [${idsArrayStr}] as const;
1530
-
1531
- export type ComponentId = typeof componentIds[number];
1532
- ${pagesStr}${featuresStr}${uiComponentsStr}
1533
- ${registrations}
1534
- ${defaultExport}`;
1535
- }
1536
- function ensureOutputDir(outputPath) {
1537
- const dir = path6.dirname(outputPath);
1538
- if (!fs7.existsSync(dir)) {
1539
- fs7.mkdirSync(dir, { recursive: true });
1540
- }
1541
- }
1542
- function runScan(config) {
1543
- const components = {};
1544
- const pages = [];
1545
- const features = [];
1546
- let allUiComponents = [];
1547
- const routeDirs = /* @__PURE__ */ new Set();
1548
- let totalComponents = 0;
1549
- let totalFiles = 0;
1550
- const allProvenanceFiles = [];
1551
- const allExtractedPrimitives = [];
1552
- for (const source of config.sources) {
1553
- const sourceResult = findFilesForSource(source, config.exclude, config.configDir);
1554
- totalFiles += sourceResult.files.length;
1555
- for (const d of sourceResult.routeDirs) {
1556
- routeDirs.add(d === "." ? source.rootDir : `${source.rootDir}/${d}`);
1557
- }
1558
- console.log(
1559
- `Scanning ${source.rootDir}: ${sourceResult.files.length} files${source.prefix ? ` (\u2192 ${source.prefix}/*)` : ""}`
1560
- );
1561
- const sourceComponentIds = /* @__PURE__ */ new Map();
1562
- for (const file of sourceResult.files) {
1563
- const content = fs7.readFileSync(file.fullPath, "utf-8");
1564
- allProvenanceFiles.push({
1565
- absolutePath: toPosix(file.fullPath),
1566
- outputPath: file.outputPath,
1567
- content
1568
- });
1569
- const fileComponents = extractComponents(content);
1570
- const seenIdsInFile = /* @__PURE__ */ new Set();
1571
- for (const component of fileComponents) {
1572
- if (component.kind === "primitive") {
1573
- allExtractedPrimitives.push({
1574
- name: component.id,
1575
- line: component.line,
1576
- outputPath: file.outputPath,
1577
- absolutePath: toPosix(file.fullPath),
1578
- relativePath: file.relativePath,
1579
- rootDir: path6.resolve(config.configDir, source.rootDir)
1580
- });
1581
- continue;
1582
- }
1583
- if (seenIdsInFile.has(component.id)) continue;
1584
- seenIdsInFile.add(component.id);
1585
- if (!components[component.id]) {
1586
- components[component.id] = [];
1587
- }
1588
- components[component.id].push({
1589
- filePath: file.outputPath,
1590
- line: component.line,
1591
- kind: component.kind,
1592
- ...component.doc ? { doc: component.doc } : {}
1593
- });
1594
- if (!sourceComponentIds.has(component.id)) {
1595
- sourceComponentIds.set(component.id, []);
1596
- }
1597
- sourceComponentIds.get(component.id).push(file.relativePath);
1598
- totalComponents++;
1599
- }
1600
- }
1601
- const parsedPageDocs = /* @__PURE__ */ new Map();
1602
- for (const [dir, rawContent] of sourceResult.pageDocs) {
1603
- const { frontmatter, body } = parseFrontmatter(rawContent);
1604
- const description = frontmatter.description ?? parseBlockquoteDescription(body);
1605
- parsedPageDocs.set(dir, {
1606
- body: normalizeAcceptanceCriteria(body),
1607
- explicitComponents: frontmatter.components ?? [],
1608
- ...frontmatter.root ? { rootId: frontmatter.root } : {},
1609
- ...description ? { description } : {}
1610
- });
1611
- }
1612
- const sourcePages = buildPages(
1613
- parsedPageDocs,
1614
- sourceComponentIds,
1615
- source.rootDir
1616
- );
1617
- pages.push(...sourcePages);
1618
- const parsedFeatureDocs = /* @__PURE__ */ new Map();
1619
- for (const [dir, rawContent] of sourceResult.featureDocs) {
1620
- const { frontmatter, body } = parseFrontmatter(rawContent);
1621
- const featureDescription = frontmatter.description ?? parseBlockquoteDescription(body);
1622
- parsedFeatureDocs.set(dir, {
1623
- body,
1624
- explicitComponents: frontmatter.components ?? [],
1625
- ...featureDescription ? { description: featureDescription } : {}
1626
- });
1627
- }
1628
- const sourceFeatures = buildFeatures(parsedFeatureDocs, sourceComponentIds, source.rootDir);
1629
- features.push(...sourceFeatures);
1630
- }
1631
- allUiComponents = buildPrimitivesFromAnnotations(allExtractedPrimitives);
1632
- let provenanceLinks = 0;
1633
- if (allUiComponents.length > 0) {
1634
- const tsconfig = loadTsconfigPaths(config.configDir);
1635
- const provenance = computeProvenance(
1636
- allProvenanceFiles,
1637
- allUiComponents,
1638
- tsconfig.paths,
1639
- tsconfig.absoluteBaseUrl
1640
- );
1641
- allUiComponents = provenance.primitives;
1642
- provenanceLinks = provenance.provenanceLinks;
1643
- for (const [, locations] of Object.entries(components)) {
1644
- for (const loc of locations) {
1645
- const scopeSet = provenance.fileScopeSets.get(loc.filePath);
1646
- if (scopeSet && scopeSet.size > 0) {
1647
- loc.scopes = [...scopeSet].sort();
1648
- }
1649
- }
1650
- }
1651
- }
1652
- return {
1653
- components,
1654
- pages,
1655
- features,
1656
- uiComponents: allUiComponents,
1657
- routeDirs,
1658
- totalComponents,
1659
- totalFiles,
1660
- provenanceLinks
1661
- };
1662
- }
1663
- function printSummary(result) {
1664
- console.log("");
1665
- const uniqueIds = Object.keys(result.components).length;
1666
- console.log(
1667
- `Found ${result.totalComponents} components with ${uniqueIds} unique IDs in ${result.totalFiles} files
1668
- `
1669
- );
1670
- if (uniqueIds > 0) {
1671
- console.log("Components found:");
1672
- for (const [id, locations] of Object.entries(result.components)) {
1673
- for (const loc of locations) {
1674
- console.log(` "${id}" at ${loc.filePath}:${loc.line}`);
1675
- }
1676
- }
1677
- console.log("");
1678
- }
1679
- if (result.pages.length > 0) {
1680
- console.log(`Pages found: ${result.pages.length}`);
1681
- for (const page of result.pages) {
1682
- console.log(` ${page.dir}/ (${page.componentIds.length} components)`);
1683
- }
1684
- console.log("");
1685
- }
1686
- if (result.features.length > 0) {
1687
- console.log(`Features found: ${result.features.length}`);
1688
- for (const feature of result.features) {
1689
- console.log(` ${feature.dir}/ (${feature.componentIds.length} components)`);
1690
- }
1691
- console.log("");
1692
- }
1693
- if (result.uiComponents.length > 0) {
1694
- console.log(
1695
- `Primitives: ${result.uiComponents.length} detected, ${result.provenanceLinks} provenance links`
1696
- );
1697
- console.log("");
1698
- }
1699
- }
1700
- function checkCoverage(result, config) {
1701
- const allComponentIds = Object.keys(result.components);
1702
- const coveredIds = /* @__PURE__ */ new Set([
1703
- ...result.pages.flatMap((p) => p.componentIds),
1704
- ...result.features.flatMap((f) => f.componentIds)
1705
- ]);
1706
- const uncoveredIds = allComponentIds.filter((id) => !coveredIds.has(id));
1707
- const orphanedPages = result.pages.filter((p) => p.componentIds.length === 0);
1708
- const orphanedFeatures = result.features.filter(
1709
- (f) => f.componentIds.every((id) => !result.components[id])
1710
- );
1711
- const pagesWithoutDescription = result.pages.filter((p) => !p.description);
1712
- const featuresWithoutDescription = result.features.filter((f) => !f.description);
1713
- let passed = true;
1714
- if (uncoveredIds.length > 0) {
1715
- passed = false;
1716
- console.log(`Components missing doc coverage (${uncoveredIds.length}):`);
1717
- for (const id of uncoveredIds.sort()) {
1718
- const locations = result.components[id];
1719
- for (const loc of locations) {
1720
- console.log(` "${id}" at ${loc.filePath}:${loc.line}`);
1721
- }
1722
- }
1723
- console.log(" Hint: add a UIDEX_PAGE.md in the component directory, or list the");
1724
- console.log(" component in a UIDEX_FEATURE.md (e.g. features/<name>/UIDEX_FEATURE.md)");
1725
- console.log("");
1726
- }
1727
- if (orphanedPages.length > 0) {
1728
- passed = false;
1729
- console.log(`UIDEX_PAGE.md files with no components (${orphanedPages.length}):`);
1730
- for (const page of orphanedPages) {
1731
- console.log(` ${page.dir}/UIDEX_PAGE.md`);
1732
- }
1733
- console.log("");
1734
- }
1735
- if (orphanedFeatures.length > 0) {
1736
- passed = false;
1737
- console.log(`UIDEX_FEATURE.md files referencing no known components (${orphanedFeatures.length}):`);
1738
- for (const feature of orphanedFeatures) {
1739
- console.log(` ${feature.dir}/UIDEX_FEATURE.md`);
1740
- }
1741
- console.log("");
1742
- }
1743
- if (pagesWithoutDescription.length > 0 || featuresWithoutDescription.length > 0) {
1744
- passed = false;
1745
- const total = pagesWithoutDescription.length + featuresWithoutDescription.length;
1746
- console.log(`Docs missing > description blockquote (${total}):`);
1747
- for (const page of pagesWithoutDescription) {
1748
- console.log(` ${page.dir}/UIDEX_PAGE.md`);
1749
- }
1750
- for (const feature of featuresWithoutDescription) {
1751
- console.log(` ${feature.dir}/UIDEX_FEATURE.md`);
1752
- }
1753
- console.log("");
1754
- }
1755
- const featureDirs = [];
1756
- let hasAnyFeaturesDir = false;
1757
- for (const source of config.sources) {
1758
- const featuresPath = path6.resolve(config.configDir, source.rootDir, "features");
1759
- if (fs7.existsSync(featuresPath) && fs7.statSync(featuresPath).isDirectory()) {
1760
- hasAnyFeaturesDir = true;
1761
- const entries = fs7.readdirSync(featuresPath, { withFileTypes: true });
1762
- for (const entry of entries) {
1763
- if (!entry.isDirectory()) continue;
1764
- const featureDocPath = path6.join(featuresPath, entry.name, UIDEX_FEATURE_FILENAME);
1765
- if (!fs7.existsSync(featureDocPath)) {
1766
- const rel = source.prefix ? `${source.prefix}/features/${entry.name}` : `features/${entry.name}`;
1767
- featureDirs.push(rel);
1768
- }
1769
- }
1770
- }
1771
- }
1772
- if (featureDirs.length > 0) {
1773
- passed = false;
1774
- console.log(`Feature directories missing UIDEX_FEATURE.md (${featureDirs.length}):`);
1775
- for (const dir of featureDirs) {
1776
- console.log(` ${dir}/`);
1777
- }
1778
- console.log("");
1779
- }
1780
- if (!hasAnyFeaturesDir) {
1781
- console.log(
1782
- "Note: no features/ directory found under any scanner root. Consider adding"
1783
- );
1784
- console.log(
1785
- " a features/ directory for cross-cutting feature documentation."
1786
- );
1787
- console.log("");
1788
- }
1789
- const pageDirs = new Set(result.pages.map((p) => p.dir));
1790
- const routesWithoutPage = [...result.routeDirs].filter((dir) => !pageDirs.has(dir)).sort();
1791
- if (routesWithoutPage.length > 0) {
1792
- passed = false;
1793
- console.log(`Route directories missing UIDEX_PAGE.md (${routesWithoutPage.length}):`);
1794
- for (const dir of routesWithoutPage) {
1795
- console.log(` ${dir}/`);
1796
- }
1797
- console.log(" Hint: each route with a page.tsx should have its own UIDEX_PAGE.md");
1798
- console.log(" to prevent components from being absorbed by a parent page doc.");
1799
- console.log("");
1800
- }
1801
- if (passed) {
1802
- const totalDocs = result.pages.length + result.features.length;
1803
- console.log(
1804
- `All ${allComponentIds.length} components covered by ${totalDocs} UIDEX docs (${result.pages.length} pages, ${result.features.length} features)`
1805
- );
1806
- }
1807
- return passed;
1808
- }
1809
- function printConfig(config) {
1810
- console.log("Configuration:");
1811
- console.log(` Sources: ${config.sources.length}`);
1812
- for (const source of config.sources) {
1813
- const prefix = source.prefix ? ` (prefix: ${source.prefix})` : "";
1814
- console.log(` - ${source.rootDir}${prefix}`);
1815
- console.log(` Include: ${source.include.join(", ")}`);
1816
- if (source.exclude) {
1817
- console.log(` Exclude: ${source.exclude.join(", ")}`);
1818
- }
1819
- }
1820
- console.log(` Global exclude: ${config.exclude.join(", ")}`);
1821
- console.log(` Output: ${config.outputPath}
1822
- `);
1823
- }
1824
- function scanSingle(configDir) {
1825
- const config = loadConfig({ configDir });
1826
- printConfig(config);
1827
- const result = runScan(config);
1828
- printSummary(result);
1829
- const outputPath = path6.resolve(config.configDir, config.outputPath);
1830
- ensureOutputDir(outputPath);
1831
- const gitCtx = detectGitContext();
1832
- if (gitCtx.branch) {
1833
- console.log(`Git context: branch=${gitCtx.branch}${gitCtx.commit ? ` commit=${gitCtx.commit.slice(0, 8)}` : ""}`);
1834
- }
1835
- const output = generateOutput(result.components, result.pages, result.features, gitCtx, result.uiComponents);
1836
- fs7.writeFileSync(outputPath, output, "utf-8");
1837
- console.log(`Generated: ${config.outputPath}`);
1838
- const testOutputPath = outputPath.replace(/\.ts$/, ".test.ts");
1839
- const testOutput = generateTestOutput(result.components, result.pages, result.features);
1840
- fs7.writeFileSync(testOutputPath, testOutput, "utf-8");
1841
- console.log(`Generated: ${config.outputPath.replace(/\.ts$/, ".test.ts")}`);
1842
- }
1843
- function printConfigHeader(configDir) {
1844
- console.log(`--- ${path6.relative(process.cwd(), configDir)}/ ---
1845
- `);
1846
- }
1847
- function scan() {
1848
- const isAudit = process.argv.includes("--audit") || process.argv.includes("--check") || process.argv.includes("--lint");
1849
- const isJson = process.argv.includes("--json");
1850
- const isVerbose = process.argv.includes("--verbose");
1851
- const configs = resolveConfigs();
1852
- if (configs.length === 0) {
1853
- console.error("No .uidex.json found in this directory or subdirectories.");
1854
- console.error("Run `npx uidex init` to create one.");
1855
- process.exit(1);
1856
- }
1857
- const isMulti = configs.length > 1 || configs[0].configDir !== process.cwd();
1858
- if (isMulti) {
1859
- console.log(`Found ${configs.length} uidex config(s):`);
1860
- for (const c of configs) {
1861
- console.log(` ${path6.relative(process.cwd(), c.configDir)}/`);
1862
- }
1863
- console.log("");
1864
- }
1865
- if (isAudit) {
1866
- const { runLint: runLint2, printLintReport: printLintReport2 } = (init_lint(), __toCommonJS(lint_exports));
1867
- console.log("uidex audit starting...\n");
1868
- let checkPassed = true;
1869
- for (const c of configs) {
1870
- if (isMulti) printConfigHeader(c.configDir);
1871
- const config = loadConfig({ configDir: c.configDir });
1872
- const result = runScan(config);
1873
- const uniqueIds = Object.keys(result.components).length;
1874
- console.log(
1875
- `Scanned ${result.totalFiles} files, found ${result.totalComponents} components (${uniqueIds} unique IDs), ${result.pages.length} pages, ${result.features.length} features
1876
- `
1877
- );
1878
- const passed = checkCoverage(result, config);
1879
- if (!passed) checkPassed = false;
1880
- }
1881
- let scopeLeakCount = 0;
1882
- const allScopeLeakResults = [];
1883
- for (const c of configs) {
1884
- const config = loadConfig({ configDir: c.configDir, silent: true });
1885
- const result = runScan(config);
1886
- if (result.uiComponents.length > 0) {
1887
- const aggregatedFiles = [];
1888
- for (const source of config.sources) {
1889
- const sourceResult = findFilesForSource(source, config.exclude, config.configDir);
1890
- aggregatedFiles.push(...sourceResult.files);
1891
- }
1892
- const scopeLeakResult = runScopeLeakCheck(
1893
- config.configDir,
1894
- aggregatedFiles,
1895
- result.uiComponents
1896
- );
1897
- allScopeLeakResults.push(scopeLeakResult);
1898
- scopeLeakCount += scopeLeakResult.errors.length;
1899
- if (!isJson) {
1900
- printScopeLeakReport(scopeLeakResult);
1901
- }
1902
- }
1903
- }
1904
- console.log("\n--- Lint ---\n");
1905
- let totalViolations = 0;
1906
- for (const c of configs) {
1907
- if (isMulti) printConfigHeader(c.configDir);
1908
- const result = runLint2(c.configDir, { verbose: isVerbose });
1909
- totalViolations += result.stats.totalViolations;
1910
- if (isJson) {
1911
- const jsonResult = {
1912
- ...result,
1913
- scopeLeaks: allScopeLeakResults.flatMap((r) => r.errors)
1914
- };
1915
- console.log(JSON.stringify(jsonResult, null, 2));
1916
- } else {
1917
- printLintReport2(result);
1918
- }
1919
- }
1920
- process.exit(!checkPassed || totalViolations > 0 || scopeLeakCount > 0 ? 1 : 0);
1921
- }
1922
- console.log("uidex scanner starting...\n");
1923
- for (const c of configs) {
1924
- if (isMulti) printConfigHeader(c.configDir);
1925
- scanSingle(c.configDir);
1926
- }
1927
- }
1928
- var fs7, path6, import_child_process, DEFAULT_SOURCE, DEFAULT_SKIP_PATHS, DEFAULT_LINT_CONFIG, DEFAULT_CONFIG;
1929
- var init_scan = __esm({
1930
- "scripts/scan.ts"() {
1931
- "use strict";
1932
- fs7 = __toESM(require("fs"), 1);
1933
- path6 = __toESM(require("path"), 1);
1934
- import_child_process = require("child_process");
1935
- init_config_discovery();
1936
- init_scanner_utils();
1937
- init_primitives();
1938
- init_provenance();
1939
- init_scope_leak();
1940
- DEFAULT_SOURCE = {
1941
- rootDir: "src",
1942
- include: ["**/*.tsx", "**/*.jsx"]
1943
- };
1944
- DEFAULT_SKIP_PATHS = [
1945
- "**/contexts/**",
1946
- "**/providers/**",
1947
- "**/hooks/**"
1948
- ];
1949
- DEFAULT_LINT_CONFIG = {
1950
- interactiveElements: [
1951
- "button",
1952
- "a",
1953
- "input",
1954
- "select",
1955
- "textarea",
1956
- "Button",
1957
- "Input",
1958
- "Select",
1959
- "Checkbox"
1960
- ],
1961
- regionElements: [
1962
- "section",
1963
- "nav",
1964
- "form",
1965
- "table",
1966
- "aside",
1967
- "article",
1968
- "header",
1969
- "footer",
1970
- "main"
1971
- ],
1972
- skipPaths: DEFAULT_SKIP_PATHS,
1973
- skipPathDefaults: true
1974
- };
1975
- DEFAULT_CONFIG = {
1976
- sources: [DEFAULT_SOURCE],
1977
- exclude: ["**/*.test.*", "**/*.spec.*", "**/node_modules/**", "**/*.gen.ts"],
1978
- outputPath: "src/uidex.gen.ts"
1979
- };
1980
- }
1981
- });
1982
-
1983
- // scripts/init.ts
1984
- var fs = __toESM(require("fs"), 1);
1985
- var path = __toESM(require("path"), 1);
1986
- var readline = __toESM(require("readline"), 1);
1987
- init_cli_utils();
1988
- function log(message) {
1989
- console.log(message);
1990
- }
1991
- function detectProject() {
1992
- const cwd = process.cwd();
1993
- const packageJsonPath = path.join(cwd, "package.json");
1994
- let packageJson = {};
1995
- if (fs.existsSync(packageJsonPath)) {
1996
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
1997
- }
1998
- const deps = {
1999
- ...packageJson.dependencies,
2000
- ...packageJson.devDependencies
2001
- };
2002
- const name = packageJson.name || path.basename(cwd);
2003
- const usesTypeScript = fs.existsSync(path.join(cwd, "tsconfig.json")) || !!deps["typescript"];
2004
- const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
2005
- const srcDir = hasSrcDir ? "src" : ".";
2006
- if (deps["next"]) {
2007
- const usesAppRouter = fs.existsSync(path.join(cwd, "src", "app")) || fs.existsSync(path.join(cwd, "app"));
2008
- return {
2009
- type: "nextjs",
2010
- name,
2011
- srcDir,
2012
- usesTypeScript,
2013
- usesAppRouter
2014
- };
2015
- }
2016
- if (deps["vite"] || fs.existsSync(path.join(cwd, "vite.config.ts")) || fs.existsSync(path.join(cwd, "vite.config.js"))) {
2017
- return {
2018
- type: "vite",
2019
- name,
2020
- srcDir,
2021
- usesTypeScript,
2022
- usesAppRouter: false
2023
- };
2024
- }
2025
- if (deps["react-scripts"]) {
2026
- return {
2027
- type: "cra",
2028
- name,
2029
- srcDir,
2030
- usesTypeScript,
2031
- usesAppRouter: false
2032
- };
2033
- }
2034
- return {
2035
- type: "unknown",
2036
- name,
2037
- srcDir,
2038
- usesTypeScript,
2039
- usesAppRouter: false
2040
- };
2041
- }
2042
- function createConfigContent(project) {
2043
- const extensions = ["**/*.tsx", "**/*.jsx"];
2044
- const exclude = ["**/*.test.*", "**/*.spec.*", "**/*.gen.ts"];
2045
- let sources;
2046
- if (project.type === "nextjs" && project.usesAppRouter) {
2047
- const prefix = project.srcDir === "src" ? "src/" : "";
2048
- sources = [
2049
- { rootDir: `${prefix}app`, include: extensions },
2050
- { rootDir: `${prefix}components`, include: extensions }
2051
- ];
2052
- const featuresDir = path.join(process.cwd(), `${prefix}features`);
2053
- if (fs.existsSync(featuresDir) && fs.statSync(featuresDir).isDirectory()) {
2054
- sources.push({ rootDir: `${prefix}features`, include: extensions });
2055
- }
2056
- } else {
2057
- sources = [{ rootDir: project.srcDir, include: extensions }];
2058
- }
2059
- const config = {
2060
- $schema: "node_modules/uidex/uidex.schema.json",
2061
- defaults: {
2062
- color: "#3b82f6",
2063
- borderStyle: "solid",
2064
- borderWidth: 2,
2065
- showLabel: true,
2066
- labelPosition: "top-left"
2067
- },
2068
- colors: {
2069
- primary: "#3b82f6",
2070
- secondary: "#8b5cf6",
2071
- success: "#10b981",
2072
- warning: "#f59e0b",
2073
- error: "#ef4444",
2074
- info: "#0ea5e9"
2075
- },
2076
- scanner: {
2077
- sources,
2078
- exclude,
2079
- outputPath: `${project.srcDir}/uidex.gen.ts`
2080
- }
2081
- };
2082
- return JSON.stringify(config, null, 2);
2083
- }
2084
- function addToGitignore(entry) {
2085
- const cwd = process.cwd();
2086
- const gitignorePath = path.join(cwd, ".gitignore");
2087
- let content = "";
2088
- if (fs.existsSync(gitignorePath)) {
2089
- content = fs.readFileSync(gitignorePath, "utf-8");
2090
- }
2091
- const lines = content.split("\n");
2092
- if (lines.some((line) => line.trim() === entry)) {
2093
- return false;
2094
- }
2095
- const newContent = content.endsWith("\n") || content === "" ? `${content}${entry}
2096
- ` : `${content}
2097
- ${entry}
2098
- `;
2099
- fs.writeFileSync(gitignorePath, newContent, "utf-8");
2100
- return true;
2101
- }
2102
- function createPrompt() {
2103
- return readline.createInterface({
2104
- input: process.stdin,
2105
- output: process.stdout
2106
- });
2107
- }
2108
- async function askYesNo(rl, question, defaultYes = true) {
2109
- const hint = defaultYes ? "[Y/n]" : "[y/N]";
2110
- return new Promise((resolve6) => {
2111
- rl.question(`${question} ${colors.dim}${hint}${colors.reset} `, (answer) => {
2112
- const normalized = answer.trim().toLowerCase();
2113
- if (normalized === "") {
2114
- resolve6(defaultYes);
2115
- } else {
2116
- resolve6(normalized === "y" || normalized === "yes");
2117
- }
2118
- });
2119
- });
2120
- }
2121
- function getEntryPointHint(project) {
2122
- if (project.type === "nextjs") {
2123
- if (project.usesAppRouter) {
2124
- return project.srcDir === "src" ? "src/app/layout.tsx" : "app/layout.tsx";
2125
- }
2126
- return project.srcDir === "src" ? "src/pages/_app.tsx" : "pages/_app.tsx";
2127
- }
2128
- return project.srcDir === "src" ? "src/main.tsx" : "main.tsx";
2129
- }
2130
- async function init() {
2131
- heading("uidex init");
2132
- const cwd = process.cwd();
2133
- const configPath = path.join(cwd, ".uidex.json");
2134
- if (fs.existsSync(configPath)) {
2135
- warn(".uidex.json already exists");
2136
- const rl = createPrompt();
2137
- const overwrite = await askYesNo(rl, "Overwrite existing config?", false);
2138
- rl.close();
2139
- if (!overwrite) {
2140
- info("Keeping existing configuration");
2141
- return;
2142
- }
2143
- }
2144
- const project = detectProject();
2145
- log(`Detected project: ${colors.bold}${project.name}${colors.reset}`);
2146
- log(`Project type: ${colors.bold}${project.type}${colors.reset}`);
2147
- log(
2148
- `Language: ${colors.bold}${project.usesTypeScript ? "TypeScript" : "JavaScript"}${colors.reset}`
2149
- );
2150
- if (project.type === "nextjs") {
2151
- log(
2152
- `Router: ${colors.bold}${project.usesAppRouter ? "App Router" : "Pages Router"}${colors.reset}`
2153
- );
2154
- }
2155
- heading("Creating configuration");
2156
- const configContent = createConfigContent(project);
2157
- fs.writeFileSync(configPath, configContent, "utf-8");
2158
- success("Created .uidex.json");
2159
- const genPatterns = ["*.gen.ts", "*.gen.test.ts"];
2160
- for (const pattern of genPatterns) {
2161
- if (addToGitignore(pattern)) {
2162
- success(`Added ${pattern} to .gitignore`);
2163
- } else {
2164
- info(`${pattern} already in .gitignore`);
2165
- }
2166
- }
2167
- heading("Next steps");
2168
- const entryPoint = getEntryPointHint(project);
2169
- log("1. Add data-uidex attributes to elements you want to track:");
2170
- log("");
2171
- log(` ${colors.green}<button${colors.reset} data-uidex="submit-btn"${colors.green}>${colors.reset}Submit${colors.green}</button>${colors.reset}`);
2172
- log("");
2173
- log("2. Run the scanner to generate the components registry:");
2174
- log("");
2175
- log(` ${colors.yellow}npx uidex-scan${colors.reset}`);
2176
- log("");
2177
- log("3. Import the generated file in your entry point:");
2178
- log("");
2179
- log(` ${colors.dim}// ${entryPoint}${colors.reset}`);
2180
- log(` ${colors.cyan}import${colors.reset} './${project.srcDir === "src" ? "" : "src/"}uidex.gen';`);
2181
- log("");
2182
- log("4. Add the UidexDevtools component anywhere in your app:");
2183
- log("");
2184
- log(` ${colors.cyan}import${colors.reset} { UidexDevtools } ${colors.cyan}from${colors.reset} 'uidex';`);
2185
- log("");
2186
- log(` ${colors.green}<UidexDevtools />${colors.reset}`);
2187
- log("");
2188
- success("Setup complete!");
2189
- log("");
2190
- }
2191
-
2192
- // scripts/cli.ts
2193
- init_scan();
2194
-
2195
- // scripts/scaffold.ts
2196
- var fs8 = __toESM(require("fs"), 1);
2197
- var path7 = __toESM(require("path"), 1);
2198
- init_scan();
2199
- init_scanner_utils();
2200
- init_cli_utils();
2201
- function toTestFileName(dir, title) {
2202
- if (title) {
2203
- return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2204
- }
2205
- const segments = dir.split("/").filter(Boolean);
2206
- return segments[segments.length - 1] || "unnamed";
2207
- }
2208
- function generateTestFile(entry, fixtureImport) {
2209
- const title = parseMarkdownTitle(entry.content) ?? entry.dir;
2210
- const componentList = entry.componentIds.map((id) => `"${id}"`).join(", ");
2211
- const docFile = entry.kind === "page" ? "UIDEX_PAGE.md" : "UIDEX_FEATURE.md";
2212
- let body = "";
2213
- body += `// Auto-generated by uidex scaffold from ${entry.dir}/${docFile}
2214
- `;
2215
- body += `// Fill in test implementations below
2216
-
2217
- `;
2218
- body += `import { test, expect } from '${fixtureImport}';
2219
-
2220
- `;
2221
- body += `test.describe('${title}', () => {
2222
- `;
2223
- if (entry.kind === "page") {
2224
- const route = formatRoute(entry.dir);
2225
- body += ` // Route: ${route}
2226
- `;
2227
- body += ` // Components: ${componentList}
2228
-
2229
- `;
2230
- body += ` test.beforeEach(async ({ page }) => {
2231
- `;
2232
- body += ` await page.goto('${route}');
2233
- `;
2234
- body += ` });
2235
- `;
2236
- const criteria = parseAcceptanceCriteria(entry.content);
2237
- if (criteria.length > 0) {
2238
- body += "\n";
2239
- for (const criterion of criteria) {
2240
- const escaped = criterion.replace(/'/g, "\\'");
2241
- body += ` test.todo('${escaped}');
2242
- `;
2243
- }
2244
- }
2245
- } else {
2246
- body += ` // Components: ${componentList}
2247
- `;
2248
- }
2249
- body += `});
2250
- `;
2251
- return body;
2252
- }
2253
- function scaffold(outputDir, configDir) {
2254
- const dir = outputDir ?? "e2e";
2255
- heading("uidex scaffold");
2256
- info(`Output directory: ${dir}/`);
2257
- const config = loadConfig({ configDir });
2258
- const result = runScan(config);
2259
- if (result.pages.length === 0 && result.features.length === 0) {
2260
- warn("No pages or features found. Create UIDEX_PAGE.md or UIDEX_FEATURE.md files first.");
2261
- return;
2262
- }
2263
- const absDir = path7.resolve(process.cwd(), dir);
2264
- if (!fs8.existsSync(absDir)) {
2265
- fs8.mkdirSync(absDir, { recursive: true });
2266
- }
2267
- const fixtureImport = "./fixtures";
2268
- let generated = 0;
2269
- let skipped = 0;
2270
- const entries = [
2271
- ...result.pages.map((p) => ({ kind: "page", ...p })),
2272
- ...result.features.map((f) => ({ kind: "feature", ...f }))
2273
- ];
2274
- for (const entry of entries) {
2275
- const title = parseMarkdownTitle(entry.content);
2276
- const fileName = `${entry.kind}-${toTestFileName(entry.dir, title)}.spec.ts`;
2277
- const filePath = path7.join(absDir, fileName);
2278
- if (fs8.existsSync(filePath)) {
2279
- info(`Skipped ${fileName} (already exists)`);
2280
- skipped++;
2281
- continue;
2282
- }
2283
- const content = generateTestFile(entry, fixtureImport);
2284
- fs8.writeFileSync(filePath, content, "utf-8");
2285
- success(`Generated ${fileName}`);
2286
- generated++;
2287
- }
2288
- const fixturesPath = path7.join(absDir, "fixtures.ts");
2289
- if (!fs8.existsSync(fixturesPath)) {
2290
- fs8.writeFileSync(
2291
- fixturesPath,
2292
- `export { test, expect } from 'uidex/playwright';
2293
- `,
2294
- "utf-8"
2295
- );
2296
- success("Generated fixtures.ts");
2297
- generated++;
2298
- }
2299
- console.log("");
2300
- info(`${generated} file(s) generated, ${skipped} skipped`);
2301
- if (generated > 0) {
2302
- console.log("");
2303
- info("Next steps:");
2304
- console.log(" 1. Replace test.todo() calls with test implementations");
2305
- console.log(" 2. Use the uidex fixture for type-safe selectors:");
2306
- console.log("");
2307
- console.log(" test('add todo', async ({ uidex }) => {");
2308
- console.log(" await uidex('todo-input').fill('Buy milk');");
2309
- console.log(" await uidex('todo-add-button').click();");
2310
- console.log(" });");
2311
- console.log("");
2312
- }
2313
- }
2314
-
2315
- // scripts/claude-setup.ts
2316
- var fs9 = __toESM(require("fs"), 1);
2317
- var path8 = __toESM(require("path"), 1);
2318
- init_cli_utils();
2319
- function readTemplate(filename) {
2320
- const candidates = [
2321
- // Built package: __dirname = <pkg>/dist/scripts → ../../claude/
2322
- path8.resolve(__dirname, "..", "..", "claude", filename),
2323
- // Dev mode: __dirname = <repo>/scripts → ../claude/
2324
- path8.resolve(__dirname, "..", "claude", filename)
2325
- ];
2326
- for (const candidate of candidates) {
2327
- try {
2328
- return fs9.readFileSync(candidate, "utf-8");
2329
- } catch {
2330
- }
2331
- }
2332
- throw new Error(
2333
- `Template not found: ${filename}. The uidex package may be corrupted \u2014 try reinstalling.`
2334
- );
2335
- }
2336
- function ensureDir(dirPath) {
2337
- fs9.mkdirSync(dirPath, { recursive: true });
2338
- }
2339
- function addRules() {
2340
- const cwd = process.cwd();
2341
- const targetDir = path8.join(cwd, ".claude", "rules");
2342
- const targetPath = path8.join(targetDir, "uidex.md");
2343
- const content = readTemplate("rules.md");
2344
- ensureDir(targetDir);
2345
- fs9.writeFileSync(targetPath, content, "utf-8");
2346
- success("Added .claude/rules/uidex.md");
2347
- }
2348
- function removeRules() {
2349
- const targetPath = path8.join(process.cwd(), ".claude", "rules", "uidex.md");
2350
- try {
2351
- fs9.unlinkSync(targetPath);
2352
- success("Removed .claude/rules/uidex.md");
2353
- } catch (e) {
2354
- if (e.code === "ENOENT") {
2355
- info(".claude/rules/uidex.md does not exist");
2356
- } else {
2357
- throw e;
2358
- }
2359
- }
2360
- }
2361
- function addSkill() {
2362
- const cwd = process.cwd();
2363
- const targetDir = path8.join(cwd, ".claude", "commands", "uidex");
2364
- const targetPath = path8.join(targetDir, "audit.md");
2365
- const content = readTemplate("audit-command.md");
2366
- ensureDir(targetDir);
2367
- fs9.writeFileSync(targetPath, content, "utf-8");
2368
- success("Added .claude/commands/uidex/audit.md (/uidex:audit)");
2369
- const legacyPath = path8.join(cwd, ".claude", "commands", "uidex-audit.md");
2370
- try {
2371
- fs9.unlinkSync(legacyPath);
2372
- } catch {
2373
- }
2374
- }
2375
- function removeSkill() {
2376
- const targetPath = path8.join(process.cwd(), ".claude", "commands", "uidex", "audit.md");
2377
- try {
2378
- fs9.unlinkSync(targetPath);
2379
- success("Removed .claude/commands/uidex/audit.md");
2380
- const dir = path8.dirname(targetPath);
2381
- try {
2382
- fs9.rmdirSync(dir);
2383
- } catch {
2384
- }
2385
- } catch (e) {
2386
- if (e.code === "ENOENT") {
2387
- info(".claude/commands/uidex/audit.md does not exist");
2388
- } else {
2389
- throw e;
2390
- }
2391
- }
2392
- const legacyPath = path8.join(process.cwd(), ".claude", "commands", "uidex-audit.md");
2393
- try {
2394
- fs9.unlinkSync(legacyPath);
2395
- } catch {
2396
- }
2397
- }
2398
- var UIDEX_HOOK_COMMAND = "npx uidex scan --audit";
2399
- function isUidexHookEntry(entry) {
2400
- return entry.hooks?.some((h) => h.command === UIDEX_HOOK_COMMAND) ?? false;
2401
- }
2402
- function readSettings(settingsPath) {
2403
- try {
2404
- return JSON.parse(fs9.readFileSync(settingsPath, "utf-8"));
2405
- } catch (e) {
2406
- if (e.code === "ENOENT") {
2407
- return {};
2408
- }
2409
- error(`Failed to parse .claude/settings.json: ${e.message}`);
2410
- process.exit(1);
2411
- }
2412
- }
2413
- function addHooks() {
2414
- const cwd = process.cwd();
2415
- const settingsDir = path8.join(cwd, ".claude");
2416
- const settingsPath = path8.join(settingsDir, "settings.json");
2417
- const settings = readSettings(settingsPath);
2418
- if (!settings.hooks) {
2419
- settings.hooks = {};
2420
- }
2421
- if (!Array.isArray(settings.hooks.PostToolUse)) {
2422
- settings.hooks.PostToolUse = [];
2423
- }
2424
- if (settings.hooks.PostToolUse.some(isUidexHookEntry)) {
2425
- info("uidex hook already configured in .claude/settings.json");
2426
- return;
2427
- }
2428
- settings.hooks.PostToolUse.push({
2429
- matcher: "Edit|Write",
2430
- hooks: [
2431
- {
2432
- type: "command",
2433
- command: UIDEX_HOOK_COMMAND,
2434
- timeout: 30
2435
- }
2436
- ]
2437
- });
2438
- ensureDir(settingsDir);
2439
- fs9.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2440
- success("Added uidex hook to .claude/settings.json");
2441
- }
2442
- function removeHooks() {
2443
- const settingsPath = path8.join(process.cwd(), ".claude", "settings.json");
2444
- const settings = readSettings(settingsPath);
2445
- if (!Array.isArray(settings.hooks?.PostToolUse)) {
2446
- info("No PostToolUse hooks configured");
2447
- return;
2448
- }
2449
- const before = settings.hooks.PostToolUse.length;
2450
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
2451
- (entry) => !isUidexHookEntry(entry)
2452
- );
2453
- const after = settings.hooks.PostToolUse.length;
2454
- if (before === after) {
2455
- info("No uidex hook found in .claude/settings.json");
2456
- return;
2457
- }
2458
- if (settings.hooks.PostToolUse.length === 0) {
2459
- delete settings.hooks.PostToolUse;
2460
- }
2461
- if (Object.keys(settings.hooks).length === 0) {
2462
- delete settings.hooks;
2463
- }
2464
- fs9.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
2465
- success("Removed uidex hook from .claude/settings.json");
2466
- }
2467
- function claudeInstall() {
2468
- heading("uidex claude install");
2469
- addRules();
2470
- addSkill();
2471
- addHooks();
2472
- console.log("");
2473
- success("Claude Code integration ready");
2474
- }
2475
- function claudeUninstall() {
2476
- heading("uidex claude uninstall");
2477
- removeRules();
2478
- removeSkill();
2479
- removeHooks();
2480
- console.log("");
2481
- success("Claude Code integration removed");
2482
- }
2483
- function printClaudeHelp() {
2484
- heading("uidex claude");
2485
- console.log("Manage Claude Code integration for uidex.\n");
2486
- console.log("Usage: uidex claude <command> [action]\n");
2487
- console.log("Commands:");
2488
- console.log(" install Add rules, skill, and hooks");
2489
- console.log(" uninstall Remove rules, skill, and hooks");
2490
- console.log(" rules [add|remove] Manage .claude/rules/uidex.md");
2491
- console.log(" skill [add|remove] Manage .claude/commands/uidex/audit.md (/uidex:audit)");
2492
- console.log(" hooks [add|remove] Manage .claude/settings.json hook");
2493
- console.log("");
2494
- }
2495
-
2496
- // scripts/login.ts
2497
- var import_http = __toESM(require("http"), 1);
2498
- var import_crypto = require("crypto");
2499
- init_cli_utils();
2500
-
2501
- // scripts/config.ts
2502
- var import_fs = __toESM(require("fs"), 1);
2503
- var import_path = __toESM(require("path"), 1);
2504
- var import_os = __toESM(require("os"), 1);
2505
- init_config_discovery();
2506
- init_scan();
2507
- var CONFIG_DIR = import_path.default.join(import_os.default.homedir(), ".uidex");
2508
- var CONFIG_PATH = import_path.default.join(CONFIG_DIR, "config.json");
2509
- var DEFAULT_ENDPOINT = "https://app.uidex.dev";
2510
- function ensureConfigDir() {
2511
- import_fs.default.mkdirSync(CONFIG_DIR, { mode: 448, recursive: true });
2512
- }
2513
- function readGlobalConfig() {
2514
- try {
2515
- const raw = import_fs.default.readFileSync(CONFIG_PATH, "utf-8");
2516
- return JSON.parse(raw);
2517
- } catch {
2518
- return { links: {} };
2519
- }
2520
- }
2521
- function writeGlobalConfig(config) {
2522
- ensureConfigDir();
2523
- const tmp = CONFIG_PATH + ".tmp";
2524
- import_fs.default.writeFileSync(tmp, JSON.stringify(config, null, 2), {
2525
- mode: 384
2526
- });
2527
- import_fs.default.renameSync(tmp, CONFIG_PATH);
2528
- }
2529
- function getToken() {
2530
- return process.env.UIDEX_TOKEN || readGlobalConfig().token;
2531
- }
2532
- function setToken(token) {
2533
- const config = readGlobalConfig();
2534
- config.token = token;
2535
- writeGlobalConfig(config);
2536
- }
2537
- function removeToken() {
2538
- const config = readGlobalConfig();
2539
- delete config.token;
2540
- writeGlobalConfig(config);
2541
- }
2542
- function getLinkContext(cwd) {
2543
- const dir = cwd || process.cwd();
2544
- const config = readGlobalConfig();
2545
- return config.links[dir];
2546
- }
2547
- function setLinkContext(link2, cwd) {
2548
- const dir = cwd || process.cwd();
2549
- const config = readGlobalConfig();
2550
- config.links[dir] = link2;
2551
- writeGlobalConfig(config);
2552
- }
2553
- function readProjectConfig() {
2554
- const configs = resolveConfigs();
2555
- if (configs.length === 0) return {};
2556
- return readConfigFile(configs[0].configDir) ?? {};
2557
- }
2558
- function resolveEndpoint(link2) {
2559
- return process.env.UIDEX_ENDPOINT || link2?.endpoint || readProjectConfig().api?.endpoint || DEFAULT_ENDPOINT;
2560
- }
2561
- function resolveContext() {
2562
- const config = readGlobalConfig();
2563
- const link2 = config.links[process.cwd()];
2564
- const token = process.env.UIDEX_TOKEN || config.token;
2565
- if (!token) return null;
2566
- const orgId = process.env.UIDEX_ORG_ID || link2?.orgId;
2567
- const projectId = process.env.UIDEX_PROJECT_ID || link2?.projectId;
2568
- if (!orgId || !projectId) return null;
2569
- return {
2570
- token,
2571
- endpoint: resolveEndpoint(link2),
2572
- orgId,
2573
- orgSlug: link2?.orgSlug || "",
2574
- projectId,
2575
- projectSlug: link2?.projectSlug || "",
2576
- environment: link2?.environment || ""
2577
- };
2578
- }
2579
- function requireContext() {
2580
- const ctx = resolveContext();
2581
- if (!ctx) {
2582
- const { error: error2 } = (init_cli_utils(), __toCommonJS(cli_utils_exports));
2583
- error2("No project linked. Run `uidex link` first.");
2584
- process.exit(1);
2585
- }
2586
- return ctx;
2587
- }
2588
-
2589
- // scripts/login.ts
2590
- function openBrowser(url) {
2591
- const { exec } = require("child_process");
2592
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
2593
- exec(`${cmd} "${url}"`);
2594
- }
2595
- async function login(args2) {
2596
- const flags = parseFlags(args2);
2597
- const endpoint = flags.endpoint || resolveEndpoint();
2598
- const code = (0, import_crypto.randomBytes)(4).toString("hex").toUpperCase();
2599
- const tokenPromise = new Promise((resolve6, reject) => {
2600
- let timeout;
2601
- const server = import_http.default.createServer((req, res) => {
2602
- const url = new URL(req.url || "/", `http://127.0.0.1`);
2603
- if (url.pathname !== "/callback") {
2604
- res.writeHead(404);
2605
- res.end();
2606
- return;
2607
- }
2608
- const receivedCode = url.searchParams.get("code");
2609
- const receivedToken = url.searchParams.get("token");
2610
- if (receivedCode !== code) {
2611
- res.writeHead(400, { "Content-Type": "text/plain" });
2612
- res.end("Code mismatch");
2613
- clearTimeout(timeout);
2614
- reject(new Error("Code mismatch \u2014 possible CSRF attempt"));
2615
- server.close();
2616
- return;
2617
- }
2618
- if (!receivedToken) {
2619
- res.writeHead(400, { "Content-Type": "text/plain" });
2620
- res.end("Missing token");
2621
- clearTimeout(timeout);
2622
- reject(new Error("No token received"));
2623
- server.close();
2624
- return;
2625
- }
2626
- res.writeHead(200, { "Content-Type": "text/plain" });
2627
- res.end("OK");
2628
- clearTimeout(timeout);
2629
- resolve6(receivedToken);
2630
- server.close();
2631
- });
2632
- server.listen(0, "127.0.0.1", () => {
2633
- const addr = server.address();
2634
- if (!addr || typeof addr === "string") {
2635
- reject(new Error("Failed to start callback server"));
2636
- return;
2637
- }
2638
- const port = addr.port;
2639
- const authUrl = `${endpoint}/cli-auth?code=${code}&port=${port}`;
2640
- console.log("");
2641
- info("Opening browser for authentication...");
2642
- console.log(
2643
- `
2644
- Confirmation code: ${colors.bold}${code}${colors.reset}
2645
- `
2646
- );
2647
- console.log(
2648
- ` If the browser doesn't open, visit:
2649
- ${colors.dim}${authUrl}${colors.reset}
2650
- `
2651
- );
2652
- openBrowser(authUrl);
2653
- });
2654
- timeout = setTimeout(() => {
2655
- server.close();
2656
- reject(new Error("Login timed out after 120 seconds"));
2657
- }, 12e4);
2658
- });
2659
- try {
2660
- const token = await tokenPromise;
2661
- setToken(token);
2662
- success("Logged in successfully.");
2663
- } catch (err) {
2664
- error(formatError(err));
2665
- process.exit(1);
2666
- }
2667
- }
2668
- function logout() {
2669
- const token = getToken();
2670
- if (!token) {
2671
- info("Not logged in.");
2672
- return;
2673
- }
2674
- removeToken();
2675
- success("Logged out.");
2676
- }
2677
-
2678
- // scripts/link.ts
2679
- var import_readline = __toESM(require("readline"), 1);
2680
- init_cli_utils();
2681
-
2682
- // src/api/feedback.ts
2683
- function buildQuery(params) {
2684
- const parts = [];
2685
- for (const [key, value] of Object.entries(params)) {
2686
- if (value === void 0 || value === null) continue;
2687
- if (Array.isArray(value)) {
2688
- for (const v of value) {
2689
- parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(v)}`);
2690
- }
2691
- } else {
2692
- parts.push(
2693
- `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
2694
- );
2695
- }
2696
- }
2697
- return parts.length > 0 ? `?${parts.join("&")}` : "";
2698
- }
2699
- function createFeedbackAPI(fetcher) {
2700
- return {
2701
- async list(orgId, projectId, params = {}) {
2702
- const query = buildQuery(params);
2703
- return fetcher(
2704
- `/api/organizations/${orgId}/projects/${projectId}/feedback${query}`
2705
- );
2706
- },
2707
- async get(orgId, projectId, feedbackId) {
2708
- return fetcher(
2709
- `/api/organizations/${orgId}/projects/${projectId}/feedback/${feedbackId}`
2710
- );
2711
- },
2712
- async update(orgId, projectId, feedbackId, params) {
2713
- return fetcher(
2714
- `/api/organizations/${orgId}/projects/${projectId}/feedback/${feedbackId}`,
2715
- {
2716
- method: "PATCH",
2717
- body: JSON.stringify(params)
2718
- }
2719
- );
2720
- },
2721
- async delete(orgId, projectId, feedbackId) {
2722
- await fetcher(
2723
- `/api/organizations/${orgId}/projects/${projectId}/feedback/${feedbackId}`,
2724
- { method: "DELETE" }
2725
- );
2726
- }
2727
- };
2728
- }
2729
-
2730
- // src/api/triage.ts
2731
- function createTriageAPI(fetcher) {
2732
- return {
2733
- async run(orgId, projectId) {
2734
- return fetcher(
2735
- `/api/organizations/${orgId}/projects/${projectId}/triage`,
2736
- { method: "POST" }
2737
- );
2738
- }
2739
- };
2740
- }
2741
-
2742
- // src/api/drafts.ts
2743
- function createDraftsAPI(fetcher) {
2744
- return {
2745
- async list(orgId, projectId, params = {}) {
2746
- const query = new URLSearchParams();
2747
- query.set("projectId", projectId);
2748
- if (params.status) query.set("status", params.status);
2749
- if (params.composed_by) query.set("composed_by", params.composed_by);
2750
- return fetcher(
2751
- `/api/organizations/${orgId}/issue-drafts?${query.toString()}`
2752
- );
2753
- },
2754
- async get(orgId, draftId) {
2755
- return fetcher(
2756
- `/api/organizations/${orgId}/issue-drafts/${draftId}`
2757
- );
2758
- },
2759
- async update(orgId, draftId, params) {
2760
- return fetcher(
2761
- `/api/organizations/${orgId}/issue-drafts/${draftId}`,
2762
- {
2763
- method: "PATCH",
2764
- body: JSON.stringify(params)
2765
- }
2766
- );
2767
- },
2768
- async submit(orgId, draftId) {
2769
- return fetcher(
2770
- `/api/organizations/${orgId}/issue-drafts/${draftId}/submit`,
2771
- { method: "POST" }
2772
- );
2773
- }
2774
- };
2775
- }
2776
-
2777
- // src/api/projects.ts
2778
- function createProjectsAPI(fetcher) {
2779
- return {
2780
- async list(orgId) {
2781
- return fetcher(`/api/organizations/${orgId}/projects`);
2782
- }
2783
- };
2784
- }
2785
-
2786
- // src/api/orgs.ts
2787
- function createOrgsAPI(fetcher) {
2788
- return {
2789
- async list() {
2790
- return fetcher("/api/organizations");
2791
- }
2792
- };
2793
- }
2794
-
2795
- // src/api/tokens.ts
2796
- function createUserAPI(fetcher) {
2797
- return {
2798
- async createToken(label, expiresAt) {
2799
- return fetcher("/api/user/tokens", {
2800
- method: "POST",
2801
- body: JSON.stringify({
2802
- label,
2803
- expires_at: expiresAt ?? null
2804
- })
2805
- });
2806
- },
2807
- async listTokens() {
2808
- return fetcher("/api/user/tokens");
2809
- },
2810
- async revokeToken(tokenId) {
2811
- await fetcher(`/api/user/tokens/${tokenId}`, {
2812
- method: "DELETE"
2813
- });
2814
- }
2815
- };
2816
- }
2817
-
2818
- // src/api/integrations.ts
2819
- function createIntegrationsAPI(fetcher) {
2820
- return {
2821
- async list(orgId) {
2822
- return fetcher(
2823
- `/api/organizations/${orgId}/integrations`
2824
- );
2825
- },
2826
- async create(orgId, params) {
2827
- return fetcher(
2828
- `/api/organizations/${orgId}/integrations`,
2829
- {
2830
- method: "POST",
2831
- body: JSON.stringify(params)
2832
- }
2833
- );
2834
- },
2835
- async get(orgId, integrationId) {
2836
- return fetcher(
2837
- `/api/organizations/${orgId}/integrations/${integrationId}`
2838
- );
2839
- },
2840
- async delete(orgId, integrationId) {
2841
- await fetcher(
2842
- `/api/organizations/${orgId}/integrations/${integrationId}`,
2843
- { method: "DELETE" }
2844
- );
2845
- },
2846
- async test(orgId, integrationId) {
2847
- return fetcher(
2848
- `/api/organizations/${orgId}/integrations/${integrationId}/test`,
2849
- { method: "POST" }
2850
- );
2851
- },
2852
- async listTargets(orgId, integrationId, parentId) {
2853
- const query = parentId ? `?parentId=${encodeURIComponent(parentId)}` : "";
2854
- return fetcher(
2855
- `/api/organizations/${orgId}/integrations/${integrationId}/targets${query}`
2856
- );
2857
- }
2858
- };
2859
- }
2860
-
2861
- // src/api/client.ts
2862
- var ApiError = class extends Error {
2863
- status;
2864
- constructor(status2, message) {
2865
- super(message);
2866
- this.name = "ApiError";
2867
- this.status = status2;
2868
- }
2869
- };
2870
- function createFetcher(config) {
2871
- return async (path10, init2) => {
2872
- const res = await fetch(`${config.endpoint}${path10}`, {
2873
- ...init2,
2874
- headers: {
2875
- "Content-Type": "application/json",
2876
- Authorization: `Bearer ${config.token}`,
2877
- ...init2?.headers
2878
- }
2879
- });
2880
- if (!res.ok) {
2881
- const body = await res.json().catch(() => ({}));
2882
- throw new ApiError(
2883
- res.status,
2884
- body.error || res.statusText
2885
- );
2886
- }
2887
- if (res.status === 204) return null;
2888
- return res.json();
2889
- };
2890
- }
2891
- function createClient(config) {
2892
- const fetcher = createFetcher(config);
2893
- return {
2894
- feedback: createFeedbackAPI(fetcher),
2895
- triage: createTriageAPI(fetcher),
2896
- drafts: createDraftsAPI(fetcher),
2897
- projects: createProjectsAPI(fetcher),
2898
- orgs: createOrgsAPI(fetcher),
2899
- user: createUserAPI(fetcher),
2900
- integrations: createIntegrationsAPI(fetcher)
2901
- };
2902
- }
2903
-
2904
- // scripts/link.ts
2905
- function prompt(question) {
2906
- const rl = import_readline.default.createInterface({
2907
- input: process.stdin,
2908
- output: process.stdout
2909
- });
2910
- return new Promise((resolve6) => {
2911
- rl.question(question, (answer) => {
2912
- rl.close();
2913
- resolve6(answer.trim());
2914
- });
2915
- });
2916
- }
2917
- async function pickFromList(items, display, label) {
2918
- console.log("");
2919
- for (let i = 0; i < items.length; i++) {
2920
- console.log(` ${colors.dim}${i + 1}.${colors.reset} ${display(items[i], i)}`);
2921
- }
2922
- console.log("");
2923
- const answer = await prompt(`Select ${label} (1-${items.length}): `);
2924
- const idx = parseInt(answer, 10) - 1;
2925
- if (isNaN(idx) || idx < 0 || idx >= items.length) {
2926
- error("Invalid selection");
2927
- process.exit(1);
2928
- }
2929
- return items[idx];
2930
- }
2931
- async function link(args2) {
2932
- const flags = parseFlags(args2);
2933
- const orgSlugArg = flags.org;
2934
- const projectSlugArg = flags.project;
2935
- const envArg = flags.env;
2936
- const endpointArg = flags.endpoint;
2937
- const token = getToken();
2938
- if (!token) {
2939
- error("Not logged in. Run `uidex login` first.");
2940
- process.exit(1);
2941
- }
2942
- const existingLink = getLinkContext();
2943
- const endpoint = endpointArg || resolveEndpoint(existingLink);
2944
- const client = createClient({ endpoint, token });
2945
- if (envArg && !orgSlugArg && !projectSlugArg && existingLink) {
2946
- setLinkContext({ ...existingLink, environment: envArg });
2947
- success(
2948
- `Switched to ${existingLink.orgSlug}/${existingLink.projectSlug} (${envArg})`
2949
- );
2950
- return;
2951
- }
2952
- let orgs;
2953
- try {
2954
- orgs = await client.orgs.list();
2955
- } catch (err) {
2956
- error(
2957
- `Failed to fetch organizations: ${formatError(err)}`
2958
- );
2959
- process.exit(1);
2960
- }
2961
- if (orgs.length === 0) {
2962
- warn("No organizations found. Create one in the web UI first.");
2963
- process.exit(1);
2964
- }
2965
- let selectedOrg;
2966
- if (orgSlugArg) {
2967
- const match = orgs.find((o) => o.slug === orgSlugArg);
2968
- if (!match) {
2969
- error(`Organization "${orgSlugArg}" not found`);
2970
- process.exit(1);
2971
- }
2972
- selectedOrg = match;
2973
- } else if (orgs.length === 1) {
2974
- selectedOrg = orgs[0];
2975
- info(`Using organization: ${selectedOrg.name}`);
2976
- } else {
2977
- heading("Select organization");
2978
- selectedOrg = await pickFromList(
2979
- orgs,
2980
- (o) => `${o.name} ${colors.dim}(${o.slug})${colors.reset}`,
2981
- "organization"
2982
- );
2983
- }
2984
- let projects;
2985
- try {
2986
- projects = await client.projects.list(selectedOrg.id);
2987
- } catch (err) {
2988
- error(
2989
- `Failed to fetch projects: ${formatError(err)}`
2990
- );
2991
- process.exit(1);
2992
- }
2993
- if (projects.length === 0) {
2994
- warn("No projects found. Create one in the web UI first.");
2995
- process.exit(1);
2996
- }
2997
- let selectedProject;
2998
- if (projectSlugArg) {
2999
- const match = projects.find((p) => p.slug === projectSlugArg);
3000
- if (!match) {
3001
- error(`Project "${projectSlugArg}" not found`);
3002
- process.exit(1);
3003
- }
3004
- selectedProject = match;
3005
- } else if (projects.length === 1) {
3006
- selectedProject = projects[0];
3007
- info(`Using project: ${selectedProject.name}`);
3008
- } else {
3009
- heading("Select project");
3010
- selectedProject = await pickFromList(
3011
- projects,
3012
- (p) => `${p.name} ${colors.dim}(${p.slug})${colors.reset}`,
3013
- "project"
3014
- );
3015
- }
3016
- const environment = envArg || "production";
3017
- setLinkContext({
3018
- orgId: selectedOrg.id,
3019
- orgSlug: selectedOrg.slug,
3020
- projectId: selectedProject.id,
3021
- projectSlug: selectedProject.slug,
3022
- environment,
3023
- endpoint
3024
- });
3025
- success(
3026
- `Linked to ${selectedOrg.slug}/${selectedProject.slug} (${environment})`
3027
- );
3028
- }
3029
- function status() {
3030
- const config = readGlobalConfig();
3031
- const token = process.env.UIDEX_TOKEN || config.token;
3032
- const link2 = config.links[process.cwd()];
3033
- heading("uidex status");
3034
- if (!token) {
3035
- info("Auth: not logged in");
3036
- console.log(` Run ${colors.cyan}uidex login${colors.reset} to authenticate.`);
3037
- return;
3038
- }
3039
- info(`Auth: logged in (token: ${token.slice(0, 8)}...)`);
3040
- if (!link2) {
3041
- console.log("");
3042
- info("Project: not linked");
3043
- console.log(` Run ${colors.cyan}uidex link${colors.reset} to link a project.`);
3044
- return;
3045
- }
3046
- console.log("");
3047
- info(`Organization: ${link2.orgSlug}`);
3048
- info(`Project: ${link2.projectSlug}`);
3049
- info(`Environment: ${link2.environment}`);
3050
- info(`Endpoint: ${link2.endpoint}`);
3051
- }
3052
-
3053
- // scripts/feedback.ts
3054
- var import_readline2 = __toESM(require("readline"), 1);
3055
- init_cli_utils();
3056
- async function listFeedback(args2) {
3057
- const ctx = requireContext();
3058
- const flags = parseFlags(args2);
3059
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3060
- const params = {};
3061
- if (flags.status) params.status = flags.status.split(",");
3062
- if (flags.type) params.type = flags.type.split(",");
3063
- if (flags.severity) params.severity = flags.severity.split(",");
3064
- if (flags.limit) params.limit = parseInt(flags.limit, 10);
3065
- if (flags.page) params.page = parseInt(flags.page, 10);
3066
- if (flags.sort) params.sort = flags.sort;
3067
- if (flags.order && (flags.order === "asc" || flags.order === "desc")) {
3068
- params.order = flags.order;
3069
- }
3070
- if (flags.search) params.search = flags.search;
3071
- try {
3072
- const result = await client.feedback.list(
3073
- ctx.orgId,
3074
- ctx.projectId,
3075
- params
3076
- );
3077
- if (result.data.length === 0) {
3078
- info("No feedback found.");
3079
- return;
3080
- }
3081
- heading(`Feedback (${ctx.orgSlug}/${ctx.projectSlug})`);
3082
- console.log(
3083
- ` ${colors.dim}${pad("#", 6)} ${pad("TYPE", 12)} ${pad("SEVERITY", 10)} ${pad("STATUS", 12)} ${pad("DESCRIPTION", 40)} ${pad("CREATED", 14)}${colors.reset}`
3084
- );
3085
- for (const f of result.data) {
3086
- const desc = truncate(f.title || f.description, 40);
3087
- console.log(
3088
- ` ${pad(String(f.sequence_number), 6)} ${pad(f.type, 12)} ${pad(f.severity, 10)} ${pad(f.status, 12)} ${pad(desc, 40)} ${pad(formatDate(f.created_at), 14)}`
3089
- );
3090
- }
3091
- console.log(
3092
- `
3093
- ${colors.dim}Page ${result.page} of ${Math.ceil(result.total / result.limit)}, ${result.total} total${colors.reset}`
3094
- );
3095
- } catch (err) {
3096
- error(`Failed to list feedback: ${formatError(err)}`);
3097
- process.exit(1);
3098
- }
3099
- }
3100
- async function showFeedback(args2) {
3101
- const ctx = requireContext();
3102
- const id = args2[0];
3103
- if (!id) {
3104
- error("Usage: uidex feedback show <id>");
3105
- process.exit(1);
3106
- }
3107
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3108
- try {
3109
- let feedback;
3110
- const seqNum = parseInt(id, 10);
3111
- if (!isNaN(seqNum) && String(seqNum) === id) {
3112
- const result = await client.feedback.list(ctx.orgId, ctx.projectId, {
3113
- sequence_number: seqNum,
3114
- limit: 1
3115
- });
3116
- if (result.data.length === 0) {
3117
- error(`Feedback #${seqNum} not found`);
3118
- process.exit(1);
3119
- }
3120
- feedback = result.data[0];
3121
- } else {
3122
- feedback = await client.feedback.get(ctx.orgId, ctx.projectId, id);
3123
- }
3124
- heading(`Feedback #${feedback.sequence_number}`);
3125
- const fields = [
3126
- ["Type", feedback.type],
3127
- ["Severity", feedback.severity],
3128
- ["Status", feedback.status],
3129
- ["Priority", feedback.priority || "-"],
3130
- ["Component", feedback.component_id],
3131
- ["URL", feedback.url],
3132
- ["Reporter", feedback.reporter_email || feedback.reporter_name || "-"],
3133
- ["Tags", feedback.tags?.join(", ") || "-"],
3134
- ["Created", formatDate(feedback.created_at)],
3135
- ["Updated", formatDate(feedback.updated_at)]
3136
- ];
3137
- for (const [label, value] of fields) {
3138
- console.log(` ${colors.dim}${pad(label, 12)}${colors.reset} ${value}`);
3139
- }
3140
- if (feedback.title) {
3141
- console.log(`
3142
- ${colors.bold}${feedback.title}${colors.reset}`);
3143
- }
3144
- console.log(`
3145
- ${feedback.description}`);
3146
- if (feedback.console_logs && feedback.console_logs.length > 0) {
3147
- console.log(`
3148
- ${colors.dim}Console Logs:${colors.reset}`);
3149
- for (const log2 of feedback.console_logs) {
3150
- const levelColor = log2.level === "error" ? colors.red : log2.level === "warn" ? colors.yellow : colors.dim;
3151
- console.log(
3152
- ` ${levelColor}[${log2.level}]${colors.reset} ${log2.message}`
3153
- );
3154
- }
3155
- }
3156
- if (feedback.network_errors && feedback.network_errors.length > 0) {
3157
- console.log(`
3158
- ${colors.dim}Network Errors:${colors.reset}`);
3159
- for (const ne of feedback.network_errors) {
3160
- console.log(
3161
- ` ${colors.red}${ne.method} ${ne.url} \u2192 ${ne.status || "failed"}${colors.reset}`
3162
- );
3163
- }
3164
- }
3165
- } catch (err) {
3166
- error(`Failed to show feedback: ${formatError(err)}`);
3167
- process.exit(1);
3168
- }
3169
- }
3170
- async function updateFeedback(args2) {
3171
- const ctx = requireContext();
3172
- const id = args2[0];
3173
- if (!id) {
3174
- error(
3175
- "Usage: uidex feedback update <id> [--status=...] [--priority=...] [--tags=...]"
3176
- );
3177
- process.exit(1);
3178
- }
3179
- const flags = parseFlags(args2.slice(1));
3180
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3181
- const params = {};
3182
- if (flags.status) params.status = flags.status;
3183
- if (flags.priority) params.priority = flags.priority;
3184
- if (flags.resolution) params.resolution = flags.resolution;
3185
- if (flags.tags) params.tags = flags.tags.split(",");
3186
- if (flags.assign) params.assignee_id = flags.assign;
3187
- if (Object.keys(params).length === 0) {
3188
- error("No update flags provided. Use --status, --priority, --tags, etc.");
3189
- process.exit(1);
3190
- }
3191
- try {
3192
- const updated = await client.feedback.update(
3193
- ctx.orgId,
3194
- ctx.projectId,
3195
- id,
3196
- params
3197
- );
3198
- success(`Updated feedback #${updated.sequence_number}`);
3199
- } catch (err) {
3200
- error(`Failed to update feedback: ${formatError(err)}`);
3201
- process.exit(1);
3202
- }
3203
- }
3204
- async function deleteFeedback(args2) {
3205
- const ctx = requireContext();
3206
- const id = args2[0];
3207
- if (!id) {
3208
- error("Usage: uidex feedback delete <id> [--force]");
3209
- process.exit(1);
3210
- }
3211
- const force = args2.includes("--force");
3212
- if (!force) {
3213
- const rl = import_readline2.default.createInterface({
3214
- input: process.stdin,
3215
- output: process.stdout
3216
- });
3217
- const answer = await new Promise((resolve6) => {
3218
- rl.question(`Delete feedback ${id}? (y/N): `, (ans) => {
3219
- rl.close();
3220
- resolve6(ans);
3221
- });
3222
- });
3223
- if (answer.trim().toLowerCase() !== "y") {
3224
- info("Cancelled.");
3225
- return;
3226
- }
3227
- }
3228
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3229
- try {
3230
- await client.feedback.delete(ctx.orgId, ctx.projectId, id);
3231
- success(`Deleted feedback ${id}`);
3232
- } catch (err) {
3233
- error(`Failed to delete feedback: ${formatError(err)}`);
3234
- process.exit(1);
3235
- }
3236
- }
3237
- function printFeedbackHelp() {
3238
- console.log("\nUsage: uidex feedback <command>\n");
3239
- console.log("Commands:");
3240
- console.log(" list List feedback with optional filters");
3241
- console.log(" show <id> Show feedback details");
3242
- console.log(" update <id> Update feedback status/priority/tags");
3243
- console.log(" delete <id> Delete feedback");
3244
- console.log("");
3245
- console.log("Flags for list:");
3246
- console.log(" --status=open,triaged Filter by status");
3247
- console.log(" --type=bug Filter by type");
3248
- console.log(" --severity=high,critical Filter by severity");
3249
- console.log(" --limit=25 Items per page");
3250
- console.log(" --page=1 Page number");
3251
- console.log(" --sort=created_at Sort field");
3252
- console.log(" --order=desc Sort order");
3253
- console.log("");
3254
- }
3255
- async function handleFeedback(args2) {
3256
- const subcommand = args2[0];
3257
- switch (subcommand) {
3258
- case "list":
3259
- return listFeedback(args2.slice(1));
3260
- case "show":
3261
- return showFeedback(args2.slice(1));
3262
- case "update":
3263
- return updateFeedback(args2.slice(1));
3264
- case "delete":
3265
- return deleteFeedback(args2.slice(1));
3266
- default:
3267
- printFeedbackHelp();
3268
- }
3269
- }
3270
-
3271
- // scripts/triage.ts
3272
- init_cli_utils();
3273
- async function runTriage() {
3274
- const ctx = requireContext();
3275
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3276
- info("Running triage...");
3277
- try {
3278
- const result = await client.triage.run(ctx.orgId, ctx.projectId);
3279
- if (result.proposals.length === 0 && result.updated.length === 0) {
3280
- info("No untriaged feedback to process.");
3281
- return;
3282
- }
3283
- if (result.proposals.length > 0) {
3284
- success(
3285
- `Created ${result.proposals.length} proposal(s) from untriaged feedback`
3286
- );
3287
- for (const p of result.proposals) {
3288
- console.log(
3289
- ` ${colors.dim}-${colors.reset} ${p.title} ${colors.dim}(${p.feedback_count || 0} feedback)${colors.reset}`
3290
- );
3291
- }
3292
- }
3293
- if (result.updated.length > 0) {
3294
- info(`Updated ${result.updated.length} existing proposal(s)`);
3295
- }
3296
- } catch (err) {
3297
- error(`Failed to run triage: ${formatError(err)}`);
3298
- process.exit(1);
3299
- }
3300
- }
3301
- async function listDrafts(args2) {
3302
- const ctx = requireContext();
3303
- const flags = parseFlags(args2);
3304
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3305
- try {
3306
- const drafts = await client.drafts.list(ctx.orgId, ctx.projectId, {
3307
- status: flags.status,
3308
- composed_by: flags["composed-by"]
3309
- });
3310
- if (drafts.length === 0) {
3311
- info("No issue drafts found.");
3312
- return;
3313
- }
3314
- heading(`Issue Drafts (${ctx.orgSlug}/${ctx.projectSlug})`);
3315
- console.log(
3316
- ` ${colors.dim}${pad("ID", 10)} ${pad("TITLE", 40)} ${pad("STATUS", 12)} ${pad("BY", 8)} ${pad("FB#", 5)} ${pad("CREATED", 14)}${colors.reset}`
3317
- );
3318
- for (const d of drafts) {
3319
- console.log(
3320
- ` ${pad(d.id.slice(0, 8), 10)} ${pad(truncate(d.title, 40), 40)} ${pad(d.status, 12)} ${pad(d.composed_by, 8)} ${pad(String(d.feedback_count || 0), 5)} ${pad(formatDate(d.created_at), 14)}`
3321
- );
3322
- }
3323
- } catch (err) {
3324
- error(`Failed to list drafts: ${formatError(err)}`);
3325
- process.exit(1);
3326
- }
3327
- }
3328
- async function showDraft(args2) {
3329
- const ctx = requireContext();
3330
- const id = args2[0];
3331
- if (!id) {
3332
- error("Usage: uidex triage show <id>");
3333
- process.exit(1);
3334
- }
3335
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3336
- try {
3337
- const draft = await client.drafts.get(ctx.orgId, id);
3338
- heading(`Draft: ${draft.title}`);
3339
- const fields = [
3340
- ["Status", draft.status],
3341
- ["Composed by", draft.composed_by],
3342
- ["Labels", (draft.labels || []).join(", ") || "-"],
3343
- ["Created", formatDate(draft.created_at)]
3344
- ];
3345
- for (const [label, value] of fields) {
3346
- console.log(
3347
- ` ${colors.dim}${pad(label, 14)}${colors.reset} ${value}`
3348
- );
3349
- }
3350
- console.log(`
3351
- ${draft.body}`);
3352
- if (draft.reasoning) {
3353
- console.log(
3354
- `
3355
- ${colors.dim}Reasoning:${colors.reset} ${draft.reasoning}`
3356
- );
3357
- }
3358
- if (draft.feedback_ids && draft.feedback_ids.length > 0) {
3359
- console.log(
3360
- `
3361
- ${colors.dim}Linked feedback:${colors.reset} ${draft.feedback_ids.length} item(s)`
3362
- );
3363
- }
3364
- } catch (err) {
3365
- error(`Failed to show draft: ${formatError(err)}`);
3366
- process.exit(1);
3367
- }
3368
- }
3369
- async function approveDraft(args2) {
3370
- const ctx = requireContext();
3371
- const id = args2[0];
3372
- if (!id) {
3373
- error("Usage: uidex triage approve <id>");
3374
- process.exit(1);
3375
- }
3376
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3377
- try {
3378
- const updated = await client.drafts.update(ctx.orgId, id, {
3379
- status: "draft"
3380
- });
3381
- success(`Approved: ${updated.title} (status: draft)`);
3382
- } catch (err) {
3383
- error(`Failed to approve draft: ${formatError(err)}`);
3384
- process.exit(1);
3385
- }
3386
- }
3387
- async function dismissDraft(args2) {
3388
- const ctx = requireContext();
3389
- const id = args2[0];
3390
- if (!id) {
3391
- error("Usage: uidex triage dismiss <id>");
3392
- process.exit(1);
3393
- }
3394
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3395
- try {
3396
- const updated = await client.drafts.update(ctx.orgId, id, {
3397
- status: "dismissed"
3398
- });
3399
- success(`Dismissed: ${updated.title}`);
3400
- } catch (err) {
3401
- error(`Failed to dismiss draft: ${formatError(err)}`);
3402
- process.exit(1);
3403
- }
3404
- }
3405
- async function submitDraft(args2) {
3406
- const ctx = requireContext();
3407
- const id = args2[0];
3408
- if (!id) {
3409
- error("Usage: uidex triage submit <id>");
3410
- process.exit(1);
3411
- }
3412
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3413
- try {
3414
- const result = await client.drafts.submit(ctx.orgId, id);
3415
- success(`Submitted! External issue: ${result.external_url}`);
3416
- } catch (err) {
3417
- error(`Failed to submit draft: ${formatError(err)}`);
3418
- process.exit(1);
3419
- }
3420
- }
3421
- function printTriageHelp() {
3422
- console.log("\nUsage: uidex triage <command>\n");
3423
- console.log("Commands:");
3424
- console.log(" run Run AI triage on untriaged feedback");
3425
- console.log(" list List issue draft proposals");
3426
- console.log(" show <id> Show draft details and linked feedback");
3427
- console.log(" approve <id> Move draft from suggested to approved");
3428
- console.log(" dismiss <id> Dismiss a draft proposal");
3429
- console.log(" submit <id> Submit draft to external issue tracker");
3430
- console.log("");
3431
- console.log("Flags for list:");
3432
- console.log(" --status=suggested,draft Filter by status");
3433
- console.log(" --composed-by=auto Filter by composition method");
3434
- console.log("");
3435
- }
3436
- async function handleTriage(args2) {
3437
- const subcommand = args2[0];
3438
- switch (subcommand) {
3439
- case "run":
3440
- return runTriage();
3441
- case "list":
3442
- return listDrafts(args2.slice(1));
3443
- case "show":
3444
- return showDraft(args2.slice(1));
3445
- case "approve":
3446
- return approveDraft(args2.slice(1));
3447
- case "dismiss":
3448
- return dismissDraft(args2.slice(1));
3449
- case "submit":
3450
- return submitDraft(args2.slice(1));
3451
- default:
3452
- printTriageHelp();
3453
- }
3454
- }
3455
-
3456
- // scripts/integrations.ts
3457
- var import_readline3 = __toESM(require("readline"), 1);
3458
- init_cli_utils();
3459
- function resolveOrgContext(flags) {
3460
- const token = getToken();
3461
- if (!token) {
3462
- error("Not logged in. Run `uidex login` first.");
3463
- process.exit(1);
3464
- }
3465
- const link2 = getLinkContext();
3466
- const endpoint = resolveEndpoint(link2 ?? void 0);
3467
- const orgId = flags.org || process.env.UIDEX_ORG_ID || link2?.orgId;
3468
- if (!orgId) {
3469
- error("No organization context. Run `uidex link` first or pass --org=<id>.");
3470
- process.exit(1);
3471
- }
3472
- return { token, endpoint, orgId };
3473
- }
3474
- function prompt2(question, mask = false) {
3475
- const rl = import_readline3.default.createInterface({
3476
- input: process.stdin,
3477
- output: process.stdout
3478
- });
3479
- if (mask) {
3480
- const origWrite = rl._writeToOutput;
3481
- rl._writeToOutput = (s) => {
3482
- if (s.includes(question)) {
3483
- origWrite.call(rl, s);
3484
- } else {
3485
- origWrite.call(rl, "*".repeat(s.replace(/\r?\n/, "").length));
3486
- }
3487
- };
3488
- }
3489
- return new Promise((resolve6) => {
3490
- rl.question(question, (answer) => {
3491
- rl.close();
3492
- if (mask) console.log();
3493
- resolve6(answer.trim());
3494
- });
3495
- });
3496
- }
3497
- function confirm(question) {
3498
- return prompt2(question).then((a) => a.toLowerCase() === "y");
3499
- }
3500
- async function listIntegrations(args2) {
3501
- const flags = parseFlags(args2);
3502
- const ctx = resolveOrgContext(flags);
3503
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3504
- try {
3505
- const integrations = await client.integrations.list(ctx.orgId);
3506
- if (integrations.length === 0) {
3507
- info(
3508
- "No integrations configured. Run `uidex integrations add` to add one."
3509
- );
3510
- return;
3511
- }
3512
- console.log(
3513
- `
3514
- ${colors.dim}${pad("ID", 20)} ${pad("PROVIDER", 10)} ${pad("LABEL", 24)} ${pad("CREATED", 14)}${colors.reset}`
3515
- );
3516
- for (const i of integrations) {
3517
- console.log(
3518
- ` ${pad(i.id.slice(0, 18), 20)} ${pad(i.provider, 10)} ${pad(i.label, 24)} ${pad(formatDate(i.created_at), 14)}`
3519
- );
3520
- }
3521
- console.log();
3522
- } catch (err) {
3523
- error(`Failed to list integrations: ${formatError(err)}`);
3524
- process.exit(1);
3525
- }
3526
- }
3527
- async function addJiraInteractive(client, orgId, flagLabel) {
3528
- const label = flagLabel || await prompt2("Label (Jira): ") || "Jira";
3529
- const url = await prompt2("Instance URL (e.g. https://team.atlassian.net): ");
3530
- if (!url) {
3531
- error("Instance URL is required.");
3532
- process.exit(1);
3533
- }
3534
- const email = await prompt2("Email: ");
3535
- if (!email) {
3536
- error("Email is required.");
3537
- process.exit(1);
3538
- }
3539
- const apiToken = await prompt2("API token: ", true);
3540
- if (!apiToken) {
3541
- error("API token is required.");
3542
- process.exit(1);
3543
- }
3544
- await createJiraIntegration(client, orgId, label, url, email, apiToken);
3545
- }
3546
- async function createJiraIntegration(client, orgId, label, url, email, apiToken) {
3547
- info("Testing connection...");
3548
- try {
3549
- const integration = await client.integrations.create(orgId, {
3550
- provider: "jira",
3551
- label,
3552
- config: { baseUrl: url },
3553
- credentials: { email, apiToken }
3554
- });
3555
- const result = await client.integrations.test(orgId, integration.id);
3556
- if (!result.ok) {
3557
- error(`Connection test failed: ${result.error}`);
3558
- const retry = await confirm("Retry with different credentials? (y/N): ");
3559
- if (retry) {
3560
- await client.integrations.delete(orgId, integration.id);
3561
- return addJiraInteractive(client, orgId, label);
3562
- }
3563
- await client.integrations.delete(orgId, integration.id);
3564
- process.exit(1);
3565
- }
3566
- success(`Integration created: ${label} (id: ${integration.id})`);
3567
- } catch (err) {
3568
- error(`Failed to create integration: ${formatError(err)}`);
3569
- process.exit(1);
3570
- }
3571
- }
3572
- async function addGithub(client, orgId, endpoint) {
3573
- let slug = null;
3574
- try {
3575
- const res = await fetch(`${endpoint}/api/server/info`);
3576
- if (res.ok) {
3577
- const data = await res.json();
3578
- slug = data.githubAppSlug;
3579
- }
3580
- } catch {
3581
- }
3582
- if (!slug) {
3583
- error("GitHub App is not configured on this uidex instance.");
3584
- process.exit(1);
3585
- }
3586
- const existingBefore = await client.integrations.list(orgId);
3587
- const installUrl = `https://github.com/apps/${slug}/installations/new`;
3588
- info("Opening browser for GitHub App installation...");
3589
- console.log(
3590
- `
3591
- If the browser doesn't open, visit:
3592
- ${colors.dim}${installUrl}${colors.reset}
3593
- `
3594
- );
3595
- openBrowser2(installUrl);
3596
- info("Waiting for GitHub App installation...");
3597
- const deadline = Date.now() + 12e4;
3598
- while (Date.now() < deadline) {
3599
- await new Promise((r) => setTimeout(r, 2e3));
3600
- try {
3601
- const current = await client.integrations.list(orgId);
3602
- const newIntegration = current.find(
3603
- (i) => i.provider === "github" && !existingBefore.some((e) => e.id === i.id)
3604
- );
3605
- if (newIntegration) {
3606
- success(
3607
- `Integration created: ${newIntegration.label} (id: ${newIntegration.id})`
3608
- );
3609
- return;
3610
- }
3611
- } catch {
3612
- }
3613
- }
3614
- error("Timed out waiting for GitHub App installation.");
3615
- process.exit(1);
3616
- }
3617
- function openBrowser2(url) {
3618
- const { exec } = require("child_process");
3619
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
3620
- exec(`${cmd} "${url}"`);
3621
- }
3622
- async function addIntegration(args2) {
3623
- const provider = args2[0];
3624
- if (!provider || provider !== "jira" && provider !== "github") {
3625
- error("Usage: uidex integrations add <jira|github> [--label=...]");
3626
- process.exit(1);
3627
- }
3628
- const flags = parseFlags(args2.slice(1));
3629
- const ctx = resolveOrgContext(flags);
3630
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3631
- if (provider === "jira") {
3632
- if (flags.url && flags.email && flags.token) {
3633
- const label = flags.label || "Jira";
3634
- await createJiraIntegration(
3635
- client,
3636
- ctx.orgId,
3637
- label,
3638
- flags.url,
3639
- flags.email,
3640
- flags.token
3641
- );
3642
- } else {
3643
- await addJiraInteractive(client, ctx.orgId, flags.label);
3644
- }
3645
- } else {
3646
- await addGithub(client, ctx.orgId, ctx.endpoint);
3647
- }
3648
- }
3649
- async function testIntegration(args2) {
3650
- const integrationId = args2[0];
3651
- if (!integrationId) {
3652
- error("Usage: uidex integrations test <integrationId>");
3653
- process.exit(1);
3654
- }
3655
- const flags = parseFlags(args2.slice(1));
3656
- const ctx = resolveOrgContext(flags);
3657
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3658
- try {
3659
- const result = await client.integrations.test(ctx.orgId, integrationId);
3660
- if (result.ok) {
3661
- success("Connection OK");
3662
- } else {
3663
- error(result.error || "Connection test failed");
3664
- process.exit(1);
3665
- }
3666
- } catch (err) {
3667
- error(`Failed to test integration: ${formatError(err)}`);
3668
- process.exit(1);
3669
- }
3670
- }
3671
- async function removeIntegration(args2) {
3672
- const integrationId = args2[0];
3673
- if (!integrationId) {
3674
- error("Usage: uidex integrations remove <integrationId> [--force]");
3675
- process.exit(1);
3676
- }
3677
- const flags = parseFlags(args2.slice(1));
3678
- const force = args2.includes("--force");
3679
- const ctx = resolveOrgContext(flags);
3680
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3681
- if (!force) {
3682
- let label = integrationId;
3683
- try {
3684
- const integration = await client.integrations.get(
3685
- ctx.orgId,
3686
- integrationId
3687
- );
3688
- label = `'${integration.label}' (${integrationId})`;
3689
- } catch {
3690
- }
3691
- const confirmed = await confirm(`Remove integration ${label}? (y/N): `);
3692
- if (!confirmed) {
3693
- info("Cancelled.");
3694
- return;
3695
- }
3696
- }
3697
- try {
3698
- await client.integrations.delete(ctx.orgId, integrationId);
3699
- success(`Removed integration ${integrationId}`);
3700
- } catch (err) {
3701
- error(`Failed to remove integration: ${formatError(err)}`);
3702
- process.exit(1);
3703
- }
3704
- }
3705
- async function listTargets(args2) {
3706
- const integrationId = args2[0];
3707
- if (!integrationId) {
3708
- error("Usage: uidex integrations targets <integrationId> [--parent=...]");
3709
- process.exit(1);
3710
- }
3711
- const flags = parseFlags(args2.slice(1));
3712
- const ctx = resolveOrgContext(flags);
3713
- const client = createClient({ endpoint: ctx.endpoint, token: ctx.token });
3714
- try {
3715
- const targets = await client.integrations.listTargets(
3716
- ctx.orgId,
3717
- integrationId,
3718
- flags.parent
3719
- );
3720
- if (targets.length === 0) {
3721
- info("No targets found.");
3722
- return;
3723
- }
3724
- console.log(
3725
- `
3726
- ${colors.dim}${pad("ID", 24)} ${pad("LABEL", 32)} ${"HAS CHILDREN"}${colors.reset}`
3727
- );
3728
- for (const t of targets) {
3729
- console.log(
3730
- ` ${pad(t.id, 24)} ${pad(t.label, 32)} ${t.children ? "yes" : "-"}`
3731
- );
3732
- }
3733
- console.log();
3734
- } catch (err) {
3735
- error(`Failed to list targets: ${formatError(err)}`);
3736
- process.exit(1);
3737
- }
3738
- }
3739
- function printIntegrationsHelp() {
3740
- console.log("\nUsage: uidex integrations <command>\n");
3741
- console.log("Commands:");
3742
- console.log(" list List configured integrations");
3743
- console.log(" add <jira|github> Add a new integration");
3744
- console.log(" test <integrationId> Test integration connection");
3745
- console.log(" remove <integrationId> Remove an integration");
3746
- console.log(
3747
- " targets <integrationId> List available targets (repos, projects)"
3748
- );
3749
- console.log("");
3750
- console.log("Flags:");
3751
- console.log(" --org=<orgId> Override organization");
3752
- console.log("");
3753
- console.log("Flags for add jira (non-interactive):");
3754
- console.log(" --label=<name> Integration label");
3755
- console.log(" --url=<url> Jira instance URL");
3756
- console.log(" --email=<email> Jira email");
3757
- console.log(" --token=<token> Jira API token");
3758
- console.log("");
3759
- console.log("Flags for remove:");
3760
- console.log(" --force Skip confirmation prompt");
3761
- console.log("");
3762
- console.log("Flags for targets:");
3763
- console.log(" --parent=<parentId> Filter targets by parent");
3764
- console.log("");
3765
- }
3766
- async function handleIntegrations(args2) {
3767
- const subcommand = args2[0];
3768
- switch (subcommand) {
3769
- case "list":
3770
- return listIntegrations(args2.slice(1));
3771
- case "add":
3772
- return addIntegration(args2.slice(1));
3773
- case "test":
3774
- return testIntegration(args2.slice(1));
3775
- case "remove":
3776
- return removeIntegration(args2.slice(1));
3777
- case "targets":
3778
- return listTargets(args2.slice(1));
3779
- default:
3780
- printIntegrationsHelp();
3781
- }
3782
- }
3783
-
3784
- // scripts/cli.ts
3785
- init_cli_utils();
3786
- function printHelp() {
3787
- console.log(`
3788
- ${colors.bold}${colors.cyan}uidex${colors.reset}
3789
- `);
3790
- console.log("Usage: uidex <command>\n");
3791
- console.log(`${colors.dim}Setup${colors.reset}`);
3792
- console.log(" init Initialize uidex in your project");
3793
- console.log(" scan Run the component scanner");
3794
- console.log(" scan --audit Validate coverage and annotations");
3795
- console.log(" scaffold [dir] Generate Playwright test stubs");
3796
- console.log(" claude Manage Claude Code integration");
3797
- console.log("");
3798
- console.log(`${colors.dim}Server${colors.reset}`);
3799
- console.log(" login Authenticate with uidex server");
3800
- console.log(" logout Remove stored credentials");
3801
- console.log(" link Link current directory to org/project");
3802
- console.log(" status Show auth and link state");
3803
- console.log(" feedback Manage feedback (list, show, update, delete)");
3804
- console.log(" triage Manage triage (run, list, show, approve, dismiss, submit)");
3805
- console.log(" integrations Manage integrations (list, add, remove, test)");
3806
- console.log("");
3807
- }
3808
- function parseAction(raw) {
3809
- if (raw === void 0 || raw === "add") return "add";
3810
- if (raw === "remove") return "remove";
3811
- return null;
3812
- }
3813
- function handleClaude(args2) {
3814
- const subcommand = args2[0];
3815
- const action = parseAction(args2[1]);
3816
- if (action === null) {
3817
- console.error(`Unknown action: ${args2[1]}. Expected "add" or "remove".`);
3818
- process.exit(1);
3819
- }
3820
- switch (subcommand) {
3821
- case "install":
3822
- claudeInstall();
3823
- break;
3824
- case "uninstall":
3825
- claudeUninstall();
3826
- break;
3827
- case "rules":
3828
- action === "remove" ? removeRules() : addRules();
3829
- break;
3830
- case "skill":
3831
- action === "remove" ? removeSkill() : addSkill();
3832
- break;
3833
- case "hooks":
3834
- action === "remove" ? removeHooks() : addHooks();
3835
- break;
3836
- default:
3837
- printClaudeHelp();
3838
- break;
3839
- }
3840
- }
3841
- var args = process.argv.slice(2);
3842
- var command = args[0];
3843
- switch (command) {
3844
- case void 0:
3845
- case "--help":
3846
- case "-h":
3847
- printHelp();
3848
- break;
3849
- case "init":
3850
- init().catch((err) => {
3851
- console.error("Error during initialization:", err);
3852
- process.exit(1);
3853
- });
3854
- break;
3855
- case "scan":
3856
- scan();
3857
- break;
3858
- case "scaffold":
3859
- scaffold(args[1]);
3860
- break;
3861
- case "claude":
3862
- handleClaude(args.slice(1));
3863
- break;
3864
- case "login":
3865
- login(args.slice(1)).catch((err) => {
3866
- console.error("Login error:", err);
3867
- process.exit(1);
3868
- });
3869
- break;
3870
- case "logout":
3871
- logout();
3872
- break;
3873
- case "link":
3874
- link(args.slice(1)).catch((err) => {
3875
- console.error("Link error:", err);
3876
- process.exit(1);
3877
- });
3878
- break;
3879
- case "status":
3880
- status();
3881
- break;
3882
- case "feedback":
3883
- handleFeedback(args.slice(1)).catch((err) => {
3884
- console.error("Feedback error:", err);
3885
- process.exit(1);
3886
- });
3887
- break;
3888
- case "triage":
3889
- handleTriage(args.slice(1)).catch((err) => {
3890
- console.error("Triage error:", err);
3891
- process.exit(1);
3892
- });
3893
- break;
3894
- case "integrations":
3895
- handleIntegrations(args.slice(1)).catch((err) => {
3896
- console.error("Integrations error:", err);
3897
- process.exit(1);
3898
- });
3899
- break;
3900
- default:
3901
- console.error(`Unknown command: ${command}`);
3902
- printHelp();
3903
- process.exit(1);
3904
- }