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.
- package/README.md +132 -32
- package/claude/audit-command.md +40 -10
- package/claude/rules.md +99 -20
- package/dist/api/index.cjs +254 -0
- package/dist/api/index.cjs.map +1 -0
- package/dist/api/index.d.cts +236 -0
- package/dist/api/index.d.ts +236 -0
- package/dist/api/index.js +226 -0
- package/dist/api/index.js.map +1 -0
- package/dist/core/index.cjs +10149 -2576
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +155 -170
- package/dist/core/index.d.ts +155 -170
- package/dist/core/index.global.js +65920 -2855
- package/dist/core/index.global.js.map +1 -1
- package/dist/core/index.js +10145 -2576
- package/dist/core/index.js.map +1 -1
- package/dist/core/style.css +1170 -612
- package/dist/index.cjs +10129 -2536
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -8
- package/dist/index.d.ts +43 -8
- package/dist/index.js +10132 -2541
- package/dist/index.js.map +1 -1
- package/dist/playwright/index.cjs.map +1 -1
- package/dist/playwright/index.d.cts +2 -2
- package/dist/playwright/index.d.ts +2 -2
- package/dist/playwright/index.js.map +1 -1
- package/dist/playwright/reporter.cjs.map +1 -1
- package/dist/playwright/reporter.js.map +1 -1
- package/dist/react/index.cjs +10129 -2536
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +43 -8
- package/dist/react/index.d.ts +43 -8
- package/dist/react/index.js +10132 -2541
- package/dist/react/index.js.map +1 -1
- package/dist/scripts/cli.cjs +3237 -500
- package/package.json +20 -8
package/dist/scripts/cli.cjs
CHANGED
|
@@ -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
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
143
|
-
|
|
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
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
158
|
+
configs = discoverConfigs(startDir);
|
|
222
159
|
}
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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
|
-
|
|
380
|
-
while ((match =
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
448
|
-
const
|
|
449
|
-
|
|
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
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
540
|
-
componentIds: [...
|
|
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
|
-
|
|
546
|
-
|
|
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
|
-
|
|
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: [...
|
|
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
|
-
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
651
|
-
if (!
|
|
652
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (
|
|
766
|
-
|
|
767
|
-
console.log(
|
|
768
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
|
789
|
-
const
|
|
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
|
-
|
|
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
|
|
803
|
-
|
|
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
|
-
|
|
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
|
|
813
|
-
var
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
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 =
|
|
877
|
-
if (!
|
|
878
|
-
|
|
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 =
|
|
891
|
-
if (
|
|
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
|
-
|
|
2284
|
+
fs8.writeFileSync(filePath, content, "utf-8");
|
|
898
2285
|
success(`Generated ${fileName}`);
|
|
899
2286
|
generated++;
|
|
900
2287
|
}
|
|
901
|
-
const fixturesPath =
|
|
902
|
-
if (!
|
|
903
|
-
|
|
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
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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/
|
|
929
|
-
var
|
|
930
|
-
|
|
931
|
-
function
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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
|
-
|
|
945
|
-
|
|
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
|
|
949
|
-
|
|
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
|
|
952
|
-
|
|
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
|
|
961
|
-
const
|
|
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
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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
|
|
974
|
-
const
|
|
975
|
-
const
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
|
983
|
-
|
|
3546
|
+
async function createJiraIntegration(client, orgId, label, url, email, apiToken) {
|
|
3547
|
+
info("Testing connection...");
|
|
984
3548
|
try {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
}
|
|
991
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
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
|
|
1011
|
-
const
|
|
1012
|
-
const
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
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
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
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
|
|
1040
|
-
const
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
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
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
);
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1056
|
-
|
|
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
|
-
|
|
1059
|
-
|
|
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
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
console.log("
|
|
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(
|
|
1100
|
-
console.log(" init
|
|
1101
|
-
console.log(" scan
|
|
1102
|
-
console.log(" scan --
|
|
1103
|
-
console.log(" scaffold [dir]
|
|
1104
|
-
console.log(" claude
|
|
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 "
|
|
1121
|
-
|
|
3821
|
+
case "install":
|
|
3822
|
+
claudeInstall();
|
|
1122
3823
|
break;
|
|
1123
|
-
case "
|
|
1124
|
-
|
|
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 "
|
|
1160
|
-
|
|
1161
|
-
|
|
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}`);
|