uidex 0.2.0 → 0.2.4

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.
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
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
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -22,23 +29,23 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
22
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
30
  mod
24
31
  ));
25
-
26
- // scripts/init.ts
27
- var fs = __toESM(require("fs"), 1);
28
- var path = __toESM(require("path"), 1);
29
- var readline = __toESM(require("readline"), 1);
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
33
 
31
34
  // scripts/cli-utils.ts
32
- var colors = {
33
- reset: "\x1B[0m",
34
- bold: "\x1B[1m",
35
- dim: "\x1B[2m",
36
- green: "\x1B[32m",
37
- yellow: "\x1B[33m",
38
- blue: "\x1B[34m",
39
- cyan: "\x1B[36m",
40
- red: "\x1B[31m"
41
- };
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
+ });
42
49
  function success(message) {
43
50
  console.log(`${colors.green}\u2713${colors.reset} ${message}`);
44
51
  }
@@ -56,202 +63,125 @@ function heading(message) {
56
63
  ${colors.bold}${colors.cyan}${message}${colors.reset}
57
64
  `);
58
65
  }
59
-
60
- // scripts/init.ts
61
- function log(message) {
62
- console.log(message);
63
- }
64
- function detectProject() {
65
- const cwd = process.cwd();
66
- const packageJsonPath = path.join(cwd, "package.json");
67
- let packageJson = {};
68
- if (fs.existsSync(packageJsonPath)) {
69
- packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
70
- }
71
- const deps = {
72
- ...packageJson.dependencies,
73
- ...packageJson.devDependencies
74
- };
75
- const name = packageJson.name || path.basename(cwd);
76
- const usesTypeScript = fs.existsSync(path.join(cwd, "tsconfig.json")) || !!deps["typescript"];
77
- const hasSrcDir = fs.existsSync(path.join(cwd, "src"));
78
- const srcDir = hasSrcDir ? "src" : ".";
79
- if (deps["next"]) {
80
- const usesAppRouter = fs.existsSync(path.join(cwd, "src", "app")) || fs.existsSync(path.join(cwd, "app"));
81
- return {
82
- type: "nextjs",
83
- name,
84
- srcDir,
85
- usesTypeScript,
86
- usesAppRouter
87
- };
88
- }
89
- if (deps["vite"] || fs.existsSync(path.join(cwd, "vite.config.ts")) || fs.existsSync(path.join(cwd, "vite.config.js"))) {
90
- return {
91
- type: "vite",
92
- name,
93
- srcDir,
94
- usesTypeScript,
95
- usesAppRouter: false
96
- };
97
- }
98
- if (deps["react-scripts"]) {
99
- return {
100
- type: "cra",
101
- name,
102
- srcDir,
103
- usesTypeScript,
104
- usesAppRouter: false
105
- };
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
+ }
106
75
  }
107
- return {
108
- type: "unknown",
109
- name,
110
- srcDir,
111
- usesTypeScript,
112
- usesAppRouter: false
113
- };
76
+ return flags;
114
77
  }
115
- function createConfigContent(project) {
116
- const config = {
117
- $schema: "node_modules/uidex/uidex.schema.json",
118
- defaults: {
119
- color: "#3b82f6",
120
- borderStyle: "solid",
121
- borderWidth: 2,
122
- showLabel: true,
123
- labelPosition: "top-left"
124
- },
125
- colors: {
126
- primary: "#3b82f6",
127
- secondary: "#8b5cf6",
128
- success: "#10b981",
129
- warning: "#f59e0b",
130
- error: "#ef4444",
131
- info: "#0ea5e9"
132
- },
133
- scanner: {
134
- rootDir: project.srcDir,
135
- include: ["**/*.tsx", "**/*.jsx"],
136
- exclude: ["**/*.test.*", "**/*.spec.*", "**/*.gen.ts"],
137
- outputPath: `${project.srcDir}/uidex.gen.ts`
138
- }
139
- };
140
- return JSON.stringify(config, null, 2);
78
+ function truncate(str, len) {
79
+ if (str.length <= len) return str;
80
+ return str.slice(0, len - 1) + "\u2026";
141
81
  }
142
- function addToGitignore(entry) {
143
- const cwd = process.cwd();
144
- const gitignorePath = path.join(cwd, ".gitignore");
145
- let content = "";
146
- if (fs.existsSync(gitignorePath)) {
147
- content = fs.readFileSync(gitignorePath, "utf-8");
148
- }
149
- const lines = content.split("\n");
150
- if (lines.some((line) => line.trim() === entry)) {
151
- return false;
152
- }
153
- const newContent = content.endsWith("\n") || content === "" ? `${content}${entry}
154
- ` : `${content}
155
- ${entry}
156
- `;
157
- fs.writeFileSync(gitignorePath, newContent, "utf-8");
158
- return true;
82
+ function pad(str, len) {
83
+ return str.padEnd(len).slice(0, len);
159
84
  }
160
- function createPrompt() {
161
- return readline.createInterface({
162
- input: process.stdin,
163
- output: process.stdout
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"
164
91
  });
165
92
  }
166
- async function askYesNo(rl, question, defaultYes = true) {
167
- const hint = defaultYes ? "[Y/n]" : "[y/N]";
168
- return new Promise((resolve4) => {
169
- rl.question(`${question} ${colors.dim}${hint}${colors.reset} `, (answer) => {
170
- const normalized = answer.trim().toLowerCase();
171
- if (normalized === "") {
172
- resolve4(defaultYes);
173
- } else {
174
- resolve4(normalized === "y" || normalized === "yes");
175
- }
176
- });
177
- });
93
+ function formatError(err) {
94
+ return err instanceof Error ? err.message : String(err);
178
95
  }
179
- function getEntryPointHint(project) {
180
- if (project.type === "nextjs") {
181
- if (project.usesAppRouter) {
182
- return project.srcDir === "src" ? "src/app/layout.tsx" : "app/layout.tsx";
183
- }
184
- return project.srcDir === "src" ? "src/pages/_app.tsx" : "pages/_app.tsx";
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
+ };
185
110
  }
186
- return project.srcDir === "src" ? "src/main.tsx" : "main.tsx";
187
- }
188
- async function init() {
189
- heading("uidex init");
190
- const cwd = process.cwd();
191
- const configPath = path.join(cwd, ".uidex.json");
192
- if (fs.existsSync(configPath)) {
193
- warn(".uidex.json already exists");
194
- const rl = createPrompt();
195
- const overwrite = await askYesNo(rl, "Overwrite existing config?", false);
196
- rl.close();
197
- if (!overwrite) {
198
- info("Keeping existing configuration");
199
- return;
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
+ }
200
141
  }
201
142
  }
202
- const project = detectProject();
203
- log(`Detected project: ${colors.bold}${project.name}${colors.reset}`);
204
- log(`Project type: ${colors.bold}${project.type}${colors.reset}`);
205
- log(
206
- `Language: ${colors.bold}${project.usesTypeScript ? "TypeScript" : "JavaScript"}${colors.reset}`
207
- );
208
- if (project.type === "nextjs") {
209
- log(
210
- `Router: ${colors.bold}${project.usesAppRouter ? "App Router" : "Pages Router"}${colors.reset}`
211
- );
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;
212
149
  }
213
- heading("Creating configuration");
214
- const configContent = createConfigContent(project);
215
- fs.writeFileSync(configPath, configContent, "utf-8");
216
- success("Created .uidex.json");
217
- const genFile = "*.gen.ts";
218
- if (addToGitignore(genFile)) {
219
- success(`Added ${genFile} to .gitignore`);
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
+ }];
220
157
  } else {
221
- info(`${genFile} already in .gitignore`);
158
+ configs = discoverConfigs(startDir);
222
159
  }
223
- heading("Next steps");
224
- const entryPoint = getEntryPointHint(project);
225
- log("1. Add data-uidex attributes to elements you want to track:");
226
- log("");
227
- log(` ${colors.green}<button${colors.reset} data-uidex="submit-btn"${colors.green}>${colors.reset}Submit${colors.green}</button>${colors.reset}`);
228
- log("");
229
- log("2. Run the scanner to generate the components registry:");
230
- log("");
231
- log(` ${colors.yellow}npx uidex-scan${colors.reset}`);
232
- log("");
233
- log("3. Import the generated file in your entry point:");
234
- log("");
235
- log(` ${colors.dim}// ${entryPoint}${colors.reset}`);
236
- log(` ${colors.cyan}import${colors.reset} './${project.srcDir === "src" ? "" : "src/"}uidex.gen';`);
237
- log("");
238
- log("4. Add the UidexDevtools component anywhere in your app:");
239
- log("");
240
- log(` ${colors.cyan}import${colors.reset} { UidexDevtools } ${colors.cyan}from${colors.reset} 'uidex';`);
241
- log("");
242
- log(` ${colors.green}<UidexDevtools />${colors.reset}`);
243
- log("");
244
- success("Setup complete!");
245
- log("");
160
+ cachedResult = { from: startDir, configs };
161
+ return configs;
246
162
  }
247
-
248
- // scripts/scan.ts
249
- var fs2 = __toESM(require("fs"), 1);
250
- var path2 = __toESM(require("path"), 1);
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
+ });
251
183
 
252
184
  // scripts/scanner-utils.ts
253
- var UIDEX_PAGE_FILENAME = "UIDEX_PAGE.md";
254
- var UIDEX_FEATURE_FILENAME = "UIDEX_FEATURE.md";
255
185
  function parseFrontmatter(content) {
256
186
  const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
257
187
  if (!match) {
@@ -264,6 +194,18 @@ function parseFrontmatter(content) {
264
194
  let inComponents = false;
265
195
  const components = [];
266
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
+ }
267
209
  if (/^components:\s*$/.test(line.trimEnd())) {
268
210
  inComponents = true;
269
211
  continue;
@@ -356,31 +298,75 @@ function parseAcceptanceCriteria(content) {
356
298
  }
357
299
  if (inAcceptance) {
358
300
  if (/^##\s+/.test(line)) break;
359
- const match = line.match(/^-\s+(.+)$/);
301
+ const match = line.match(/^-\s+(?:\[[ x]\]\s*)?(.+)$/);
360
302
  if (match) criteria.push(match[1].trim());
361
303
  }
362
304
  }
363
305
  return criteria;
364
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
+ }
365
329
  function parseMarkdownTitle(content) {
366
330
  const match = content.match(/^#\s+(.+)$/m);
367
331
  return match ? match[1].trim() : null;
368
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
+ }
369
353
  function escapeRegex(str) {
370
354
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
371
355
  }
372
356
  function extractComponents(content) {
373
357
  const results = [];
374
358
  const jsDocBlocks = extractJSDocBlocks(content);
375
- const regex = /data-uidex\s*=\s*(?:"([^"]+)"|'([^']+)'|\{["'`]([^"'`]+)["'`]\})/g;
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;
376
362
  const lines = content.split("\n");
377
363
  lines.forEach((line, index) => {
364
+ const lineNumber = index + 1;
378
365
  let match;
379
- regex.lastIndex = 0;
380
- while ((match = regex.exec(line)) !== null) {
366
+ componentRegex.lastIndex = 0;
367
+ while ((match = componentRegex.exec(line)) !== null) {
381
368
  const id = match[1] || match[2] || match[3];
382
369
  if (id) {
383
- const lineNumber = index + 1;
384
370
  const matchingBlocks = jsDocBlocks.filter(
385
371
  (block) => block.id === id && block.endLine <= lineNumber && lineNumber - block.endLine <= 5
386
372
  );
@@ -390,72 +376,830 @@ function extractComponents(content) {
390
376
  results.push({
391
377
  id,
392
378
  line: lineNumber,
379
+ kind: "component",
393
380
  ...matchingBlock?.description ? { doc: matchingBlock.description } : {}
394
381
  });
395
382
  }
396
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
+ }
397
406
  });
398
407
  return results;
399
408
  }
400
-
401
- // scripts/scan.ts
402
- var DEFAULT_SOURCE = {
403
- rootDir: "src",
404
- include: ["**/*.tsx", "**/*.jsx"]
405
- };
406
- var DEFAULT_CONFIG = {
407
- sources: [DEFAULT_SOURCE],
408
- exclude: ["**/*.test.*", "**/*.spec.*", "**/node_modules/**", "**/*.gen.ts"],
409
- outputPath: "src/uidex.gen.ts"
410
- };
411
- function loadConfig() {
412
- const configPath = path2.resolve(process.cwd(), ".uidex.json");
413
- if (!fs2.existsSync(configPath)) {
414
- console.log("No .uidex.json found, using defaults");
415
- return DEFAULT_CONFIG;
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";
416
415
  }
417
- try {
418
- const content = fs2.readFileSync(configPath, "utf-8");
419
- const config = JSON.parse(content);
420
- if (!config.scanner) {
421
- console.log("No scanner config in .uidex.json, using defaults");
422
- return DEFAULT_CONFIG;
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)}`;
423
429
  }
424
- const scanner = config.scanner;
425
- return {
426
- sources: scanner.sources?.length ? scanner.sources.map((source) => ({
427
- rootDir: source.rootDir,
428
- include: source.include,
429
- exclude: source.exclude,
430
- prefix: source.prefix
431
- })) : DEFAULT_CONFIG.sources,
432
- exclude: scanner.exclude ?? DEFAULT_CONFIG.exclude,
433
- outputPath: scanner.outputPath ?? DEFAULT_CONFIG.outputPath
434
- };
435
- } catch (error2) {
436
- console.error("Error reading .uidex.json:", error2);
437
- return DEFAULT_CONFIG;
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;
438
436
  }
437
+ return "global";
439
438
  }
440
- function globToRegex(glob) {
441
- const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/{{GLOBSTAR}}/g, ".*");
442
- return new RegExp(`^${escaped}$`);
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
+ }));
443
450
  }
444
- function compilePatterns(patterns) {
445
- return patterns.map(globToRegex);
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;
446
470
  }
447
- function matchesPatterns(filePath, patterns) {
448
- const normalizedPath = filePath.replace(/\\/g, "/");
449
- return patterns.some((regex) => regex.test(normalizedPath));
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;
450
484
  }
451
- function walkDir(dir, baseDir = dir) {
452
- const result = { files: [], pageDocs: /* @__PURE__ */ new Map(), featureDocs: /* @__PURE__ */ new Map() };
453
- if (!fs2.existsSync(dir)) {
454
- return result;
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
+ }
455
495
  }
456
- const entries = fs2.readdirSync(dir, { withFileTypes: true });
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 });
457
1201
  for (const entry of entries) {
458
- const fullPath = path2.join(dir, entry.name);
1202
+ const fullPath = path6.join(dir, entry.name);
459
1203
  if (entry.isDirectory()) {
460
1204
  if (entry.name === "node_modules") continue;
461
1205
  const sub = walkDir(fullPath, baseDir);
@@ -466,25 +1210,31 @@ function walkDir(dir, baseDir = dir) {
466
1210
  for (const [k, v] of sub.featureDocs) {
467
1211
  result.featureDocs.set(k, v);
468
1212
  }
1213
+ for (const d of sub.routeDirs) {
1214
+ result.routeDirs.add(d);
1215
+ }
469
1216
  } else if (entry.isFile()) {
1217
+ const relativeDir = path6.relative(baseDir, dir).replace(/\\/g, "/") || ".";
470
1218
  if (entry.name === UIDEX_PAGE_FILENAME || entry.name === UIDEX_FEATURE_FILENAME) {
471
- const relativeDir = path2.relative(baseDir, dir).replace(/\\/g, "/") || ".";
472
- const content = fs2.readFileSync(fullPath, "utf-8");
1219
+ const content = fs7.readFileSync(fullPath, "utf-8");
473
1220
  if (entry.name === UIDEX_PAGE_FILENAME) {
474
1221
  result.pageDocs.set(relativeDir, content);
475
1222
  } else {
476
1223
  result.featureDocs.set(relativeDir, content);
477
1224
  }
478
1225
  } else {
479
- const relativePath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
1226
+ const relativePath = path6.relative(baseDir, fullPath).replace(/\\/g, "/");
480
1227
  result.files.push(relativePath);
1228
+ if (/^page\.[tjm]sx?$/.test(entry.name)) {
1229
+ result.routeDirs.add(relativeDir);
1230
+ }
481
1231
  }
482
1232
  }
483
1233
  }
484
1234
  return result;
485
1235
  }
486
- function findFilesForSource(source, globalExclude) {
487
- const rootDir = path2.resolve(process.cwd(), source.rootDir);
1236
+ function findFilesForSource(source, globalExclude, configDir = process.cwd()) {
1237
+ const rootDir = path6.resolve(configDir, source.rootDir);
488
1238
  const walkResult = walkDir(rootDir);
489
1239
  const includeRegexes = compilePatterns(source.include);
490
1240
  const excludeRegexes = compilePatterns([...globalExclude, ...source.exclude ?? []]);
@@ -496,7 +1246,7 @@ function findFilesForSource(source, globalExclude) {
496
1246
  const outputPath = source.prefix ? `${source.prefix}/${relativePath}` : relativePath;
497
1247
  return {
498
1248
  relativePath,
499
- fullPath: path2.join(rootDir, relativePath),
1249
+ fullPath: path6.join(rootDir, relativePath),
500
1250
  outputPath
501
1251
  };
502
1252
  });
@@ -508,10 +1258,21 @@ function findFilesForSource(source, globalExclude) {
508
1258
  }
509
1259
  return result;
510
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
+ }
511
1271
  return {
512
1272
  files,
513
1273
  pageDocs: applyPrefix(walkResult.pageDocs),
514
- featureDocs: applyPrefix(walkResult.featureDocs)
1274
+ featureDocs: applyPrefix(walkResult.featureDocs),
1275
+ routeDirs
515
1276
  };
516
1277
  }
517
1278
  function buildPages(docs, sourceComponentIds, sourceRootDir) {
@@ -523,7 +1284,7 @@ function buildPages(docs, sourceComponentIds, sourceRootDir) {
523
1284
  }
524
1285
  for (const [id, filePaths] of sourceComponentIds) {
525
1286
  for (const filePath of filePaths) {
526
- const fileDir = filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ".";
1287
+ const fileDir = parentDir(filePath);
527
1288
  const nearestDir = docDirs.find(
528
1289
  (dir) => dir === "." || fileDir === dir || fileDir.startsWith(dir + "/")
529
1290
  );
@@ -534,24 +1295,111 @@ function buildPages(docs, sourceComponentIds, sourceRootDir) {
534
1295
  }
535
1296
  return docDirs.map((dir) => {
536
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
+ }
537
1303
  return {
538
1304
  dir: fullDir,
539
- content: docs.get(dir),
540
- componentIds: [...pageComponentSets.get(dir)].sort()
1305
+ content: doc.body,
1306
+ componentIds: [...ids].sort(),
1307
+ ...doc.rootId ? { rootId: doc.rootId } : {},
1308
+ ...doc.description ? { description: doc.description } : {}
541
1309
  };
542
1310
  });
543
1311
  }
544
- function buildFeatures(featureDocs, sourceRootDir) {
545
- const features = [];
546
- for (const [dir, { body, explicitComponents }] of featureDocs) {
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) => {
547
1331
  const fullDir = dir === "." ? sourceRootDir : `${sourceRootDir}/${dir}`;
548
- features.push({
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 {
549
1338
  dir: fullDir,
550
- content: body,
551
- componentIds: [...explicitComponents].sort()
552
- });
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
+ }
553
1382
  }
554
- return features;
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 };
555
1403
  }
556
1404
  function generateTestOutput(components, pages, features) {
557
1405
  const sortedIds = Object.keys(components).sort();
@@ -568,7 +1416,8 @@ export type Route = typeof routes[keyof typeof routes];
568
1416
  export const pages = [
569
1417
  ${pageRoutes.map((p) => {
570
1418
  const ids = p.componentIds.map((id) => `"${id}"`).join(", ");
571
- return ` { dir: "${p.dir}", route: "${p.route}", componentIds: [${ids}] as const }`;
1419
+ const rootPart = p.rootId ? `, rootId: "${p.rootId}"` : "";
1420
+ return ` { dir: "${p.dir}", route: "${p.route}", componentIds: [${ids}] as const${rootPart} }`;
572
1421
  }).join(",\n")}
573
1422
  ] as const;
574
1423
  ` : "";
@@ -588,15 +1437,23 @@ export const componentIds = [${idsArrayStr}] as const;
588
1437
  export type ComponentId = typeof componentIds[number];
589
1438
  ${routesStr}${pagesStr}${featuresStr}`;
590
1439
  }
591
- function generateOutput(components, pages, features) {
1440
+ function generateOutput(components, pages, features, gitContext, uiComponents) {
592
1441
  const sortedIds = Object.keys(components).sort();
593
1442
  const entriesStr = sortedIds.map((id) => {
594
1443
  const locations = components[id].map((loc) => {
595
- const parts = [`filePath: "${loc.filePath}"`, `line: ${loc.line}`];
1444
+ const parts = [
1445
+ `filePath: "${loc.filePath}"`,
1446
+ `line: ${loc.line}`,
1447
+ `kind: "${loc.kind}"`
1448
+ ];
596
1449
  if (loc.doc) {
597
1450
  const escapedDoc = loc.doc.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
598
1451
  parts.push(`doc: "${escapedDoc}"`);
599
1452
  }
1453
+ if (loc.scopes && loc.scopes.length > 0) {
1454
+ const scopesList = loc.scopes.map((s) => `"${s}"`).join(", ");
1455
+ parts.push(`scopes: [${scopesList}]`);
1456
+ }
600
1457
  return `{ ${parts.join(", ")} }`;
601
1458
  }).join(", ");
602
1459
  return ` "${id}": [${locations}]`;
@@ -606,7 +1463,9 @@ function generateOutput(components, pages, features) {
606
1463
  return entries.map((entry) => {
607
1464
  const escaped = entry.content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
608
1465
  const ids = entry.componentIds.map((id) => `"${id}"`).join(", ");
609
- return ` { dir: "${entry.dir}", content: \`${escaped}\`, componentIds: [${ids}] }`;
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} }`;
610
1469
  }).join(",\n");
611
1470
  }
612
1471
  const pagesStr = pages.length > 0 ? `
@@ -621,60 +1480,115 @@ ${serializeDocEntries(features)}
621
1480
  ` : "";
622
1481
  const hasPages = pages.length > 0;
623
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
+ ` : "";
624
1494
  const importParts = ["registerComponents"];
625
1495
  if (hasPages) importParts.push("registerPages");
626
1496
  if (hasFeatures) importParts.push("registerFeatures");
627
- const imports = `import { ${importParts.join(", ")} } from 'uidex';`;
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';`;
628
1503
  const regParts = ["registerComponents(components);"];
629
1504
  if (hasPages) regParts.push("registerPages(pages);");
630
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
+ }
631
1510
  const registrations = `// Auto-register
632
1511
  ${regParts.join("\n")}
633
1512
  `;
634
- return `// Auto-generated by uidex scanner
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
635
1521
  // Do not edit this file manually
636
1522
 
637
1523
  ${imports}
638
1524
 
639
1525
  export const components = {
640
1526
  ${entriesStr}
641
- };
1527
+ } satisfies UidexMap;
642
1528
 
643
1529
  export const componentIds = [${idsArrayStr}] as const;
644
1530
 
645
1531
  export type ComponentId = typeof componentIds[number];
646
- ${pagesStr}${featuresStr}
647
- ${registrations}`;
1532
+ ${pagesStr}${featuresStr}${uiComponentsStr}
1533
+ ${registrations}
1534
+ ${defaultExport}`;
648
1535
  }
649
1536
  function ensureOutputDir(outputPath) {
650
- const dir = path2.dirname(outputPath);
651
- if (!fs2.existsSync(dir)) {
652
- fs2.mkdirSync(dir, { recursive: true });
1537
+ const dir = path6.dirname(outputPath);
1538
+ if (!fs7.existsSync(dir)) {
1539
+ fs7.mkdirSync(dir, { recursive: true });
653
1540
  }
654
1541
  }
655
1542
  function runScan(config) {
656
1543
  const components = {};
657
1544
  const pages = [];
658
1545
  const features = [];
1546
+ let allUiComponents = [];
1547
+ const routeDirs = /* @__PURE__ */ new Set();
659
1548
  let totalComponents = 0;
660
1549
  let totalFiles = 0;
1550
+ const allProvenanceFiles = [];
1551
+ const allExtractedPrimitives = [];
661
1552
  for (const source of config.sources) {
662
- const sourceResult = findFilesForSource(source, config.exclude);
1553
+ const sourceResult = findFilesForSource(source, config.exclude, config.configDir);
663
1554
  totalFiles += sourceResult.files.length;
1555
+ for (const d of sourceResult.routeDirs) {
1556
+ routeDirs.add(d === "." ? source.rootDir : `${source.rootDir}/${d}`);
1557
+ }
664
1558
  console.log(
665
1559
  `Scanning ${source.rootDir}: ${sourceResult.files.length} files${source.prefix ? ` (\u2192 ${source.prefix}/*)` : ""}`
666
1560
  );
667
1561
  const sourceComponentIds = /* @__PURE__ */ new Map();
668
1562
  for (const file of sourceResult.files) {
669
- const content = fs2.readFileSync(file.fullPath, "utf-8");
1563
+ const content = fs7.readFileSync(file.fullPath, "utf-8");
1564
+ allProvenanceFiles.push({
1565
+ absolutePath: toPosix(file.fullPath),
1566
+ outputPath: file.outputPath,
1567
+ content
1568
+ });
670
1569
  const fileComponents = extractComponents(content);
1570
+ const seenIdsInFile = /* @__PURE__ */ new Set();
671
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);
672
1585
  if (!components[component.id]) {
673
1586
  components[component.id] = [];
674
1587
  }
675
1588
  components[component.id].push({
676
1589
  filePath: file.outputPath,
677
1590
  line: component.line,
1591
+ kind: component.kind,
678
1592
  ...component.doc ? { doc: component.doc } : {}
679
1593
  });
680
1594
  if (!sourceComponentIds.has(component.id)) {
@@ -684,8 +1598,19 @@ function runScan(config) {
684
1598
  totalComponents++;
685
1599
  }
686
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
+ }
687
1612
  const sourcePages = buildPages(
688
- sourceResult.pageDocs,
1613
+ parsedPageDocs,
689
1614
  sourceComponentIds,
690
1615
  source.rootDir
691
1616
  );
@@ -693,15 +1618,47 @@ function runScan(config) {
693
1618
  const parsedFeatureDocs = /* @__PURE__ */ new Map();
694
1619
  for (const [dir, rawContent] of sourceResult.featureDocs) {
695
1620
  const { frontmatter, body } = parseFrontmatter(rawContent);
1621
+ const featureDescription = frontmatter.description ?? parseBlockquoteDescription(body);
696
1622
  parsedFeatureDocs.set(dir, {
697
1623
  body,
698
- explicitComponents: frontmatter.components ?? []
1624
+ explicitComponents: frontmatter.components ?? [],
1625
+ ...featureDescription ? { description: featureDescription } : {}
699
1626
  });
700
1627
  }
701
- const sourceFeatures = buildFeatures(parsedFeatureDocs, source.rootDir);
1628
+ const sourceFeatures = buildFeatures(parsedFeatureDocs, sourceComponentIds, source.rootDir);
702
1629
  features.push(...sourceFeatures);
703
1630
  }
704
- return { components, pages, features, totalComponents, totalFiles };
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
+ };
705
1662
  }
706
1663
  function printSummary(result) {
707
1664
  console.log("");
@@ -733,8 +1690,14 @@ function printSummary(result) {
733
1690
  }
734
1691
  console.log("");
735
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
+ }
736
1699
  }
737
- function checkCoverage(result) {
1700
+ function checkCoverage(result, config) {
738
1701
  const allComponentIds = Object.keys(result.components);
739
1702
  const coveredIds = /* @__PURE__ */ new Set([
740
1703
  ...result.pages.flatMap((p) => p.componentIds),
@@ -742,16 +1705,23 @@ function checkCoverage(result) {
742
1705
  ]);
743
1706
  const uncoveredIds = allComponentIds.filter((id) => !coveredIds.has(id));
744
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);
745
1713
  let passed = true;
746
1714
  if (uncoveredIds.length > 0) {
747
1715
  passed = false;
748
- console.log(`Components missing UIDEX_PAGE.md coverage (${uncoveredIds.length}):`);
1716
+ console.log(`Components missing doc coverage (${uncoveredIds.length}):`);
749
1717
  for (const id of uncoveredIds.sort()) {
750
1718
  const locations = result.components[id];
751
1719
  for (const loc of locations) {
752
1720
  console.log(` "${id}" at ${loc.filePath}:${loc.line}`);
753
1721
  }
754
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)");
755
1725
  console.log("");
756
1726
  }
757
1727
  if (orphanedPages.length > 0) {
@@ -762,18 +1732,84 @@ function checkCoverage(result) {
762
1732
  }
763
1733
  console.log("");
764
1734
  }
765
- if (passed) {
766
- const totalDocs = result.pages.length + result.features.length;
767
- console.log(
768
- `All ${allComponentIds.length} components covered by ${totalDocs} UIDEX docs (${result.pages.length} pages, ${result.features.length} features)`
769
- );
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("");
770
1742
  }
771
- return passed;
772
- }
773
- function printConfig(config) {
774
- console.log("Configuration:");
775
- console.log(` Sources: ${config.sources.length}`);
776
- for (const source of config.sources) {
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) {
777
1813
  const prefix = source.prefix ? ` (prefix: ${source.prefix})` : "";
778
1814
  console.log(` - ${source.rootDir}${prefix}`);
779
1815
  console.log(` Include: ${source.include.join(", ")}`);
@@ -785,32 +1821,383 @@ function printConfig(config) {
785
1821
  console.log(` Output: ${config.outputPath}
786
1822
  `);
787
1823
  }
788
- function scan() {
789
- const isCheck = process.argv.includes("--check");
790
- console.log(`uidex scanner${isCheck ? " (check mode)" : ""} starting...
791
- `);
792
- const config = loadConfig();
1824
+ function scanSingle(configDir) {
1825
+ const config = loadConfig({ configDir });
793
1826
  printConfig(config);
794
1827
  const result = runScan(config);
795
1828
  printSummary(result);
796
- if (isCheck) {
797
- const passed = checkCoverage(result);
798
- process.exit(passed ? 0 : 1);
799
- }
800
- const outputPath = path2.resolve(process.cwd(), config.outputPath);
1829
+ const outputPath = path6.resolve(config.configDir, config.outputPath);
801
1830
  ensureOutputDir(outputPath);
802
- const output = generateOutput(result.components, result.pages, result.features);
803
- fs2.writeFileSync(outputPath, output, "utf-8");
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");
804
1837
  console.log(`Generated: ${config.outputPath}`);
805
1838
  const testOutputPath = outputPath.replace(/\.ts$/, ".test.ts");
806
1839
  const testOutput = generateTestOutput(result.components, result.pages, result.features);
807
- fs2.writeFileSync(testOutputPath, testOutput, "utf-8");
1840
+ fs7.writeFileSync(testOutputPath, testOutput, "utf-8");
808
1841
  console.log(`Generated: ${config.outputPath.replace(/\.ts$/, ".test.ts")}`);
809
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();
810
2194
 
811
2195
  // scripts/scaffold.ts
812
- var fs3 = __toESM(require("fs"), 1);
813
- var path3 = __toESM(require("path"), 1);
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();
814
2201
  function toTestFileName(dir, title) {
815
2202
  if (title) {
816
2203
  return title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -820,7 +2207,6 @@ function toTestFileName(dir, title) {
820
2207
  }
821
2208
  function generateTestFile(entry, fixtureImport) {
822
2209
  const title = parseMarkdownTitle(entry.content) ?? entry.dir;
823
- const criteria = parseAcceptanceCriteria(entry.content);
824
2210
  const componentList = entry.componentIds.map((id) => `"${id}"`).join(", ");
825
2211
  const docFile = entry.kind === "page" ? "UIDEX_PAGE.md" : "UIDEX_FEATURE.md";
826
2212
  let body = "";
@@ -847,35 +2233,36 @@ function generateTestFile(entry, fixtureImport) {
847
2233
  `;
848
2234
  body += ` });
849
2235
  `;
850
- } else {
851
- body += ` // Components: ${componentList}
852
- `;
853
- }
854
- if (criteria.length > 0) {
855
- body += "\n";
856
- for (const criterion of criteria) {
857
- const escaped = criterion.replace(/'/g, "\\'");
858
- body += ` test.todo('${escaped}');
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}');
859
2242
  `;
2243
+ }
860
2244
  }
2245
+ } else {
2246
+ body += ` // Components: ${componentList}
2247
+ `;
861
2248
  }
862
2249
  body += `});
863
2250
  `;
864
2251
  return body;
865
2252
  }
866
- function scaffold(outputDir) {
2253
+ function scaffold(outputDir, configDir) {
867
2254
  const dir = outputDir ?? "e2e";
868
2255
  heading("uidex scaffold");
869
2256
  info(`Output directory: ${dir}/`);
870
- const config = loadConfig();
2257
+ const config = loadConfig({ configDir });
871
2258
  const result = runScan(config);
872
2259
  if (result.pages.length === 0 && result.features.length === 0) {
873
2260
  warn("No pages or features found. Create UIDEX_PAGE.md or UIDEX_FEATURE.md files first.");
874
2261
  return;
875
2262
  }
876
- const absDir = path3.resolve(process.cwd(), dir);
877
- if (!fs3.existsSync(absDir)) {
878
- fs3.mkdirSync(absDir, { recursive: true });
2263
+ const absDir = path7.resolve(process.cwd(), dir);
2264
+ if (!fs8.existsSync(absDir)) {
2265
+ fs8.mkdirSync(absDir, { recursive: true });
879
2266
  }
880
2267
  const fixtureImport = "./fixtures";
881
2268
  let generated = 0;
@@ -887,221 +2274,1535 @@ function scaffold(outputDir) {
887
2274
  for (const entry of entries) {
888
2275
  const title = parseMarkdownTitle(entry.content);
889
2276
  const fileName = `${entry.kind}-${toTestFileName(entry.dir, title)}.spec.ts`;
890
- const filePath = path3.join(absDir, fileName);
891
- if (fs3.existsSync(filePath)) {
2277
+ const filePath = path7.join(absDir, fileName);
2278
+ if (fs8.existsSync(filePath)) {
892
2279
  info(`Skipped ${fileName} (already exists)`);
893
2280
  skipped++;
894
2281
  continue;
895
2282
  }
896
2283
  const content = generateTestFile(entry, fixtureImport);
897
- fs3.writeFileSync(filePath, content, "utf-8");
2284
+ fs8.writeFileSync(filePath, content, "utf-8");
898
2285
  success(`Generated ${fileName}`);
899
2286
  generated++;
900
2287
  }
901
- const fixturesPath = path3.join(absDir, "fixtures.ts");
902
- if (!fs3.existsSync(fixturesPath)) {
903
- fs3.writeFileSync(
904
- fixturesPath,
905
- `export { test, expect } from 'uidex/playwright';
906
- `,
907
- "utf-8"
908
- );
909
- success("Generated fixtures.ts");
910
- generated++;
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);
911
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");
912
3430
  console.log("");
913
- info(`${generated} file(s) generated, ${skipped} skipped`);
914
- if (generated > 0) {
915
- console.log("");
916
- info("Next steps:");
917
- console.log(" 1. Replace test.todo() calls with test implementations");
918
- console.log(" 2. Use the uidex fixture for type-safe selectors:");
919
- console.log("");
920
- console.log(" test('add todo', async ({ uidex }) => {");
921
- console.log(" await uidex('todo-input').fill('Buy milk');");
922
- console.log(" await uidex('todo-add-button').click();");
923
- console.log(" });");
924
- 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();
925
3453
  }
926
3454
  }
927
3455
 
928
- // scripts/claude-setup.ts
929
- var fs4 = __toESM(require("fs"), 1);
930
- var path4 = __toESM(require("path"), 1);
931
- function readTemplate(filename) {
932
- const candidates = [
933
- // Built package: __dirname = <pkg>/dist/scripts → ../../claude/
934
- path4.resolve(__dirname, "..", "..", "claude", filename),
935
- // Dev mode: __dirname = <repo>/scripts → ../claude/
936
- path4.resolve(__dirname, "..", "claude", filename)
937
- ];
938
- for (const candidate of candidates) {
939
- try {
940
- return fs4.readFileSync(candidate, "utf-8");
941
- } catch {
942
- }
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);
943
3464
  }
944
- throw new Error(
945
- `Template not found: ${filename}. The uidex package may be corrupted \u2014 try reinstalling.`
946
- );
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 };
947
3473
  }
948
- function ensureDir(dirPath) {
949
- fs4.mkdirSync(dirPath, { recursive: true });
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
+ });
950
3496
  }
951
- function addRules() {
952
- const cwd = process.cwd();
953
- const targetDir = path4.join(cwd, ".claude", "rules");
954
- const targetPath = path4.join(targetDir, "uidex.md");
955
- const content = readTemplate("rules.md");
956
- ensureDir(targetDir);
957
- fs4.writeFileSync(targetPath, content, "utf-8");
958
- success("Added .claude/rules/uidex.md");
3497
+ function confirm(question) {
3498
+ return prompt2(question).then((a) => a.toLowerCase() === "y");
959
3499
  }
960
- function removeRules() {
961
- const targetPath = path4.join(process.cwd(), ".claude", "rules", "uidex.md");
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 });
962
3504
  try {
963
- fs4.unlinkSync(targetPath);
964
- success("Removed .claude/rules/uidex.md");
965
- } catch (e) {
966
- if (e.code === "ENOENT") {
967
- info(".claude/rules/uidex.md does not exist");
968
- } else {
969
- throw e;
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
+ );
970
3520
  }
3521
+ console.log();
3522
+ } catch (err) {
3523
+ error(`Failed to list integrations: ${formatError(err)}`);
3524
+ process.exit(1);
971
3525
  }
972
3526
  }
973
- function addSkill() {
974
- const cwd = process.cwd();
975
- const targetDir = path4.join(cwd, ".claude", "commands");
976
- const targetPath = path4.join(targetDir, "uidex-audit.md");
977
- const content = readTemplate("audit-command.md");
978
- ensureDir(targetDir);
979
- fs4.writeFileSync(targetPath, content, "utf-8");
980
- success("Added .claude/commands/uidex-audit.md");
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);
981
3545
  }
982
- function removeSkill() {
983
- const targetPath = path4.join(process.cwd(), ".claude", "commands", "uidex-audit.md");
3546
+ async function createJiraIntegration(client, orgId, label, url, email, apiToken) {
3547
+ info("Testing connection...");
984
3548
  try {
985
- fs4.unlinkSync(targetPath);
986
- success("Removed .claude/commands/uidex-audit.md");
987
- } catch (e) {
988
- if (e.code === "ENOENT") {
989
- info(".claude/commands/uidex-audit.md does not exist");
990
- } else {
991
- throw e;
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);
992
3565
  }
3566
+ success(`Integration created: ${label} (id: ${integration.id})`);
3567
+ } catch (err) {
3568
+ error(`Failed to create integration: ${formatError(err)}`);
3569
+ process.exit(1);
993
3570
  }
994
3571
  }
995
- var UIDEX_HOOK_COMMAND = "npx uidex scan --check";
996
- function isUidexHookEntry(entry) {
997
- return entry.hooks?.some((h) => h.command === UIDEX_HOOK_COMMAND) ?? false;
998
- }
999
- function readSettings(settingsPath) {
3572
+ async function addGithub(client, orgId, endpoint) {
3573
+ let slug = null;
1000
3574
  try {
1001
- return JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
1002
- } catch (e) {
1003
- if (e.code === "ENOENT") {
1004
- return {};
3575
+ const res = await fetch(`${endpoint}/api/server/info`);
3576
+ if (res.ok) {
3577
+ const data = await res.json();
3578
+ slug = data.githubAppSlug;
1005
3579
  }
1006
- error(`Failed to parse .claude/settings.json: ${e.message}`);
3580
+ } catch {
3581
+ }
3582
+ if (!slug) {
3583
+ error("GitHub App is not configured on this uidex instance.");
1007
3584
  process.exit(1);
1008
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);
1009
3616
  }
1010
- function addHooks() {
1011
- const cwd = process.cwd();
1012
- const settingsDir = path4.join(cwd, ".claude");
1013
- const settingsPath = path4.join(settingsDir, "settings.json");
1014
- const settings = readSettings(settingsPath);
1015
- if (!settings.hooks) {
1016
- settings.hooks = {};
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);
1017
3627
  }
1018
- if (!Array.isArray(settings.hooks.PostToolUse)) {
1019
- settings.hooks.PostToolUse = [];
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);
1020
3647
  }
1021
- if (settings.hooks.PostToolUse.some(isUidexHookEntry)) {
1022
- info("uidex hook already configured in .claude/settings.json");
1023
- return;
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);
1024
3669
  }
1025
- settings.hooks.PostToolUse.push({
1026
- matcher: "Edit|Write",
1027
- hooks: [
1028
- {
1029
- type: "command",
1030
- command: UIDEX_HOOK_COMMAND,
1031
- timeout: 30
1032
- }
1033
- ]
1034
- });
1035
- ensureDir(settingsDir);
1036
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1037
- success("Added uidex hook to .claude/settings.json");
1038
3670
  }
1039
- function removeHooks() {
1040
- const settingsPath = path4.join(process.cwd(), ".claude", "settings.json");
1041
- const settings = readSettings(settingsPath);
1042
- if (!Array.isArray(settings.hooks?.PostToolUse)) {
1043
- info("No PostToolUse hooks configured");
1044
- return;
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);
1045
3676
  }
1046
- const before = settings.hooks.PostToolUse.length;
1047
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(
1048
- (entry) => !isUidexHookEntry(entry)
1049
- );
1050
- const after = settings.hooks.PostToolUse.length;
1051
- if (before === after) {
1052
- info("No uidex hook found in .claude/settings.json");
1053
- return;
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
+ }
1054
3696
  }
1055
- if (settings.hooks.PostToolUse.length === 0) {
1056
- delete settings.hooks.PostToolUse;
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);
1057
3703
  }
1058
- if (Object.keys(settings.hooks).length === 0) {
1059
- delete settings.hooks;
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);
1060
3737
  }
1061
- fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1062
- success("Removed uidex hook from .claude/settings.json");
1063
3738
  }
1064
- function claudeInit() {
1065
- heading("uidex claude init");
1066
- addRules();
1067
- addSkill();
1068
- addHooks();
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
+ );
1069
3749
  console.log("");
1070
- success("Claude Code integration ready");
1071
- }
1072
- function claudeTeardown() {
1073
- heading("uidex claude teardown");
1074
- removeRules();
1075
- removeSkill();
1076
- removeHooks();
3750
+ console.log("Flags:");
3751
+ console.log(" --org=<orgId> Override organization");
1077
3752
  console.log("");
1078
- success("Claude Code integration removed");
1079
- }
1080
- function printClaudeHelp() {
1081
- heading("uidex claude");
1082
- console.log("Manage Claude Code integration for uidex.\n");
1083
- console.log("Usage: uidex claude <command> [action]\n");
1084
- console.log("Commands:");
1085
- console.log(" init Add rules, skill, and hooks");
1086
- console.log(" teardown Remove rules, skill, and hooks");
1087
- console.log(" rules [add|remove] Manage .claude/rules/uidex.md");
1088
- console.log(" skill [add|remove] Manage .claude/commands/uidex-audit.md");
1089
- console.log(" hooks [add|remove] Manage .claude/settings.json hook");
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");
1090
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
+ }
1091
3782
  }
1092
3783
 
1093
3784
  // scripts/cli.ts
3785
+ init_cli_utils();
1094
3786
  function printHelp() {
1095
3787
  console.log(`
1096
3788
  ${colors.bold}${colors.cyan}uidex${colors.reset}
1097
3789
  `);
1098
3790
  console.log("Usage: uidex <command>\n");
1099
- console.log("Commands:");
1100
- console.log(" init Initialize uidex in your project");
1101
- console.log(" scan Run the component scanner");
1102
- console.log(" scan --check Validate UIDEX_PAGE.md coverage");
1103
- console.log(" scaffold [dir] Generate Playwright test stubs from pages/features");
1104
- console.log(" claude Manage Claude Code integration");
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)");
1105
3806
  console.log("");
1106
3807
  }
1107
3808
  function parseAction(raw) {
@@ -1117,11 +3818,11 @@ function handleClaude(args2) {
1117
3818
  process.exit(1);
1118
3819
  }
1119
3820
  switch (subcommand) {
1120
- case "init":
1121
- claudeInit();
3821
+ case "install":
3822
+ claudeInstall();
1122
3823
  break;
1123
- case "teardown":
1124
- claudeTeardown();
3824
+ case "uninstall":
3825
+ claudeUninstall();
1125
3826
  break;
1126
3827
  case "rules":
1127
3828
  action === "remove" ? removeRules() : addRules();
@@ -1141,6 +3842,10 @@ var args = process.argv.slice(2);
1141
3842
  var command = args[0];
1142
3843
  switch (command) {
1143
3844
  case void 0:
3845
+ case "--help":
3846
+ case "-h":
3847
+ printHelp();
3848
+ break;
1144
3849
  case "init":
1145
3850
  init().catch((err) => {
1146
3851
  console.error("Error during initialization:", err);
@@ -1156,9 +3861,41 @@ switch (command) {
1156
3861
  case "claude":
1157
3862
  handleClaude(args.slice(1));
1158
3863
  break;
1159
- case "--help":
1160
- case "-h":
1161
- printHelp();
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
+ });
1162
3899
  break;
1163
3900
  default:
1164
3901
  console.error(`Unknown command: ${command}`);