uilint 0.2.8 → 0.2.10

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.
@@ -0,0 +1,4011 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ confirm,
4
+ detectNextAppRouter,
5
+ findNextAppRouterProjects,
6
+ log,
7
+ multiselect,
8
+ note,
9
+ pc,
10
+ select
11
+ } from "./chunk-FRNXXIEM.js";
12
+ import {
13
+ GENSTYLEGUIDE_COMMAND_MD,
14
+ loadSkill
15
+ } from "./chunk-2RNDQVEK.js";
16
+
17
+ // src/commands/install-ui.tsx
18
+ import { render } from "ink";
19
+
20
+ // src/commands/install/components/InstallApp.tsx
21
+ import { useState as useState5, useEffect as useEffect2 } from "react";
22
+ import { Box as Box4, Text as Text5, useApp as useApp4, useInput as useInput4 } from "ink";
23
+
24
+ // src/commands/install/components/Spinner.tsx
25
+ import { useState, useEffect } from "react";
26
+ import { Text } from "ink";
27
+ import { jsx } from "react/jsx-runtime";
28
+ var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
29
+ function Spinner() {
30
+ const [frame, setFrame] = useState(0);
31
+ useEffect(() => {
32
+ const timer = setInterval(() => {
33
+ setFrame((prevFrame) => (prevFrame + 1) % frames.length);
34
+ }, 80);
35
+ return () => {
36
+ clearInterval(timer);
37
+ };
38
+ }, []);
39
+ return /* @__PURE__ */ jsx(Text, { color: "cyan", children: frames[frame] });
40
+ }
41
+
42
+ // src/commands/install/components/ProjectSelector.tsx
43
+ import { useState as useState2 } from "react";
44
+ import { Box, Text as Text2, useInput, useApp } from "ink";
45
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
46
+ function getDetectedProjects(project) {
47
+ const projects = [];
48
+ for (const app of project.nextApps) {
49
+ const relativePath = app.projectPath.replace(project.workspaceRoot + "/", "");
50
+ projects.push({
51
+ id: `next:${app.projectPath}`,
52
+ name: relativePath || ".",
53
+ path: app.projectPath,
54
+ type: "nextjs",
55
+ hint: "Next.js App Router",
56
+ isConfigured: false
57
+ // TODO: detect if overlay is installed
58
+ });
59
+ }
60
+ for (const app of project.viteApps) {
61
+ const relativePath = app.projectPath.replace(project.workspaceRoot + "/", "");
62
+ projects.push({
63
+ id: `vite:${app.projectPath}`,
64
+ name: relativePath || ".",
65
+ path: app.projectPath,
66
+ type: "vite",
67
+ hint: "Vite + React",
68
+ isConfigured: false
69
+ });
70
+ }
71
+ return projects;
72
+ }
73
+ function FrameworkBadge({ type }) {
74
+ switch (type) {
75
+ case "nextjs":
76
+ return /* @__PURE__ */ jsx2(Text2, { color: "white", backgroundColor: "black", children: " Next.js " });
77
+ case "vite":
78
+ return /* @__PURE__ */ jsx2(Text2, { color: "black", backgroundColor: "yellow", children: " Vite " });
79
+ default:
80
+ return /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "Other" });
81
+ }
82
+ }
83
+ function ProjectSelector({
84
+ projects,
85
+ onSelect,
86
+ onCancel
87
+ }) {
88
+ const { exit } = useApp();
89
+ const [cursor, setCursor] = useState2(0);
90
+ useInput((input, key) => {
91
+ if (key.upArrow) {
92
+ setCursor((prev) => prev > 0 ? prev - 1 : projects.length - 1);
93
+ } else if (key.downArrow) {
94
+ setCursor((prev) => prev < projects.length - 1 ? prev + 1 : 0);
95
+ } else if (key.return) {
96
+ const selected = projects[cursor];
97
+ if (selected) {
98
+ onSelect(selected);
99
+ }
100
+ } else if (input === "q" || key.escape) {
101
+ onCancel?.();
102
+ exit();
103
+ }
104
+ });
105
+ if (projects.length === 0) {
106
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
107
+ /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "No projects detected." }),
108
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "UILint works with Next.js (App Router) and Vite + React projects." })
109
+ ] });
110
+ }
111
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
112
+ /* @__PURE__ */ jsx2(Box, { marginBottom: 1, children: /* @__PURE__ */ jsx2(Text2, { bold: true, children: "Select a project to configure:" }) }),
113
+ projects.map((project, index) => {
114
+ const isCursor = index === cursor;
115
+ return /* @__PURE__ */ jsxs(Box, { paddingLeft: 1, children: [
116
+ /* @__PURE__ */ jsx2(Text2, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u203A " : " " }),
117
+ /* @__PURE__ */ jsx2(Box, { width: 12, children: /* @__PURE__ */ jsx2(FrameworkBadge, { type: project.type }) }),
118
+ /* @__PURE__ */ jsx2(Box, { width: 30, children: /* @__PURE__ */ jsx2(Text2, { color: isCursor ? "cyan" : void 0, bold: isCursor, children: project.name }) }),
119
+ project.isConfigured && /* @__PURE__ */ jsx2(Text2, { color: "green", dimColor: true, children: "configured" })
120
+ ] }, project.id);
121
+ }),
122
+ /* @__PURE__ */ jsx2(Box, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs(Text2, { dimColor: true, children: [
123
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "\u2191\u2193" }),
124
+ " navigate",
125
+ " ",
126
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "enter" }),
127
+ " select",
128
+ " ",
129
+ /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: "q" }),
130
+ " quit"
131
+ ] }) })
132
+ ] });
133
+ }
134
+
135
+ // src/commands/install/components/MultiSelect.tsx
136
+ import { useState as useState3, useCallback } from "react";
137
+ import { Box as Box2, Text as Text3, useInput as useInput2, useApp as useApp2 } from "ink";
138
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
139
+ function StatusIndicator({ status, isSelected }) {
140
+ if (status === "installed") {
141
+ return /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" });
142
+ }
143
+ if (isSelected || status === "selected") {
144
+ return /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u25C9" });
145
+ }
146
+ if (status === "partial") {
147
+ return /* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "\u25D0" });
148
+ }
149
+ return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u25CB" });
150
+ }
151
+ function StatusLabel({ status }) {
152
+ if (status === "installed") {
153
+ return /* @__PURE__ */ jsx3(Text3, { color: "green", dimColor: true, children: "installed" });
154
+ }
155
+ if (status === "partial") {
156
+ return /* @__PURE__ */ jsx3(Text3, { color: "yellow", dimColor: true, children: "partial" });
157
+ }
158
+ return /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "-" });
159
+ }
160
+ function ConfigSelector({
161
+ items,
162
+ onSubmit,
163
+ onCancel
164
+ }) {
165
+ const { exit } = useApp2();
166
+ const [cursor, setCursor] = useState3(0);
167
+ const [selected, setSelected] = useState3(() => {
168
+ return new Set(
169
+ items.filter((item) => item.status !== "installed" && !item.disabled).map((item) => item.id)
170
+ );
171
+ });
172
+ const categories = Array.from(new Set(items.map((item) => item.category)));
173
+ const itemsByCategory = /* @__PURE__ */ new Map();
174
+ for (const cat of categories) {
175
+ itemsByCategory.set(cat, items.filter((item) => item.category === cat));
176
+ }
177
+ const flatItems = items;
178
+ const handleToggle = useCallback(() => {
179
+ const item = flatItems[cursor];
180
+ if (!item || item.disabled || item.status === "installed") return;
181
+ setSelected((prev) => {
182
+ const next = new Set(prev);
183
+ if (next.has(item.id)) {
184
+ next.delete(item.id);
185
+ } else {
186
+ next.add(item.id);
187
+ }
188
+ return next;
189
+ });
190
+ }, [cursor, flatItems]);
191
+ useInput2((input, key) => {
192
+ if (key.upArrow) {
193
+ setCursor((prev) => prev > 0 ? prev - 1 : flatItems.length - 1);
194
+ } else if (key.downArrow) {
195
+ setCursor((prev) => prev < flatItems.length - 1 ? prev + 1 : 0);
196
+ } else if (input === " ") {
197
+ handleToggle();
198
+ } else if (key.return) {
199
+ onSubmit(Array.from(selected));
200
+ } else if (input === "q" || key.escape) {
201
+ onCancel?.();
202
+ exit();
203
+ } else if (input === "a") {
204
+ setSelected(
205
+ new Set(
206
+ items.filter((item) => item.status !== "installed" && !item.disabled).map((item) => item.id)
207
+ )
208
+ );
209
+ } else if (input === "n") {
210
+ setSelected(/* @__PURE__ */ new Set());
211
+ }
212
+ });
213
+ let globalIndex = 0;
214
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
215
+ categories.map((category) => {
216
+ const categoryItems = itemsByCategory.get(category) || [];
217
+ const categoryIcon = categoryItems[0]?.categoryIcon || "\u2022";
218
+ return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginBottom: 1, children: [
219
+ /* @__PURE__ */ jsx3(Box2, { children: /* @__PURE__ */ jsxs2(Text3, { bold: true, color: "white", children: [
220
+ categoryIcon,
221
+ " ",
222
+ category
223
+ ] }) }),
224
+ categoryItems.map((item) => {
225
+ const itemIndex = globalIndex++;
226
+ const isCursor = itemIndex === cursor;
227
+ const isItemSelected = selected.has(item.id);
228
+ const isDisabled = item.disabled || item.status === "installed";
229
+ return /* @__PURE__ */ jsxs2(Box2, { paddingLeft: 2, children: [
230
+ /* @__PURE__ */ jsx3(Text3, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u203A " : " " }),
231
+ /* @__PURE__ */ jsx3(Box2, { width: 2, children: /* @__PURE__ */ jsx3(StatusIndicator, { status: item.status, isSelected: isItemSelected }) }),
232
+ /* @__PURE__ */ jsx3(Box2, { width: 28, children: /* @__PURE__ */ jsx3(
233
+ Text3,
234
+ {
235
+ color: isDisabled ? void 0 : isCursor ? "cyan" : void 0,
236
+ dimColor: isDisabled,
237
+ children: item.label
238
+ }
239
+ ) }),
240
+ /* @__PURE__ */ jsx3(Box2, { width: 20, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: item.hint || "" }) }),
241
+ /* @__PURE__ */ jsx3(StatusLabel, { status: item.status })
242
+ ] }, item.id);
243
+ })
244
+ ] }, category);
245
+ }),
246
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
247
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u2191\u2193" }),
248
+ " navigate",
249
+ " ",
250
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "space" }),
251
+ " toggle",
252
+ " ",
253
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "a" }),
254
+ " all",
255
+ " ",
256
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "n" }),
257
+ " none",
258
+ " ",
259
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "enter" }),
260
+ " apply",
261
+ " ",
262
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "q" }),
263
+ " quit"
264
+ ] }) }),
265
+ /* @__PURE__ */ jsx3(Box2, { marginTop: 1, children: /* @__PURE__ */ jsxs2(Text3, { children: [
266
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: selected.size }),
267
+ /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
268
+ " item",
269
+ selected.size !== 1 ? "s" : "",
270
+ " selected"
271
+ ] })
272
+ ] }) })
273
+ ] });
274
+ }
275
+
276
+ // src/commands/install/components/RuleSelector.tsx
277
+ import { useState as useState4, useMemo } from "react";
278
+ import { Box as Box3, Text as Text4, useInput as useInput3, useApp as useApp3 } from "ink";
279
+ import { getRulesByCategory } from "uilint-eslint";
280
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
281
+ function SeverityBadge({ severity }) {
282
+ if (severity === "error") {
283
+ return /* @__PURE__ */ jsx4(Text4, { color: "red", children: "error" });
284
+ }
285
+ if (severity === "warn") {
286
+ return /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "warn" });
287
+ }
288
+ return /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "off" });
289
+ }
290
+ function CategoryHeader({ name, icon }) {
291
+ return /* @__PURE__ */ jsx4(Box3, { marginTop: 1, marginBottom: 0, children: /* @__PURE__ */ jsxs3(Text4, { bold: true, color: "white", children: [
292
+ icon,
293
+ " ",
294
+ name
295
+ ] }) });
296
+ }
297
+ function RuleSelector({
298
+ onSubmit,
299
+ onBack,
300
+ onCancel
301
+ }) {
302
+ const { exit } = useApp3();
303
+ const staticRules = useMemo(() => getRulesByCategory("static"), []);
304
+ const semanticRules = useMemo(() => getRulesByCategory("semantic"), []);
305
+ const allRules = useMemo(() => [...staticRules, ...semanticRules], [staticRules, semanticRules]);
306
+ const [cursor, setCursor] = useState4(0);
307
+ const [viewMode, setViewMode] = useState4("list");
308
+ const [ruleStates, setRuleStates] = useState4(
309
+ () => {
310
+ const map = /* @__PURE__ */ new Map();
311
+ for (const rule of staticRules) {
312
+ map.set(rule.id, {
313
+ enabled: true,
314
+ severity: rule.defaultSeverity === "off" ? "warn" : rule.defaultSeverity
315
+ });
316
+ }
317
+ for (const rule of semanticRules) {
318
+ map.set(rule.id, {
319
+ enabled: false,
320
+ severity: rule.defaultSeverity === "off" ? "warn" : rule.defaultSeverity
321
+ });
322
+ }
323
+ return map;
324
+ }
325
+ );
326
+ const currentRule = allRules[cursor];
327
+ const currentState = currentRule ? ruleStates.get(currentRule.id) : void 0;
328
+ const toggleRule = () => {
329
+ if (!currentRule) return;
330
+ setRuleStates((prev) => {
331
+ const next = new Map(prev);
332
+ const current = next.get(currentRule.id);
333
+ next.set(currentRule.id, { ...current, enabled: !current.enabled });
334
+ return next;
335
+ });
336
+ };
337
+ const cycleSeverity = () => {
338
+ if (!currentRule) return;
339
+ setRuleStates((prev) => {
340
+ const next = new Map(prev);
341
+ const current = next.get(currentRule.id);
342
+ const newSeverity = current.severity === "error" ? "warn" : "error";
343
+ next.set(currentRule.id, { ...current, severity: newSeverity });
344
+ return next;
345
+ });
346
+ };
347
+ const handleSubmit = () => {
348
+ const configuredRules = [];
349
+ for (const rule of allRules) {
350
+ const state = ruleStates.get(rule.id);
351
+ if (state?.enabled) {
352
+ configuredRules.push({
353
+ rule,
354
+ severity: state.severity,
355
+ options: rule.defaultOptions
356
+ });
357
+ }
358
+ }
359
+ onSubmit(configuredRules);
360
+ };
361
+ useInput3((input, key) => {
362
+ if (viewMode === "docs") {
363
+ if (key.escape || key.return || input === "d" || input === "q") {
364
+ setViewMode("list");
365
+ }
366
+ return;
367
+ }
368
+ if (key.upArrow) {
369
+ setCursor((prev) => prev > 0 ? prev - 1 : allRules.length - 1);
370
+ } else if (key.downArrow) {
371
+ setCursor((prev) => prev < allRules.length - 1 ? prev + 1 : 0);
372
+ } else if (input === " ") {
373
+ toggleRule();
374
+ } else if (input === "s") {
375
+ cycleSeverity();
376
+ } else if (input === "d") {
377
+ setViewMode("docs");
378
+ } else if (key.return) {
379
+ handleSubmit();
380
+ } else if (key.escape || input === "q") {
381
+ onCancel?.();
382
+ exit();
383
+ } else if ((input === "b" || key.leftArrow) && onBack) {
384
+ onBack();
385
+ } else if (input === "a") {
386
+ setRuleStates((prev) => {
387
+ const next = new Map(prev);
388
+ for (const rule of allRules) {
389
+ const current = next.get(rule.id);
390
+ next.set(rule.id, { ...current, enabled: true });
391
+ }
392
+ return next;
393
+ });
394
+ } else if (input === "n") {
395
+ setRuleStates((prev) => {
396
+ const next = new Map(prev);
397
+ for (const rule of allRules) {
398
+ const current = next.get(rule.id);
399
+ next.set(rule.id, { ...current, enabled: false });
400
+ }
401
+ return next;
402
+ });
403
+ }
404
+ });
405
+ if (viewMode === "docs" && currentRule) {
406
+ const docLines = currentRule.docs.trim().split("\n").slice(0, 20);
407
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
408
+ /* @__PURE__ */ jsxs3(Box3, { marginBottom: 1, children: [
409
+ /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: currentRule.name }),
410
+ /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
411
+ " - ",
412
+ currentRule.description
413
+ ] })
414
+ ] }),
415
+ /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [
416
+ docLines.map((line, i) => /* @__PURE__ */ jsx4(Text4, { dimColor: line.startsWith("#"), children: line }, i)),
417
+ currentRule.docs.split("\n").length > 20 && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "... (truncated)" })
418
+ ] }),
419
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "Press any key to return to list" }) })
420
+ ] });
421
+ }
422
+ const enabledCount = Array.from(ruleStates.values()).filter((s) => s.enabled).length;
423
+ const errorCount = Array.from(ruleStates.entries()).filter(
424
+ ([id, s]) => s.enabled && s.severity === "error"
425
+ ).length;
426
+ const warnCount = enabledCount - errorCount;
427
+ let globalIndex = 0;
428
+ return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
429
+ /* @__PURE__ */ jsx4(Box3, { marginBottom: 1, children: /* @__PURE__ */ jsx4(Text4, { bold: true, children: "Configure ESLint Rules" }) }),
430
+ /* @__PURE__ */ jsx4(CategoryHeader, { name: "Static Rules", icon: "\u{1F4CB}" }),
431
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " Pattern-based, fast analysis" }),
432
+ staticRules.map((rule) => {
433
+ const itemIndex = globalIndex++;
434
+ const isCursor = itemIndex === cursor;
435
+ const state = ruleStates.get(rule.id);
436
+ return /* @__PURE__ */ jsxs3(Box3, { paddingLeft: 2, children: [
437
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u203A " : " " }),
438
+ /* @__PURE__ */ jsx4(Box3, { width: 3, children: /* @__PURE__ */ jsx4(Text4, { color: state.enabled ? "green" : void 0, dimColor: !state.enabled, children: state.enabled ? "\u2713" : "\u25CB" }) }),
439
+ /* @__PURE__ */ jsx4(Box3, { width: 30, children: /* @__PURE__ */ jsx4(
440
+ Text4,
441
+ {
442
+ color: isCursor ? "cyan" : void 0,
443
+ dimColor: !state.enabled,
444
+ bold: isCursor,
445
+ children: rule.name
446
+ }
447
+ ) }),
448
+ /* @__PURE__ */ jsx4(Box3, { width: 8, children: state.enabled ? /* @__PURE__ */ jsx4(SeverityBadge, { severity: state.severity }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "-" }) })
449
+ ] }, rule.id);
450
+ }),
451
+ /* @__PURE__ */ jsx4(CategoryHeader, { name: "Semantic Rules", icon: "\u{1F9E0}" }),
452
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " LLM-powered analysis (requires Ollama)" }),
453
+ semanticRules.map((rule) => {
454
+ const itemIndex = globalIndex++;
455
+ const isCursor = itemIndex === cursor;
456
+ const state = ruleStates.get(rule.id);
457
+ return /* @__PURE__ */ jsxs3(Box3, { paddingLeft: 2, children: [
458
+ /* @__PURE__ */ jsx4(Text4, { color: isCursor ? "cyan" : void 0, children: isCursor ? "\u203A " : " " }),
459
+ /* @__PURE__ */ jsx4(Box3, { width: 3, children: /* @__PURE__ */ jsx4(Text4, { color: state.enabled ? "green" : void 0, dimColor: !state.enabled, children: state.enabled ? "\u2713" : "\u25CB" }) }),
460
+ /* @__PURE__ */ jsx4(Box3, { width: 30, children: /* @__PURE__ */ jsx4(
461
+ Text4,
462
+ {
463
+ color: isCursor ? "cyan" : void 0,
464
+ dimColor: !state.enabled,
465
+ bold: isCursor,
466
+ children: rule.name
467
+ }
468
+ ) }),
469
+ /* @__PURE__ */ jsx4(Box3, { width: 8, children: state.enabled ? /* @__PURE__ */ jsx4(SeverityBadge, { severity: state.severity }) : /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "-" }) })
470
+ ] }, rule.id);
471
+ }),
472
+ currentRule && /* @__PURE__ */ jsx4(Box3, { marginTop: 1, paddingX: 2, children: /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: currentRule.description }) }),
473
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
474
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "\u2191\u2193" }),
475
+ " navigate",
476
+ " ",
477
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "space" }),
478
+ " toggle",
479
+ " ",
480
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "s" }),
481
+ " severity",
482
+ " ",
483
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "d" }),
484
+ " docs",
485
+ " ",
486
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "a" }),
487
+ " all",
488
+ " ",
489
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "n" }),
490
+ " none",
491
+ " ",
492
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "enter" }),
493
+ " confirm"
494
+ ] }) }),
495
+ /* @__PURE__ */ jsx4(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text4, { children: [
496
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: enabledCount }),
497
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " rules enabled (" }),
498
+ /* @__PURE__ */ jsx4(Text4, { color: "red", children: errorCount }),
499
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " errors, " }),
500
+ /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: warnCount }),
501
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " warnings)" })
502
+ ] }) })
503
+ ] });
504
+ }
505
+
506
+ // src/commands/install/installers/registry.ts
507
+ var installers = [];
508
+ function registerInstaller(installer) {
509
+ if (installers.some((i) => i.id === installer.id)) {
510
+ console.warn(`Installer "${installer.id}" is already registered`);
511
+ return;
512
+ }
513
+ installers.push(installer);
514
+ }
515
+ function getAllInstallers() {
516
+ return installers;
517
+ }
518
+
519
+ // src/commands/install/components/InstallApp.tsx
520
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
521
+ function buildConfigItemsForProject(project, selectedProject, selections) {
522
+ const items = [];
523
+ for (const selection of selections) {
524
+ const { installer, targets } = selection;
525
+ const relevantTargets = targets.filter((target) => {
526
+ if (installer.id === "next" || installer.id === "vite") {
527
+ return target.path === selectedProject.path;
528
+ }
529
+ if (installer.id === "eslint") {
530
+ return target.path === selectedProject.path;
531
+ }
532
+ return true;
533
+ });
534
+ if (relevantTargets.length === 0) continue;
535
+ const displayInfo = {
536
+ next: { category: "UI Analysis", icon: "\u{1F50D}" },
537
+ vite: { category: "UI Analysis", icon: "\u{1F50D}" },
538
+ eslint: { category: "ESLint Rules", icon: "\u{1F4CB}" },
539
+ genstyleguide: { category: "Cursor Integration", icon: "\u{1F4DD}" },
540
+ skill: { category: "Cursor Integration", icon: "\u26A1" }
541
+ };
542
+ const info = displayInfo[installer.id] || { category: "Other", icon: "\u2022" };
543
+ for (const target of relevantTargets) {
544
+ items.push({
545
+ id: `${installer.id}:${target.id}`,
546
+ label: installer.name,
547
+ hint: target.hint,
548
+ status: target.isInstalled ? "installed" : "not_installed",
549
+ category: info.category,
550
+ categoryIcon: info.icon
551
+ });
552
+ }
553
+ }
554
+ return items;
555
+ }
556
+ function buildGlobalConfigItems(selections) {
557
+ const items = [];
558
+ for (const selection of selections) {
559
+ const { installer, targets } = selection;
560
+ if (installer.id !== "genstyleguide" && installer.id !== "skill") {
561
+ continue;
562
+ }
563
+ const displayInfo = {
564
+ genstyleguide: { category: "Cursor Integration", icon: "\u{1F4DD}" },
565
+ skill: { category: "Cursor Integration", icon: "\u26A1" }
566
+ };
567
+ const info = displayInfo[installer.id] || { category: "Other", icon: "\u2022" };
568
+ for (const target of targets) {
569
+ items.push({
570
+ id: `${installer.id}:${target.id}`,
571
+ label: installer.name,
572
+ hint: target.hint,
573
+ status: target.isInstalled ? "installed" : "not_installed",
574
+ category: info.category,
575
+ categoryIcon: info.icon
576
+ });
577
+ }
578
+ }
579
+ return items;
580
+ }
581
+ function Header({ subtitle }) {
582
+ return /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", marginBottom: 1, children: /* @__PURE__ */ jsxs4(Box4, { children: [
583
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "\u25C6 UILint" }),
584
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " v0.5.0" }),
585
+ subtitle && /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
586
+ " \xB7 ",
587
+ subtitle
588
+ ] })
589
+ ] }) });
590
+ }
591
+ function FeatureConfig({
592
+ selectedProject,
593
+ configItems,
594
+ canGoBack,
595
+ onSubmit,
596
+ onBack,
597
+ onCancel
598
+ }) {
599
+ useInput4((input, key) => {
600
+ if ((input === "b" || key.leftArrow) && canGoBack) {
601
+ onBack();
602
+ }
603
+ });
604
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
605
+ selectedProject && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
606
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Project: " }),
607
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: selectedProject.name }),
608
+ /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
609
+ " (",
610
+ selectedProject.hint,
611
+ ")"
612
+ ] })
613
+ ] }),
614
+ /* @__PURE__ */ jsx5(
615
+ ConfigSelector,
616
+ {
617
+ items: configItems,
618
+ onSubmit,
619
+ onCancel
620
+ }
621
+ ),
622
+ canGoBack && /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsxs4(Text5, { dimColor: true, children: [
623
+ "Press ",
624
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "b" }),
625
+ " or ",
626
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "\u2190" }),
627
+ " to select a different project"
628
+ ] }) })
629
+ ] });
630
+ }
631
+ function InstallApp({
632
+ projectPromise,
633
+ onComplete,
634
+ onError
635
+ }) {
636
+ const { exit } = useApp4();
637
+ const [phase, setPhase] = useState5("scanning");
638
+ const [project, setProject] = useState5(null);
639
+ const [detectedProjects, setDetectedProjects] = useState5([]);
640
+ const [selectedProject, setSelectedProject] = useState5(null);
641
+ const [selections, setSelections] = useState5([]);
642
+ const [configItems, setConfigItems] = useState5([]);
643
+ const [selectedFeatureIds, setSelectedFeatureIds] = useState5([]);
644
+ const [error, setError] = useState5(null);
645
+ const isEslintSelected = selectedFeatureIds.some((id) => id.startsWith("eslint:"));
646
+ useEffect2(() => {
647
+ if (phase !== "scanning") return;
648
+ projectPromise.then((proj) => {
649
+ setProject(proj);
650
+ const projects = getDetectedProjects(proj);
651
+ setDetectedProjects(projects);
652
+ const installers2 = getAllInstallers();
653
+ const initialSelections = installers2.filter((installer) => installer.isApplicable(proj)).map((installer) => {
654
+ const targets = installer.getTargets(proj);
655
+ const nonInstalledTargets = targets.filter((t) => !t.isInstalled);
656
+ return {
657
+ installer,
658
+ targets,
659
+ selected: nonInstalledTargets.length > 0
660
+ };
661
+ });
662
+ setSelections(initialSelections);
663
+ if (projects.length === 1) {
664
+ const singleProject = projects[0];
665
+ setSelectedProject(singleProject);
666
+ const items = buildConfigItemsForProject(proj, singleProject, initialSelections);
667
+ setConfigItems(items);
668
+ setPhase("configure-features");
669
+ } else if (projects.length === 0) {
670
+ setPhase("configure-features");
671
+ const items = buildGlobalConfigItems(initialSelections);
672
+ setConfigItems(items);
673
+ } else {
674
+ setPhase("select-project");
675
+ }
676
+ }).catch((err) => {
677
+ setError(err);
678
+ setPhase("error");
679
+ onError?.(err);
680
+ });
681
+ }, [phase, projectPromise, onError]);
682
+ const handleProjectSelect = (selected) => {
683
+ setSelectedProject(selected);
684
+ if (project) {
685
+ const items = buildConfigItemsForProject(project, selected, selections);
686
+ setConfigItems(items);
687
+ }
688
+ setPhase("configure-features");
689
+ };
690
+ const handleBackToProject = () => {
691
+ if (detectedProjects.length > 1) {
692
+ setSelectedProject(null);
693
+ setPhase("select-project");
694
+ }
695
+ };
696
+ const handleFeatureSubmit = (selectedIds) => {
697
+ setSelectedFeatureIds(selectedIds);
698
+ const eslintSelected = selectedIds.some((id) => id.startsWith("eslint:"));
699
+ if (eslintSelected) {
700
+ setPhase("configure-eslint");
701
+ } else {
702
+ finishInstallation(selectedIds, void 0);
703
+ }
704
+ };
705
+ const handleRuleSubmit = (configuredRules) => {
706
+ finishInstallation(selectedFeatureIds, configuredRules);
707
+ };
708
+ const handleBackToFeatures = () => {
709
+ setPhase("configure-features");
710
+ };
711
+ const finishInstallation = (selectedIds, eslintRules) => {
712
+ const selectedSet = new Set(selectedIds);
713
+ const updatedSelections = selections.map((sel) => {
714
+ const selectedTargets = sel.targets.filter(
715
+ (t) => selectedSet.has(`${sel.installer.id}:${t.id}`)
716
+ );
717
+ return {
718
+ ...sel,
719
+ targets: selectedTargets,
720
+ selected: selectedTargets.length > 0
721
+ };
722
+ });
723
+ setSelections(updatedSelections);
724
+ onComplete(updatedSelections, eslintRules);
725
+ };
726
+ const handleCancel = () => {
727
+ exit();
728
+ };
729
+ if (phase === "scanning") {
730
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
731
+ /* @__PURE__ */ jsx5(Header, { subtitle: "Install" }),
732
+ /* @__PURE__ */ jsxs4(Box4, { children: [
733
+ /* @__PURE__ */ jsx5(Spinner, {}),
734
+ /* @__PURE__ */ jsx5(Text5, { children: " Scanning project..." })
735
+ ] })
736
+ ] });
737
+ }
738
+ if (phase === "error") {
739
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
740
+ /* @__PURE__ */ jsx5(Header, {}),
741
+ /* @__PURE__ */ jsxs4(Box4, { children: [
742
+ /* @__PURE__ */ jsx5(Text5, { color: "red", children: "\u2717 " }),
743
+ /* @__PURE__ */ jsx5(Text5, { color: "red", children: error?.message || "An unknown error occurred" })
744
+ ] })
745
+ ] });
746
+ }
747
+ if (phase === "select-project") {
748
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
749
+ /* @__PURE__ */ jsx5(Header, { subtitle: "Install" }),
750
+ /* @__PURE__ */ jsx5(
751
+ ProjectSelector,
752
+ {
753
+ projects: detectedProjects,
754
+ onSelect: handleProjectSelect,
755
+ onCancel: handleCancel
756
+ }
757
+ )
758
+ ] });
759
+ }
760
+ if (phase === "configure-features" && project && configItems.length > 0) {
761
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
762
+ /* @__PURE__ */ jsx5(Header, { subtitle: "Features" }),
763
+ /* @__PURE__ */ jsx5(
764
+ FeatureConfig,
765
+ {
766
+ selectedProject,
767
+ configItems,
768
+ canGoBack: detectedProjects.length > 1,
769
+ onSubmit: handleFeatureSubmit,
770
+ onBack: handleBackToProject,
771
+ onCancel: handleCancel
772
+ }
773
+ )
774
+ ] });
775
+ }
776
+ if (phase === "configure-eslint") {
777
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
778
+ /* @__PURE__ */ jsx5(Header, { subtitle: "ESLint Rules" }),
779
+ selectedProject && /* @__PURE__ */ jsxs4(Box4, { marginBottom: 1, children: [
780
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Project: " }),
781
+ /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: selectedProject.name })
782
+ ] }),
783
+ /* @__PURE__ */ jsx5(
784
+ RuleSelector,
785
+ {
786
+ onSubmit: handleRuleSubmit,
787
+ onBack: handleBackToFeatures,
788
+ onCancel: handleCancel
789
+ }
790
+ )
791
+ ] });
792
+ }
793
+ return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", children: [
794
+ /* @__PURE__ */ jsx5(Header, {}),
795
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "Loading..." })
796
+ ] });
797
+ }
798
+
799
+ // src/commands/install/analyze.ts
800
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
801
+ import { join as join5 } from "path";
802
+ import { findWorkspaceRoot as findWorkspaceRoot2 } from "uilint-core/node";
803
+
804
+ // src/utils/vite-detect.ts
805
+ import { existsSync, readdirSync, readFileSync } from "fs";
806
+ import { join } from "path";
807
+ var VITE_CONFIG_EXTS = [".ts", ".mjs", ".js", ".cjs"];
808
+ function findViteConfigFile(projectPath) {
809
+ for (const ext of VITE_CONFIG_EXTS) {
810
+ const rel = `vite.config${ext}`;
811
+ if (existsSync(join(projectPath, rel))) return rel;
812
+ }
813
+ return null;
814
+ }
815
+ function looksLikeReactPackage(projectPath) {
816
+ try {
817
+ const pkgPath = join(projectPath, "package.json");
818
+ if (!existsSync(pkgPath)) return false;
819
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
820
+ const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
821
+ return "react" in deps || "react-dom" in deps;
822
+ } catch {
823
+ return false;
824
+ }
825
+ }
826
+ function fileExists(projectPath, relPath) {
827
+ return existsSync(join(projectPath, relPath));
828
+ }
829
+ function detectViteReact(projectPath) {
830
+ const configFile = findViteConfigFile(projectPath);
831
+ if (!configFile) return null;
832
+ if (!looksLikeReactPackage(projectPath)) return null;
833
+ const entryRoot = "src";
834
+ const candidates = [];
835
+ const entryCandidates = [
836
+ join(entryRoot, "main.tsx"),
837
+ join(entryRoot, "main.jsx"),
838
+ join(entryRoot, "main.ts"),
839
+ join(entryRoot, "main.js")
840
+ ];
841
+ for (const rel of entryCandidates) {
842
+ if (fileExists(projectPath, rel)) candidates.push(rel);
843
+ }
844
+ const fallbackCandidates = [
845
+ join(entryRoot, "App.tsx"),
846
+ join(entryRoot, "App.jsx")
847
+ ];
848
+ for (const rel of fallbackCandidates) {
849
+ if (!candidates.includes(rel) && fileExists(projectPath, rel)) {
850
+ candidates.push(rel);
851
+ }
852
+ }
853
+ return {
854
+ configFile,
855
+ configFileAbs: join(projectPath, configFile),
856
+ entryRoot,
857
+ candidates
858
+ };
859
+ }
860
+ var DEFAULT_IGNORE_DIRS = /* @__PURE__ */ new Set([
861
+ "node_modules",
862
+ ".git",
863
+ ".next",
864
+ "dist",
865
+ "build",
866
+ "out",
867
+ ".turbo",
868
+ ".vercel",
869
+ ".cursor",
870
+ "coverage",
871
+ ".uilint"
872
+ ]);
873
+ function findViteReactProjects(rootDir, options) {
874
+ const maxDepth = options?.maxDepth ?? 4;
875
+ const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS;
876
+ const results = [];
877
+ const visited = /* @__PURE__ */ new Set();
878
+ function walk(dir, depth) {
879
+ if (depth > maxDepth) return;
880
+ if (visited.has(dir)) return;
881
+ visited.add(dir);
882
+ const detection = detectViteReact(dir);
883
+ if (detection) {
884
+ results.push({ projectPath: dir, detection });
885
+ return;
886
+ }
887
+ let entries = [];
888
+ try {
889
+ entries = readdirSync(dir, { withFileTypes: true }).map((d) => ({
890
+ name: d.name,
891
+ isDirectory: d.isDirectory()
892
+ }));
893
+ } catch {
894
+ return;
895
+ }
896
+ for (const ent of entries) {
897
+ if (!ent.isDirectory) continue;
898
+ if (ignoreDirs.has(ent.name)) continue;
899
+ if (ent.name.startsWith(".") && ent.name !== ".") continue;
900
+ walk(join(dir, ent.name), depth + 1);
901
+ }
902
+ }
903
+ walk(rootDir, 0);
904
+ return results;
905
+ }
906
+
907
+ // src/utils/package-detect.ts
908
+ import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
909
+ import { join as join2, relative } from "path";
910
+ var DEFAULT_IGNORE_DIRS2 = /* @__PURE__ */ new Set([
911
+ "node_modules",
912
+ ".git",
913
+ ".next",
914
+ "dist",
915
+ "build",
916
+ "out",
917
+ ".turbo",
918
+ ".vercel",
919
+ ".cursor",
920
+ "coverage",
921
+ ".uilint",
922
+ ".pnpm"
923
+ ]);
924
+ var ESLINT_CONFIG_FILES = [
925
+ "eslint.config.js",
926
+ "eslint.config.ts",
927
+ "eslint.config.mjs",
928
+ "eslint.config.cjs",
929
+ ".eslintrc.js",
930
+ ".eslintrc.cjs",
931
+ ".eslintrc.json",
932
+ ".eslintrc.yml",
933
+ ".eslintrc.yaml",
934
+ ".eslintrc"
935
+ ];
936
+ var FRONTEND_INDICATORS = [
937
+ "react",
938
+ "react-dom",
939
+ "next",
940
+ "vue",
941
+ "svelte",
942
+ "@angular/core",
943
+ "solid-js",
944
+ "preact"
945
+ ];
946
+ function isFrontendPackage(pkgJson) {
947
+ const deps = {
948
+ ...pkgJson.dependencies,
949
+ ...pkgJson.devDependencies
950
+ };
951
+ return FRONTEND_INDICATORS.some((pkg) => pkg in deps);
952
+ }
953
+ function isTypeScriptPackage(dir, pkgJson) {
954
+ if (existsSync2(join2(dir, "tsconfig.json"))) {
955
+ return true;
956
+ }
957
+ const deps = {
958
+ ...pkgJson.dependencies,
959
+ ...pkgJson.devDependencies
960
+ };
961
+ if ("typescript" in deps) {
962
+ return true;
963
+ }
964
+ for (const configFile of ESLINT_CONFIG_FILES) {
965
+ if (configFile.endsWith(".ts") && existsSync2(join2(dir, configFile))) {
966
+ return true;
967
+ }
968
+ }
969
+ return false;
970
+ }
971
+ function hasEslintConfig(dir) {
972
+ for (const file of ESLINT_CONFIG_FILES) {
973
+ if (existsSync2(join2(dir, file))) {
974
+ return true;
975
+ }
976
+ }
977
+ try {
978
+ const pkgPath = join2(dir, "package.json");
979
+ if (existsSync2(pkgPath)) {
980
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
981
+ if (pkg.eslintConfig) return true;
982
+ }
983
+ } catch {
984
+ }
985
+ return false;
986
+ }
987
+ function findPackages(rootDir, options) {
988
+ const maxDepth = options?.maxDepth ?? 5;
989
+ const ignoreDirs = options?.ignoreDirs ?? DEFAULT_IGNORE_DIRS2;
990
+ const results = [];
991
+ const visited = /* @__PURE__ */ new Set();
992
+ function processPackage(dir, isRoot) {
993
+ const pkgPath = join2(dir, "package.json");
994
+ if (!existsSync2(pkgPath)) return null;
995
+ try {
996
+ const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
997
+ const name = pkg.name || relative(rootDir, dir) || ".";
998
+ return {
999
+ path: dir,
1000
+ displayPath: relative(rootDir, dir) || ".",
1001
+ name,
1002
+ hasEslintConfig: hasEslintConfig(dir),
1003
+ isFrontend: isFrontendPackage(pkg),
1004
+ isRoot,
1005
+ isTypeScript: isTypeScriptPackage(dir, pkg)
1006
+ };
1007
+ } catch {
1008
+ return null;
1009
+ }
1010
+ }
1011
+ function walk(dir, depth) {
1012
+ if (depth > maxDepth) return;
1013
+ if (visited.has(dir)) return;
1014
+ visited.add(dir);
1015
+ const pkg = processPackage(dir, depth === 0);
1016
+ if (pkg) {
1017
+ results.push(pkg);
1018
+ }
1019
+ let entries = [];
1020
+ try {
1021
+ entries = readdirSync2(dir, { withFileTypes: true }).map((d) => ({
1022
+ name: d.name,
1023
+ isDirectory: d.isDirectory()
1024
+ }));
1025
+ } catch {
1026
+ return;
1027
+ }
1028
+ for (const ent of entries) {
1029
+ if (!ent.isDirectory) continue;
1030
+ if (ignoreDirs.has(ent.name)) continue;
1031
+ if (ent.name.startsWith(".")) continue;
1032
+ walk(join2(dir, ent.name), depth + 1);
1033
+ }
1034
+ }
1035
+ walk(rootDir, 0);
1036
+ return results.sort((a, b) => {
1037
+ if (a.isRoot && !b.isRoot) return -1;
1038
+ if (!a.isRoot && b.isRoot) return 1;
1039
+ if (a.isFrontend && !b.isFrontend) return -1;
1040
+ if (!a.isFrontend && b.isFrontend) return 1;
1041
+ return a.displayPath.localeCompare(b.displayPath);
1042
+ });
1043
+ }
1044
+
1045
+ // src/utils/package-manager.ts
1046
+ import { existsSync as existsSync3 } from "fs";
1047
+ import { spawn } from "child_process";
1048
+ import { dirname, join as join3 } from "path";
1049
+ function detectPackageManager(projectPath) {
1050
+ let dir = projectPath;
1051
+ for (; ; ) {
1052
+ if (existsSync3(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
1053
+ if (existsSync3(join3(dir, "pnpm-workspace.yaml"))) return "pnpm";
1054
+ if (existsSync3(join3(dir, "yarn.lock"))) return "yarn";
1055
+ if (existsSync3(join3(dir, "bun.lockb"))) return "bun";
1056
+ if (existsSync3(join3(dir, "bun.lock"))) return "bun";
1057
+ if (existsSync3(join3(dir, "package-lock.json"))) return "npm";
1058
+ const parent = dirname(dir);
1059
+ if (parent === dir) break;
1060
+ dir = parent;
1061
+ }
1062
+ return "npm";
1063
+ }
1064
+ function spawnAsync(command, args, cwd) {
1065
+ return new Promise((resolve, reject) => {
1066
+ const child = spawn(command, args, {
1067
+ cwd,
1068
+ stdio: "inherit",
1069
+ shell: process.platform === "win32"
1070
+ });
1071
+ child.on("error", reject);
1072
+ child.on("close", (code) => {
1073
+ if (code === 0) resolve();
1074
+ else
1075
+ reject(new Error(`${command} ${args.join(" ")} exited with ${code}`));
1076
+ });
1077
+ });
1078
+ }
1079
+ async function installDependencies(pm, projectPath, packages) {
1080
+ if (!packages.length) return;
1081
+ switch (pm) {
1082
+ case "pnpm":
1083
+ await spawnAsync("pnpm", ["add", ...packages], projectPath);
1084
+ return;
1085
+ case "yarn":
1086
+ await spawnAsync("yarn", ["add", ...packages], projectPath);
1087
+ return;
1088
+ case "bun":
1089
+ await spawnAsync("bun", ["add", ...packages], projectPath);
1090
+ return;
1091
+ case "npm":
1092
+ default:
1093
+ await spawnAsync("npm", ["install", "--save", ...packages], projectPath);
1094
+ return;
1095
+ }
1096
+ }
1097
+
1098
+ // src/utils/eslint-config-inject.ts
1099
+ import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync } from "fs";
1100
+ import { join as join4, relative as relative2, dirname as dirname2 } from "path";
1101
+ import { parseExpression, parseModule, generateCode } from "magicast";
1102
+ import { findWorkspaceRoot } from "uilint-core/node";
1103
+ var CONFIG_EXTENSIONS = [".ts", ".mjs", ".js", ".cjs"];
1104
+ function findEslintConfigFile(projectPath) {
1105
+ for (const ext of CONFIG_EXTENSIONS) {
1106
+ const configPath = join4(projectPath, `eslint.config${ext}`);
1107
+ if (existsSync4(configPath)) {
1108
+ return configPath;
1109
+ }
1110
+ }
1111
+ return null;
1112
+ }
1113
+ function getEslintConfigFilename(configPath) {
1114
+ const parts = configPath.split("/");
1115
+ return parts[parts.length - 1] || "eslint.config.mjs";
1116
+ }
1117
+ function isIdentifier(node, name) {
1118
+ return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
1119
+ }
1120
+ function isStringLiteral(node) {
1121
+ return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
1122
+ }
1123
+ function getObjectPropertyValue(obj, keyName) {
1124
+ if (!obj || obj.type !== "ObjectExpression") return null;
1125
+ for (const prop of obj.properties ?? []) {
1126
+ if (!prop) continue;
1127
+ if (prop.type === "ObjectProperty" || prop.type === "Property") {
1128
+ const key = prop.key;
1129
+ const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral(key) && key.value === keyName;
1130
+ if (keyMatch) return prop.value;
1131
+ }
1132
+ }
1133
+ return null;
1134
+ }
1135
+ function hasSpreadProperties(obj) {
1136
+ if (!obj || obj.type !== "ObjectExpression") return false;
1137
+ return (obj.properties ?? []).some(
1138
+ (p) => p && (p.type === "SpreadElement" || p.type === "SpreadProperty")
1139
+ );
1140
+ }
1141
+ var IGNORED_AST_KEYS = /* @__PURE__ */ new Set([
1142
+ "loc",
1143
+ "start",
1144
+ "end",
1145
+ "extra",
1146
+ "leadingComments",
1147
+ "trailingComments",
1148
+ "innerComments"
1149
+ ]);
1150
+ function normalizeAstForCompare(node) {
1151
+ if (node === null) return null;
1152
+ if (node === void 0) return void 0;
1153
+ if (typeof node !== "object") return node;
1154
+ if (Array.isArray(node)) return node.map(normalizeAstForCompare);
1155
+ const out = {};
1156
+ const keys = Object.keys(node).filter((k) => !IGNORED_AST_KEYS.has(k)).sort();
1157
+ for (const k of keys) {
1158
+ if (k.startsWith("$")) continue;
1159
+ out[k] = normalizeAstForCompare(node[k]);
1160
+ }
1161
+ return out;
1162
+ }
1163
+ function astEquivalent(a, b) {
1164
+ try {
1165
+ return JSON.stringify(normalizeAstForCompare(a)) === JSON.stringify(normalizeAstForCompare(b));
1166
+ } catch {
1167
+ return false;
1168
+ }
1169
+ }
1170
+ function collectUilintRuleIdsFromRulesObject(rulesObj) {
1171
+ const ids = /* @__PURE__ */ new Set();
1172
+ if (!rulesObj || rulesObj.type !== "ObjectExpression") return ids;
1173
+ for (const prop of rulesObj.properties ?? []) {
1174
+ if (!prop) continue;
1175
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
1176
+ const key = prop.key;
1177
+ if (!isStringLiteral(key)) continue;
1178
+ const val = key.value;
1179
+ if (typeof val !== "string") continue;
1180
+ if (val.startsWith("uilint/")) {
1181
+ ids.add(val.slice("uilint/".length));
1182
+ }
1183
+ }
1184
+ return ids;
1185
+ }
1186
+ function findExportedConfigArrayExpression(mod) {
1187
+ function unwrapExpression2(expr) {
1188
+ let e = expr;
1189
+ while (e) {
1190
+ if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
1191
+ e = e.expression;
1192
+ continue;
1193
+ }
1194
+ if (e.type === "TSSatisfiesExpression") {
1195
+ e = e.expression;
1196
+ continue;
1197
+ }
1198
+ if (e.type === "ParenthesizedExpression") {
1199
+ e = e.expression;
1200
+ continue;
1201
+ }
1202
+ break;
1203
+ }
1204
+ return e;
1205
+ }
1206
+ function resolveTopLevelIdentifierToArrayExpr(program2, name) {
1207
+ if (!program2 || program2.type !== "Program") return null;
1208
+ for (const stmt of program2.body ?? []) {
1209
+ if (stmt?.type !== "VariableDeclaration") continue;
1210
+ for (const decl of stmt.declarations ?? []) {
1211
+ const id = decl?.id;
1212
+ if (!isIdentifier(id, name)) continue;
1213
+ const init = unwrapExpression2(decl?.init);
1214
+ if (!init) return null;
1215
+ if (init.type === "ArrayExpression") return init;
1216
+ if (init.type === "CallExpression" && isIdentifier(init.callee, "defineConfig") && unwrapExpression2(init.arguments?.[0])?.type === "ArrayExpression") {
1217
+ return unwrapExpression2(init.arguments?.[0]);
1218
+ }
1219
+ return null;
1220
+ }
1221
+ }
1222
+ return null;
1223
+ }
1224
+ const program = mod?.$ast;
1225
+ if (program && program.type === "Program") {
1226
+ for (const stmt of program.body ?? []) {
1227
+ if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
1228
+ const decl = unwrapExpression2(stmt.declaration);
1229
+ if (!decl) break;
1230
+ if (decl.type === "ArrayExpression") {
1231
+ return { kind: "esm", arrayExpr: decl, program };
1232
+ }
1233
+ if (decl.type === "CallExpression" && isIdentifier(decl.callee, "defineConfig") && unwrapExpression2(decl.arguments?.[0])?.type === "ArrayExpression") {
1234
+ return {
1235
+ kind: "esm",
1236
+ arrayExpr: unwrapExpression2(decl.arguments?.[0]),
1237
+ program
1238
+ };
1239
+ }
1240
+ if (decl.type === "Identifier" && typeof decl.name === "string") {
1241
+ const resolved = resolveTopLevelIdentifierToArrayExpr(
1242
+ program,
1243
+ decl.name
1244
+ );
1245
+ if (resolved) return { kind: "esm", arrayExpr: resolved, program };
1246
+ }
1247
+ break;
1248
+ }
1249
+ }
1250
+ if (!program || program.type !== "Program") return null;
1251
+ for (const stmt of program.body ?? []) {
1252
+ if (!stmt || stmt.type !== "ExpressionStatement") continue;
1253
+ const expr = stmt.expression;
1254
+ if (!expr || expr.type !== "AssignmentExpression") continue;
1255
+ const left = expr.left;
1256
+ const right = expr.right;
1257
+ const isModuleExports = left?.type === "MemberExpression" && isIdentifier(left.object, "module") && isIdentifier(left.property, "exports");
1258
+ if (!isModuleExports) continue;
1259
+ if (right?.type === "ArrayExpression") {
1260
+ return { kind: "cjs", arrayExpr: right, program };
1261
+ }
1262
+ if (right?.type === "CallExpression" && isIdentifier(right.callee, "defineConfig") && right.arguments?.[0]?.type === "ArrayExpression") {
1263
+ return { kind: "cjs", arrayExpr: right.arguments[0], program };
1264
+ }
1265
+ if (right?.type === "Identifier" && typeof right.name === "string") {
1266
+ const resolved = resolveTopLevelIdentifierToArrayExpr(
1267
+ program,
1268
+ right.name
1269
+ );
1270
+ if (resolved) return { kind: "cjs", arrayExpr: resolved, program };
1271
+ }
1272
+ }
1273
+ return null;
1274
+ }
1275
+ function collectConfiguredUilintRuleIdsFromConfigArray(arrayExpr) {
1276
+ const ids = /* @__PURE__ */ new Set();
1277
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return ids;
1278
+ for (const el of arrayExpr.elements ?? []) {
1279
+ if (!el || el.type !== "ObjectExpression") continue;
1280
+ const rules = getObjectPropertyValue(el, "rules");
1281
+ for (const id of collectUilintRuleIdsFromRulesObject(rules)) ids.add(id);
1282
+ }
1283
+ return ids;
1284
+ }
1285
+ function findExistingUilintRulesObject(arrayExpr) {
1286
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") {
1287
+ return { configObj: null, rulesObj: null, safeToMutate: false };
1288
+ }
1289
+ for (const el of arrayExpr.elements ?? []) {
1290
+ if (!el || el.type !== "ObjectExpression") continue;
1291
+ const plugins = getObjectPropertyValue(el, "plugins");
1292
+ const rules = getObjectPropertyValue(el, "rules");
1293
+ const hasUilintPlugin = plugins?.type === "ObjectExpression" && getObjectPropertyValue(plugins, "uilint") !== null;
1294
+ const uilintIds = collectUilintRuleIdsFromRulesObject(rules);
1295
+ const hasUilintRules = uilintIds.size > 0;
1296
+ if (!hasUilintPlugin && !hasUilintRules) continue;
1297
+ const safe = rules?.type === "ObjectExpression" && !hasSpreadProperties(rules);
1298
+ return { configObj: el, rulesObj: rules, safeToMutate: safe };
1299
+ }
1300
+ return { configObj: null, rulesObj: null, safeToMutate: false };
1301
+ }
1302
+ function collectTopLevelBindings(program) {
1303
+ const names = /* @__PURE__ */ new Set();
1304
+ if (!program || program.type !== "Program") return names;
1305
+ for (const stmt of program.body ?? []) {
1306
+ if (stmt?.type === "VariableDeclaration") {
1307
+ for (const decl of stmt.declarations ?? []) {
1308
+ const id = decl?.id;
1309
+ if (id?.type === "Identifier" && typeof id.name === "string") {
1310
+ names.add(id.name);
1311
+ }
1312
+ }
1313
+ } else if (stmt?.type === "FunctionDeclaration") {
1314
+ if (stmt.id?.type === "Identifier" && typeof stmt.id.name === "string") {
1315
+ names.add(stmt.id.name);
1316
+ }
1317
+ }
1318
+ }
1319
+ return names;
1320
+ }
1321
+ function chooseUniqueIdentifier(base, used) {
1322
+ if (!used.has(base)) return base;
1323
+ let i = 2;
1324
+ while (used.has(`${base}${i}`)) i++;
1325
+ return `${base}${i}`;
1326
+ }
1327
+ function addLocalRuleImportsAst(mod, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
1328
+ const importNames = /* @__PURE__ */ new Map();
1329
+ let changed = false;
1330
+ const configDir = dirname2(configPath);
1331
+ const rulesDir = join4(rulesRoot, ".uilint", "rules");
1332
+ const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
1333
+ const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
1334
+ const used = collectTopLevelBindings(mod.$ast);
1335
+ for (const rule of selectedRules) {
1336
+ const importName = chooseUniqueIdentifier(
1337
+ `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
1338
+ used
1339
+ );
1340
+ importNames.set(rule.id, importName);
1341
+ used.add(importName);
1342
+ const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
1343
+ mod.imports.$add({
1344
+ imported: "default",
1345
+ local: importName,
1346
+ from: rulePath
1347
+ });
1348
+ changed = true;
1349
+ }
1350
+ return { importNames, changed };
1351
+ }
1352
+ function addLocalRuleRequiresAst(program, selectedRules, configPath, rulesRoot, fileExtension = ".js") {
1353
+ const importNames = /* @__PURE__ */ new Map();
1354
+ let changed = false;
1355
+ if (!program || program.type !== "Program") {
1356
+ return { importNames, changed };
1357
+ }
1358
+ const configDir = dirname2(configPath);
1359
+ const rulesDir = join4(rulesRoot, ".uilint", "rules");
1360
+ const relativeRulesPath = relative2(configDir, rulesDir).replace(/\\/g, "/");
1361
+ const normalizedRulesPath = relativeRulesPath.startsWith("./") || relativeRulesPath.startsWith("../") ? relativeRulesPath : `./${relativeRulesPath}`;
1362
+ const used = collectTopLevelBindings(program);
1363
+ for (const rule of selectedRules) {
1364
+ const importName = chooseUniqueIdentifier(
1365
+ `${rule.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toUpperCase())}Rule`,
1366
+ used
1367
+ );
1368
+ importNames.set(rule.id, importName);
1369
+ used.add(importName);
1370
+ const rulePath = `${normalizedRulesPath}/${rule.id}${fileExtension}`;
1371
+ const stmtMod = parseModule(
1372
+ `const ${importName} = require("${rulePath}");`
1373
+ );
1374
+ const stmt = stmtMod.$ast.body?.[0];
1375
+ if (stmt) {
1376
+ let insertAt = 0;
1377
+ const first = program.body?.[0];
1378
+ if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
1379
+ insertAt = 1;
1380
+ }
1381
+ program.body.splice(insertAt, 0, stmt);
1382
+ changed = true;
1383
+ }
1384
+ }
1385
+ return { importNames, changed };
1386
+ }
1387
+ function appendUilintConfigBlockToArray(arrayExpr, selectedRules, ruleImportNames) {
1388
+ const pluginRulesCode = Array.from(ruleImportNames.entries()).map(([ruleId, importName]) => ` "${ruleId}": ${importName},`).join("\n");
1389
+ const rulesPropsCode = selectedRules.map((r) => {
1390
+ const ruleKey = `uilint/${r.id}`;
1391
+ const valueCode = r.defaultOptions && r.defaultOptions.length > 0 ? `["${r.defaultSeverity}", ...${JSON.stringify(
1392
+ r.defaultOptions,
1393
+ null,
1394
+ 2
1395
+ )}]` : `"${r.defaultSeverity}"`;
1396
+ return ` "${ruleKey}": ${valueCode},`;
1397
+ }).join("\n");
1398
+ const blockCode = `{
1399
+ files: [
1400
+ "src/**/*.{js,jsx,ts,tsx}",
1401
+ "app/**/*.{js,jsx,ts,tsx}",
1402
+ "pages/**/*.{js,jsx,ts,tsx}",
1403
+ ],
1404
+ plugins: {
1405
+ uilint: {
1406
+ rules: {
1407
+ ${pluginRulesCode}
1408
+ },
1409
+ },
1410
+ },
1411
+ rules: {
1412
+ ${rulesPropsCode}
1413
+ },
1414
+ }`;
1415
+ const objExpr = parseExpression(blockCode).$ast;
1416
+ arrayExpr.elements.push(objExpr);
1417
+ }
1418
+ function getUilintEslintConfigInfoFromSourceAst(source) {
1419
+ try {
1420
+ const mod = parseModule(source);
1421
+ const found = findExportedConfigArrayExpression(mod);
1422
+ if (!found) {
1423
+ return {
1424
+ error: "Could not locate an exported ESLint flat config array (expected `export default [...]`, `export default defineConfig([...])`, `module.exports = [...]`, or `module.exports = defineConfig([...])`)."
1425
+ };
1426
+ }
1427
+ const configuredRuleIds = collectConfiguredUilintRuleIdsFromConfigArray(
1428
+ found.arrayExpr
1429
+ );
1430
+ const existingUilint = findExistingUilintRulesObject(found.arrayExpr);
1431
+ const configured = configuredRuleIds.size > 0 || existingUilint.configObj !== null;
1432
+ return {
1433
+ info: { configuredRuleIds, configured },
1434
+ mod,
1435
+ arrayExpr: found.arrayExpr,
1436
+ kind: found.kind
1437
+ };
1438
+ } catch {
1439
+ return {
1440
+ error: "Unable to parse ESLint config as JavaScript. Please update it manually or simplify the config so it can be safely auto-modified."
1441
+ };
1442
+ }
1443
+ }
1444
+ function getUilintEslintConfigInfoFromSource(source) {
1445
+ const ast = getUilintEslintConfigInfoFromSourceAst(source);
1446
+ if ("error" in ast) {
1447
+ const configuredRuleIds = extractConfiguredUilintRuleIds(source);
1448
+ return {
1449
+ configuredRuleIds,
1450
+ configured: configuredRuleIds.size > 0
1451
+ };
1452
+ }
1453
+ return ast.info;
1454
+ }
1455
+ function extractConfiguredUilintRuleIds(source) {
1456
+ const ids = /* @__PURE__ */ new Set();
1457
+ const re = /["']uilint\/([^"']+)["']\s*:/g;
1458
+ for (const m of source.matchAll(re)) {
1459
+ if (m[1]) ids.add(m[1]);
1460
+ }
1461
+ return ids;
1462
+ }
1463
+ function getMissingSelectedRules(selectedRules, configuredIds) {
1464
+ return selectedRules.filter((r) => !configuredIds.has(r.id));
1465
+ }
1466
+ function buildDesiredRuleValueExpression(rule) {
1467
+ if (rule.defaultOptions && rule.defaultOptions.length > 0) {
1468
+ return `["${rule.defaultSeverity}", ...${JSON.stringify(
1469
+ rule.defaultOptions,
1470
+ null,
1471
+ 2
1472
+ )}]`;
1473
+ }
1474
+ return `"${rule.defaultSeverity}"`;
1475
+ }
1476
+ function collectUilintRuleValueNodesFromConfigArray(arrayExpr) {
1477
+ const out = /* @__PURE__ */ new Map();
1478
+ if (!arrayExpr || arrayExpr.type !== "ArrayExpression") return out;
1479
+ for (const el of arrayExpr.elements ?? []) {
1480
+ if (!el || el.type !== "ObjectExpression") continue;
1481
+ const rules = getObjectPropertyValue(el, "rules");
1482
+ if (!rules || rules.type !== "ObjectExpression") continue;
1483
+ for (const prop of rules.properties ?? []) {
1484
+ if (!prop) continue;
1485
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
1486
+ const key = prop.key;
1487
+ if (!isStringLiteral(key)) continue;
1488
+ const k = key.value;
1489
+ if (typeof k !== "string" || !k.startsWith("uilint/")) continue;
1490
+ const id = k.slice("uilint/".length);
1491
+ if (!out.has(id)) out.set(id, prop.value);
1492
+ }
1493
+ }
1494
+ return out;
1495
+ }
1496
+ function getRulesNeedingUpdate(selectedRules, configuredIds, arrayExpr) {
1497
+ const existingVals = collectUilintRuleValueNodesFromConfigArray(arrayExpr);
1498
+ return selectedRules.filter((r) => {
1499
+ if (!configuredIds.has(r.id)) return false;
1500
+ const existing = existingVals.get(r.id);
1501
+ if (!existing) return true;
1502
+ const desiredExpr = buildDesiredRuleValueExpression(r);
1503
+ const desiredAst = parseExpression(desiredExpr).$ast;
1504
+ return !astEquivalent(existing, desiredAst);
1505
+ });
1506
+ }
1507
+ async function installEslintPlugin(opts) {
1508
+ const configPath = findEslintConfigFile(opts.projectPath);
1509
+ if (!configPath) {
1510
+ return {
1511
+ configFile: null,
1512
+ modified: false,
1513
+ missingRuleIds: [],
1514
+ configured: false
1515
+ };
1516
+ }
1517
+ const configFilename = getEslintConfigFilename(configPath);
1518
+ const original = readFileSync3(configPath, "utf-8");
1519
+ const isCommonJS = configPath.endsWith(".cjs");
1520
+ const ast = getUilintEslintConfigInfoFromSourceAst(original);
1521
+ if ("error" in ast) {
1522
+ return {
1523
+ configFile: configFilename,
1524
+ modified: false,
1525
+ missingRuleIds: [],
1526
+ configured: false,
1527
+ error: ast.error
1528
+ };
1529
+ }
1530
+ const { info, mod, arrayExpr, kind } = ast;
1531
+ const configuredIds = info.configuredRuleIds;
1532
+ const missingRules = getMissingSelectedRules(
1533
+ opts.selectedRules,
1534
+ configuredIds
1535
+ );
1536
+ const rulesToUpdate = getRulesNeedingUpdate(
1537
+ opts.selectedRules,
1538
+ configuredIds,
1539
+ arrayExpr
1540
+ );
1541
+ let rulesToApply = [];
1542
+ if (!info.configured) {
1543
+ rulesToApply = opts.selectedRules;
1544
+ } else {
1545
+ rulesToApply = [...missingRules, ...rulesToUpdate];
1546
+ if (missingRules.length > 0 && !opts.force) {
1547
+ const ok = await opts.confirmAddMissingRules?.(
1548
+ configFilename,
1549
+ missingRules
1550
+ );
1551
+ if (!ok) {
1552
+ return {
1553
+ configFile: configFilename,
1554
+ modified: false,
1555
+ missingRuleIds: missingRules.map((r) => r.id),
1556
+ configured: true
1557
+ };
1558
+ }
1559
+ }
1560
+ }
1561
+ if (rulesToApply.length === 0) {
1562
+ return {
1563
+ configFile: configFilename,
1564
+ modified: false,
1565
+ missingRuleIds: missingRules.map((r) => r.id),
1566
+ configured: info.configured
1567
+ };
1568
+ }
1569
+ let modifiedAst = false;
1570
+ const localRulesDir = join4(opts.projectPath, ".uilint", "rules");
1571
+ const workspaceRoot = findWorkspaceRoot(opts.projectPath);
1572
+ const workspaceRulesDir = join4(workspaceRoot, ".uilint", "rules");
1573
+ const rulesRoot = existsSync4(localRulesDir) ? opts.projectPath : workspaceRoot;
1574
+ let fileExtension = ".js";
1575
+ if (rulesToApply.length > 0) {
1576
+ const firstRulePath = join4(
1577
+ rulesRoot,
1578
+ ".uilint",
1579
+ "rules",
1580
+ `${rulesToApply[0].id}.ts`
1581
+ );
1582
+ if (existsSync4(firstRulePath)) {
1583
+ fileExtension = ".ts";
1584
+ }
1585
+ }
1586
+ let ruleImportNames;
1587
+ if (kind === "esm") {
1588
+ const result = addLocalRuleImportsAst(
1589
+ mod,
1590
+ rulesToApply,
1591
+ configPath,
1592
+ rulesRoot,
1593
+ fileExtension
1594
+ );
1595
+ ruleImportNames = result.importNames;
1596
+ if (result.changed) modifiedAst = true;
1597
+ } else {
1598
+ const result = addLocalRuleRequiresAst(
1599
+ mod.$ast,
1600
+ rulesToApply,
1601
+ configPath,
1602
+ rulesRoot,
1603
+ fileExtension
1604
+ );
1605
+ ruleImportNames = result.importNames;
1606
+ if (result.changed) modifiedAst = true;
1607
+ }
1608
+ if (ruleImportNames && ruleImportNames.size > 0) {
1609
+ appendUilintConfigBlockToArray(arrayExpr, rulesToApply, ruleImportNames);
1610
+ modifiedAst = true;
1611
+ }
1612
+ if (!info.configured) {
1613
+ if (kind === "esm") {
1614
+ mod.imports.$add({
1615
+ imported: "createRule",
1616
+ local: "createRule",
1617
+ from: "uilint-eslint"
1618
+ });
1619
+ modifiedAst = true;
1620
+ } else {
1621
+ const stmtMod = parseModule(
1622
+ `const { createRule } = require("uilint-eslint");`
1623
+ );
1624
+ const stmt = stmtMod.$ast.body?.[0];
1625
+ if (stmt) {
1626
+ let insertAt = 0;
1627
+ const first = mod.$ast.body?.[0];
1628
+ if (first?.type === "ExpressionStatement" && first.expression?.type === "StringLiteral" && first.expression.value === "use strict") {
1629
+ insertAt = 1;
1630
+ }
1631
+ mod.$ast.body.splice(insertAt, 0, stmt);
1632
+ modifiedAst = true;
1633
+ }
1634
+ }
1635
+ }
1636
+ const updated = modifiedAst ? generateCode(mod).code : original;
1637
+ if (updated !== original) {
1638
+ writeFileSync(configPath, updated, "utf-8");
1639
+ return {
1640
+ configFile: configFilename,
1641
+ modified: true,
1642
+ missingRuleIds: missingRules.map((r) => r.id),
1643
+ configured: getUilintEslintConfigInfoFromSource(updated).configured
1644
+ };
1645
+ }
1646
+ return {
1647
+ configFile: configFilename,
1648
+ modified: false,
1649
+ missingRuleIds: missingRules.map((r) => r.id),
1650
+ configured: getUilintEslintConfigInfoFromSource(updated).configured
1651
+ };
1652
+ }
1653
+
1654
+ // src/commands/install/analyze.ts
1655
+ async function analyze(projectPath = process.cwd()) {
1656
+ const workspaceRoot = findWorkspaceRoot2(projectPath);
1657
+ const packageManager = detectPackageManager(projectPath);
1658
+ const cursorDir = join5(projectPath, ".cursor");
1659
+ const cursorDirExists = existsSync5(cursorDir);
1660
+ const styleguidePath = join5(projectPath, ".uilint", "styleguide.md");
1661
+ const styleguideExists = existsSync5(styleguidePath);
1662
+ const commandsDir = join5(cursorDir, "commands");
1663
+ const genstyleguideExists = existsSync5(join5(commandsDir, "genstyleguide.md"));
1664
+ const nextApps = [];
1665
+ const directDetection = detectNextAppRouter(projectPath);
1666
+ if (directDetection) {
1667
+ nextApps.push({ projectPath, detection: directDetection });
1668
+ } else {
1669
+ const matches = findNextAppRouterProjects(workspaceRoot, { maxDepth: 5 });
1670
+ for (const match of matches) {
1671
+ nextApps.push({
1672
+ projectPath: match.projectPath,
1673
+ detection: match.detection
1674
+ });
1675
+ }
1676
+ }
1677
+ const viteApps = [];
1678
+ const directVite = detectViteReact(projectPath);
1679
+ if (directVite) {
1680
+ viteApps.push({ projectPath, detection: directVite });
1681
+ } else {
1682
+ const matches = findViteReactProjects(workspaceRoot, { maxDepth: 5 });
1683
+ for (const match of matches) {
1684
+ viteApps.push({
1685
+ projectPath: match.projectPath,
1686
+ detection: match.detection
1687
+ });
1688
+ }
1689
+ }
1690
+ const rawPackages = findPackages(workspaceRoot);
1691
+ const packages = rawPackages.map((pkg) => {
1692
+ const eslintConfigPath = findEslintConfigFile(pkg.path);
1693
+ let eslintConfigFilename = null;
1694
+ let hasRules = false;
1695
+ let configuredRuleIds = [];
1696
+ if (eslintConfigPath) {
1697
+ eslintConfigFilename = getEslintConfigFilename(eslintConfigPath);
1698
+ try {
1699
+ const source = readFileSync4(eslintConfigPath, "utf-8");
1700
+ const info = getUilintEslintConfigInfoFromSource(source);
1701
+ hasRules = info.configuredRuleIds.size > 0;
1702
+ configuredRuleIds = Array.from(info.configuredRuleIds);
1703
+ } catch {
1704
+ }
1705
+ }
1706
+ return {
1707
+ ...pkg,
1708
+ eslintConfigPath,
1709
+ eslintConfigFilename,
1710
+ hasUilintRules: hasRules,
1711
+ configuredRuleIds
1712
+ };
1713
+ });
1714
+ return {
1715
+ projectPath,
1716
+ workspaceRoot,
1717
+ packageManager,
1718
+ cursorDir: {
1719
+ exists: cursorDirExists,
1720
+ path: cursorDir
1721
+ },
1722
+ styleguide: {
1723
+ exists: styleguideExists,
1724
+ path: styleguidePath
1725
+ },
1726
+ commands: {
1727
+ genstyleguide: genstyleguideExists
1728
+ },
1729
+ nextApps,
1730
+ viteApps,
1731
+ packages
1732
+ };
1733
+ }
1734
+
1735
+ // src/commands/install/execute.ts
1736
+ import {
1737
+ existsSync as existsSync11,
1738
+ mkdirSync,
1739
+ writeFileSync as writeFileSync5,
1740
+ readFileSync as readFileSync8,
1741
+ unlinkSync,
1742
+ chmodSync
1743
+ } from "fs";
1744
+ import { dirname as dirname4 } from "path";
1745
+
1746
+ // src/utils/react-inject.ts
1747
+ import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "fs";
1748
+ import { join as join6 } from "path";
1749
+ import { parseModule as parseModule2, generateCode as generateCode2 } from "magicast";
1750
+ function getDefaultCandidates(projectPath, appRoot) {
1751
+ const viteMainCandidates = [
1752
+ join6(appRoot, "main.tsx"),
1753
+ join6(appRoot, "main.jsx"),
1754
+ join6(appRoot, "main.ts"),
1755
+ join6(appRoot, "main.js")
1756
+ ];
1757
+ const existingViteMain = viteMainCandidates.filter(
1758
+ (rel) => existsSync6(join6(projectPath, rel))
1759
+ );
1760
+ if (existingViteMain.length > 0) return existingViteMain;
1761
+ const viteAppCandidates = [join6(appRoot, "App.tsx"), join6(appRoot, "App.jsx")];
1762
+ const existingViteApp = viteAppCandidates.filter(
1763
+ (rel) => existsSync6(join6(projectPath, rel))
1764
+ );
1765
+ if (existingViteApp.length > 0) return existingViteApp;
1766
+ const layoutCandidates = [
1767
+ join6(appRoot, "layout.tsx"),
1768
+ join6(appRoot, "layout.jsx"),
1769
+ join6(appRoot, "layout.ts"),
1770
+ join6(appRoot, "layout.js")
1771
+ ];
1772
+ const existingLayouts = layoutCandidates.filter(
1773
+ (rel) => existsSync6(join6(projectPath, rel))
1774
+ );
1775
+ if (existingLayouts.length > 0) {
1776
+ return existingLayouts;
1777
+ }
1778
+ const pageCandidates = [join6(appRoot, "page.tsx"), join6(appRoot, "page.jsx")];
1779
+ return pageCandidates.filter((rel) => existsSync6(join6(projectPath, rel)));
1780
+ }
1781
+ function isUseClientDirective(stmt) {
1782
+ return stmt?.type === "ExpressionStatement" && stmt.expression?.type === "StringLiteral" && stmt.expression.value === "use client";
1783
+ }
1784
+ function findImportDeclaration(program, from) {
1785
+ if (!program || program.type !== "Program") return null;
1786
+ for (const stmt of program.body ?? []) {
1787
+ if (stmt?.type !== "ImportDeclaration") continue;
1788
+ if (stmt.source?.value === from) return stmt;
1789
+ }
1790
+ return null;
1791
+ }
1792
+ function walkAst(node, visit) {
1793
+ if (!node || typeof node !== "object") return;
1794
+ if (node.type) visit(node);
1795
+ for (const key of Object.keys(node)) {
1796
+ const v = node[key];
1797
+ if (!v) continue;
1798
+ if (Array.isArray(v)) {
1799
+ for (const item of v) walkAst(item, visit);
1800
+ } else if (typeof v === "object" && v.type) {
1801
+ walkAst(v, visit);
1802
+ }
1803
+ }
1804
+ }
1805
+ function hasUILintDevtoolsJsx(program) {
1806
+ let found = false;
1807
+ walkAst(program, (node) => {
1808
+ if (found) return;
1809
+ if (node.type !== "JSXElement") return;
1810
+ const name = node.openingElement?.name;
1811
+ if (name?.type === "JSXIdentifier") {
1812
+ if (name.name === "UILintProvider" || name.name === "uilint-devtools") {
1813
+ found = true;
1814
+ }
1815
+ }
1816
+ });
1817
+ return found;
1818
+ }
1819
+ function addDevtoolsElementNextJs(program) {
1820
+ if (!program || program.type !== "Program") return { changed: false };
1821
+ if (hasUILintDevtoolsJsx(program)) return { changed: false };
1822
+ const devtoolsMod = parseModule2(
1823
+ "const __uilint_devtools = (<uilint-devtools />);"
1824
+ );
1825
+ const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
1826
+ if (!devtoolsJsx || devtoolsJsx.type !== "JSXElement")
1827
+ return { changed: false };
1828
+ let added = false;
1829
+ walkAst(program, (node) => {
1830
+ if (added) return;
1831
+ if (node.type !== "JSXElement" && node.type !== "JSXFragment") return;
1832
+ const children = node.children ?? [];
1833
+ const childrenIndex = children.findIndex(
1834
+ (child) => child?.type === "JSXExpressionContainer" && child.expression?.type === "Identifier" && child.expression.name === "children"
1835
+ );
1836
+ if (childrenIndex === -1) return;
1837
+ children.splice(childrenIndex + 1, 0, devtoolsJsx);
1838
+ added = true;
1839
+ });
1840
+ if (!added) {
1841
+ throw new Error("Could not find `{children}` in target file to add devtools.");
1842
+ }
1843
+ return { changed: true };
1844
+ }
1845
+ function addDevtoolsElementVite(program) {
1846
+ if (!program || program.type !== "Program") return { changed: false };
1847
+ if (hasUILintDevtoolsJsx(program)) return { changed: false };
1848
+ let added = false;
1849
+ walkAst(program, (node) => {
1850
+ if (added) return;
1851
+ if (node.type !== "CallExpression") return;
1852
+ const callee = node.callee;
1853
+ if (callee?.type !== "MemberExpression") return;
1854
+ const prop = callee.property;
1855
+ const isRender = prop?.type === "Identifier" && prop.name === "render" || prop?.type === "StringLiteral" && prop.value === "render" || prop?.type === "Literal" && prop.value === "render";
1856
+ if (!isRender) return;
1857
+ const arg0 = node.arguments?.[0];
1858
+ if (!arg0) return;
1859
+ if (arg0.type !== "JSXElement" && arg0.type !== "JSXFragment") return;
1860
+ const devtoolsMod = parseModule2(
1861
+ "const __uilint_devtools = (<uilint-devtools />);"
1862
+ );
1863
+ const devtoolsJsx = devtoolsMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
1864
+ if (!devtoolsJsx) return;
1865
+ const fragmentMod = parseModule2(
1866
+ "const __fragment = (<></>);"
1867
+ );
1868
+ const fragmentJsx = fragmentMod.$ast.body?.[0]?.declarations?.[0]?.init ?? null;
1869
+ if (!fragmentJsx) return;
1870
+ fragmentJsx.children = [arg0, devtoolsJsx];
1871
+ node.arguments[0] = fragmentJsx;
1872
+ added = true;
1873
+ });
1874
+ if (!added) {
1875
+ throw new Error(
1876
+ "Could not find a `.render(<...>)` call to add devtools. Expected a React entry like `createRoot(...).render(<App />)`."
1877
+ );
1878
+ }
1879
+ return { changed: true };
1880
+ }
1881
+ function ensureSideEffectImport(program, from) {
1882
+ if (!program || program.type !== "Program") return { changed: false };
1883
+ const existing = findImportDeclaration(program, from);
1884
+ if (existing) return { changed: false };
1885
+ const importDecl = parseModule2(`import "${from}";`).$ast.body?.[0];
1886
+ if (!importDecl) return { changed: false };
1887
+ const body = program.body ?? [];
1888
+ let insertAt = 0;
1889
+ while (insertAt < body.length && isUseClientDirective(body[insertAt])) {
1890
+ insertAt++;
1891
+ }
1892
+ while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
1893
+ insertAt++;
1894
+ }
1895
+ program.body.splice(insertAt, 0, importDecl);
1896
+ return { changed: true };
1897
+ }
1898
+ async function installReactUILintOverlay(opts) {
1899
+ const candidates = getDefaultCandidates(opts.projectPath, opts.appRoot);
1900
+ if (!candidates.length) {
1901
+ throw new Error(
1902
+ `No suitable entry files found under ${opts.appRoot} (expected Next.js layout/page or Vite main/App).`
1903
+ );
1904
+ }
1905
+ let chosen;
1906
+ if (candidates.length > 1 && opts.confirmFileChoice) {
1907
+ chosen = await opts.confirmFileChoice(candidates);
1908
+ } else {
1909
+ chosen = candidates[0];
1910
+ }
1911
+ const absTarget = join6(opts.projectPath, chosen);
1912
+ const original = readFileSync5(absTarget, "utf-8");
1913
+ let mod;
1914
+ try {
1915
+ mod = parseModule2(original);
1916
+ } catch {
1917
+ throw new Error(
1918
+ `Unable to parse ${chosen} as JavaScript/TypeScript. Please update it manually.`
1919
+ );
1920
+ }
1921
+ const program = mod.$ast;
1922
+ const hasDevtoolsImport = !!findImportDeclaration(program, "uilint-react/devtools");
1923
+ const hasOldImport = !!findImportDeclaration(program, "uilint-react");
1924
+ const alreadyConfigured = (hasDevtoolsImport || hasOldImport) && hasUILintDevtoolsJsx(program);
1925
+ let changed = false;
1926
+ const importRes = ensureSideEffectImport(program, "uilint-react/devtools");
1927
+ if (importRes.changed) changed = true;
1928
+ const mode = opts.mode ?? "next";
1929
+ const addRes = mode === "vite" ? addDevtoolsElementVite(program) : addDevtoolsElementNextJs(program);
1930
+ if (addRes.changed) changed = true;
1931
+ const updated = changed ? generateCode2(mod).code : original;
1932
+ const modified = updated !== original;
1933
+ if (modified) {
1934
+ writeFileSync2(absTarget, updated, "utf-8");
1935
+ }
1936
+ return {
1937
+ targetFile: chosen,
1938
+ modified,
1939
+ alreadyConfigured: alreadyConfigured && !modified
1940
+ };
1941
+ }
1942
+
1943
+ // src/utils/next-config-inject.ts
1944
+ import { existsSync as existsSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1945
+ import { join as join7 } from "path";
1946
+ import { parseModule as parseModule3, generateCode as generateCode3 } from "magicast";
1947
+ var CONFIG_EXTENSIONS2 = [".ts", ".mjs", ".js", ".cjs"];
1948
+ function findNextConfigFile(projectPath) {
1949
+ for (const ext of CONFIG_EXTENSIONS2) {
1950
+ const configPath = join7(projectPath, `next.config${ext}`);
1951
+ if (existsSync7(configPath)) {
1952
+ return configPath;
1953
+ }
1954
+ }
1955
+ return null;
1956
+ }
1957
+ function getNextConfigFilename(configPath) {
1958
+ const parts = configPath.split("/");
1959
+ return parts[parts.length - 1] || "next.config.ts";
1960
+ }
1961
+ function isIdentifier2(node, name) {
1962
+ return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
1963
+ }
1964
+ function isStringLiteral2(node) {
1965
+ return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
1966
+ }
1967
+ function ensureEsmWithJsxLocImport(program) {
1968
+ if (!program || program.type !== "Program") return { changed: false };
1969
+ const existing = (program.body ?? []).find(
1970
+ (s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin"
1971
+ );
1972
+ if (existing) {
1973
+ const has = (existing.specifiers ?? []).some(
1974
+ (sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "withJsxLoc" || sp.imported?.value === "withJsxLoc")
1975
+ );
1976
+ if (has) return { changed: false };
1977
+ const spec = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0]?.specifiers?.[0];
1978
+ if (!spec) return { changed: false };
1979
+ existing.specifiers = [...existing.specifiers ?? [], spec];
1980
+ return { changed: true };
1981
+ }
1982
+ const importDecl = parseModule3('import { withJsxLoc } from "jsx-loc-plugin";').$ast.body?.[0];
1983
+ if (!importDecl) return { changed: false };
1984
+ const body = program.body ?? [];
1985
+ let insertAt = 0;
1986
+ while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
1987
+ insertAt++;
1988
+ }
1989
+ program.body.splice(insertAt, 0, importDecl);
1990
+ return { changed: true };
1991
+ }
1992
+ function ensureCjsWithJsxLocRequire(program) {
1993
+ if (!program || program.type !== "Program") return { changed: false };
1994
+ for (const stmt of program.body ?? []) {
1995
+ if (stmt?.type !== "VariableDeclaration") continue;
1996
+ for (const decl of stmt.declarations ?? []) {
1997
+ const init = decl?.init;
1998
+ if (init?.type === "CallExpression" && isIdentifier2(init.callee, "require") && isStringLiteral2(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin") {
1999
+ if (decl.id?.type === "ObjectPattern") {
2000
+ const has = (decl.id.properties ?? []).some((p) => {
2001
+ if (p?.type !== "ObjectProperty" && p?.type !== "Property") return false;
2002
+ return isIdentifier2(p.key, "withJsxLoc");
2003
+ });
2004
+ if (has) return { changed: false };
2005
+ const prop = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
2006
+ if (!prop) return { changed: false };
2007
+ decl.id.properties = [...decl.id.properties ?? [], prop];
2008
+ return { changed: true };
2009
+ }
2010
+ return { changed: false };
2011
+ }
2012
+ }
2013
+ }
2014
+ const reqDecl = parseModule3('const { withJsxLoc } = require("jsx-loc-plugin");').$ast.body?.[0];
2015
+ if (!reqDecl) return { changed: false };
2016
+ program.body.unshift(reqDecl);
2017
+ return { changed: true };
2018
+ }
2019
+ function wrapEsmExportDefault(program) {
2020
+ if (!program || program.type !== "Program") return { changed: false };
2021
+ const exportDecl = (program.body ?? []).find(
2022
+ (s) => s?.type === "ExportDefaultDeclaration"
2023
+ );
2024
+ if (!exportDecl) return { changed: false };
2025
+ const decl = exportDecl.declaration;
2026
+ if (decl?.type === "CallExpression" && isIdentifier2(decl.callee, "withJsxLoc")) {
2027
+ return { changed: false };
2028
+ }
2029
+ exportDecl.declaration = {
2030
+ type: "CallExpression",
2031
+ callee: { type: "Identifier", name: "withJsxLoc" },
2032
+ arguments: [decl]
2033
+ };
2034
+ return { changed: true };
2035
+ }
2036
+ function wrapCjsModuleExports(program) {
2037
+ if (!program || program.type !== "Program") return { changed: false };
2038
+ for (const stmt of program.body ?? []) {
2039
+ if (!stmt || stmt.type !== "ExpressionStatement") continue;
2040
+ const expr = stmt.expression;
2041
+ if (!expr || expr.type !== "AssignmentExpression") continue;
2042
+ const left = expr.left;
2043
+ const right = expr.right;
2044
+ const isModuleExports = left?.type === "MemberExpression" && isIdentifier2(left.object, "module") && isIdentifier2(left.property, "exports");
2045
+ if (!isModuleExports) continue;
2046
+ if (right?.type === "CallExpression" && isIdentifier2(right.callee, "withJsxLoc")) {
2047
+ return { changed: false };
2048
+ }
2049
+ expr.right = {
2050
+ type: "CallExpression",
2051
+ callee: { type: "Identifier", name: "withJsxLoc" },
2052
+ arguments: [right]
2053
+ };
2054
+ return { changed: true };
2055
+ }
2056
+ return { changed: false };
2057
+ }
2058
+ async function installJsxLocPlugin(opts) {
2059
+ const configPath = findNextConfigFile(opts.projectPath);
2060
+ if (!configPath) {
2061
+ return { configFile: null, modified: false };
2062
+ }
2063
+ const configFilename = getNextConfigFilename(configPath);
2064
+ const original = readFileSync6(configPath, "utf-8");
2065
+ let mod;
2066
+ try {
2067
+ mod = parseModule3(original);
2068
+ } catch {
2069
+ return { configFile: configFilename, modified: false };
2070
+ }
2071
+ const program = mod.$ast;
2072
+ const isCjs = configPath.endsWith(".cjs");
2073
+ let changed = false;
2074
+ if (isCjs) {
2075
+ const reqRes = ensureCjsWithJsxLocRequire(program);
2076
+ if (reqRes.changed) changed = true;
2077
+ const wrapRes = wrapCjsModuleExports(program);
2078
+ if (wrapRes.changed) changed = true;
2079
+ } else {
2080
+ const impRes = ensureEsmWithJsxLocImport(program);
2081
+ if (impRes.changed) changed = true;
2082
+ const wrapRes = wrapEsmExportDefault(program);
2083
+ if (wrapRes.changed) changed = true;
2084
+ }
2085
+ const updated = changed ? generateCode3(mod).code : original;
2086
+ if (updated !== original) {
2087
+ writeFileSync3(configPath, updated, "utf-8");
2088
+ return { configFile: configFilename, modified: true };
2089
+ }
2090
+ return { configFile: configFilename, modified: false };
2091
+ }
2092
+
2093
+ // src/utils/vite-config-inject.ts
2094
+ import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
2095
+ import { join as join8 } from "path";
2096
+ import { parseModule as parseModule4, generateCode as generateCode4 } from "magicast";
2097
+ var CONFIG_EXTENSIONS3 = [".ts", ".mjs", ".js", ".cjs"];
2098
+ function findViteConfigFile2(projectPath) {
2099
+ for (const ext of CONFIG_EXTENSIONS3) {
2100
+ const configPath = join8(projectPath, `vite.config${ext}`);
2101
+ if (existsSync8(configPath)) return configPath;
2102
+ }
2103
+ return null;
2104
+ }
2105
+ function getViteConfigFilename(configPath) {
2106
+ const parts = configPath.split("/");
2107
+ return parts[parts.length - 1] || "vite.config.ts";
2108
+ }
2109
+ function isIdentifier3(node, name) {
2110
+ return !!node && node.type === "Identifier" && (name ? node.name === name : typeof node.name === "string");
2111
+ }
2112
+ function isStringLiteral3(node) {
2113
+ return !!node && (node.type === "StringLiteral" || node.type === "Literal") && typeof node.value === "string";
2114
+ }
2115
+ function unwrapExpression(expr) {
2116
+ let e = expr;
2117
+ while (e) {
2118
+ if (e.type === "TSAsExpression" || e.type === "TSNonNullExpression") {
2119
+ e = e.expression;
2120
+ continue;
2121
+ }
2122
+ if (e.type === "TSSatisfiesExpression") {
2123
+ e = e.expression;
2124
+ continue;
2125
+ }
2126
+ if (e.type === "ParenthesizedExpression") {
2127
+ e = e.expression;
2128
+ continue;
2129
+ }
2130
+ break;
2131
+ }
2132
+ return e;
2133
+ }
2134
+ function findExportedConfigObjectExpression(mod) {
2135
+ const program = mod?.$ast;
2136
+ if (!program || program.type !== "Program") return null;
2137
+ for (const stmt of program.body ?? []) {
2138
+ if (!stmt || stmt.type !== "ExportDefaultDeclaration") continue;
2139
+ const decl = unwrapExpression(stmt.declaration);
2140
+ if (!decl) break;
2141
+ if (decl.type === "ObjectExpression") {
2142
+ return { kind: "esm", objExpr: decl, program };
2143
+ }
2144
+ if (decl.type === "CallExpression" && isIdentifier3(decl.callee, "defineConfig") && unwrapExpression(decl.arguments?.[0])?.type === "ObjectExpression") {
2145
+ return {
2146
+ kind: "esm",
2147
+ objExpr: unwrapExpression(decl.arguments?.[0]),
2148
+ program
2149
+ };
2150
+ }
2151
+ break;
2152
+ }
2153
+ for (const stmt of program.body ?? []) {
2154
+ if (!stmt || stmt.type !== "ExpressionStatement") continue;
2155
+ const expr = stmt.expression;
2156
+ if (!expr || expr.type !== "AssignmentExpression") continue;
2157
+ const left = expr.left;
2158
+ const right = unwrapExpression(expr.right);
2159
+ const isModuleExports = left?.type === "MemberExpression" && isIdentifier3(left.object, "module") && isIdentifier3(left.property, "exports");
2160
+ if (!isModuleExports) continue;
2161
+ if (right?.type === "ObjectExpression") {
2162
+ return { kind: "cjs", objExpr: right, program };
2163
+ }
2164
+ if (right?.type === "CallExpression" && isIdentifier3(right.callee, "defineConfig") && unwrapExpression(right.arguments?.[0])?.type === "ObjectExpression") {
2165
+ return {
2166
+ kind: "cjs",
2167
+ objExpr: unwrapExpression(right.arguments?.[0]),
2168
+ program
2169
+ };
2170
+ }
2171
+ }
2172
+ return null;
2173
+ }
2174
+ function getObjectProperty(obj, keyName) {
2175
+ if (!obj || obj.type !== "ObjectExpression") return null;
2176
+ for (const prop of obj.properties ?? []) {
2177
+ if (!prop) continue;
2178
+ if (prop.type !== "ObjectProperty" && prop.type !== "Property") continue;
2179
+ const key = prop.key;
2180
+ const keyMatch = key?.type === "Identifier" && key.name === keyName || isStringLiteral3(key) && key.value === keyName;
2181
+ if (keyMatch) return prop;
2182
+ }
2183
+ return null;
2184
+ }
2185
+ function ensureEsmJsxLocImport(program) {
2186
+ if (!program || program.type !== "Program") return { changed: false };
2187
+ const existing = (program.body ?? []).find(
2188
+ (s) => s?.type === "ImportDeclaration" && s.source?.value === "jsx-loc-plugin/vite"
2189
+ );
2190
+ if (existing) {
2191
+ const has = (existing.specifiers ?? []).some(
2192
+ (sp) => sp?.type === "ImportSpecifier" && (sp.imported?.name === "jsxLoc" || sp.imported?.value === "jsxLoc")
2193
+ );
2194
+ if (has) return { changed: false };
2195
+ const spec = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0]?.specifiers?.[0];
2196
+ if (!spec) return { changed: false };
2197
+ existing.specifiers = [...existing.specifiers ?? [], spec];
2198
+ return { changed: true };
2199
+ }
2200
+ const importDecl = parseModule4('import { jsxLoc } from "jsx-loc-plugin/vite";').$ast.body?.[0];
2201
+ if (!importDecl) return { changed: false };
2202
+ const body = program.body ?? [];
2203
+ let insertAt = 0;
2204
+ while (insertAt < body.length && body[insertAt]?.type === "ImportDeclaration") {
2205
+ insertAt++;
2206
+ }
2207
+ program.body.splice(insertAt, 0, importDecl);
2208
+ return { changed: true };
2209
+ }
2210
+ function ensureCjsJsxLocRequire(program) {
2211
+ if (!program || program.type !== "Program") return { changed: false };
2212
+ for (const stmt of program.body ?? []) {
2213
+ if (stmt?.type !== "VariableDeclaration") continue;
2214
+ for (const decl of stmt.declarations ?? []) {
2215
+ const init = decl?.init;
2216
+ if (init?.type === "CallExpression" && isIdentifier3(init.callee, "require") && isStringLiteral3(init.arguments?.[0]) && init.arguments[0].value === "jsx-loc-plugin/vite") {
2217
+ if (decl.id?.type === "ObjectPattern") {
2218
+ const has = (decl.id.properties ?? []).some((p) => {
2219
+ if (p?.type !== "ObjectProperty" && p?.type !== "Property") return false;
2220
+ return isIdentifier3(p.key, "jsxLoc");
2221
+ });
2222
+ if (has) return { changed: false };
2223
+ const prop = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0]?.declarations?.[0]?.id?.properties?.[0];
2224
+ if (!prop) return { changed: false };
2225
+ decl.id.properties = [...decl.id.properties ?? [], prop];
2226
+ return { changed: true };
2227
+ }
2228
+ return { changed: false };
2229
+ }
2230
+ }
2231
+ }
2232
+ const reqDecl = parseModule4('const { jsxLoc } = require("jsx-loc-plugin/vite");').$ast.body?.[0];
2233
+ if (!reqDecl) return { changed: false };
2234
+ program.body.unshift(reqDecl);
2235
+ return { changed: true };
2236
+ }
2237
+ function pluginsHasJsxLoc(arr) {
2238
+ if (!arr || arr.type !== "ArrayExpression") return false;
2239
+ for (const el of arr.elements ?? []) {
2240
+ const e = unwrapExpression(el);
2241
+ if (!e) continue;
2242
+ if (e.type === "CallExpression" && isIdentifier3(e.callee, "jsxLoc")) return true;
2243
+ }
2244
+ return false;
2245
+ }
2246
+ function ensurePluginsContainsJsxLoc(configObj) {
2247
+ const pluginsProp = getObjectProperty(configObj, "plugins");
2248
+ if (!pluginsProp) {
2249
+ const prop = parseModule4("export default { plugins: [jsxLoc()] };").$ast.body?.find((s) => s.type === "ExportDefaultDeclaration")?.declaration?.properties?.find((p) => {
2250
+ const k = p?.key;
2251
+ return k?.type === "Identifier" && k.name === "plugins" || isStringLiteral3(k) && k.value === "plugins";
2252
+ });
2253
+ if (!prop) return { changed: false };
2254
+ configObj.properties = [...configObj.properties ?? [], prop];
2255
+ return { changed: true };
2256
+ }
2257
+ const value = unwrapExpression(pluginsProp.value);
2258
+ if (!value) return { changed: false };
2259
+ if (value.type === "ArrayExpression") {
2260
+ if (pluginsHasJsxLoc(value)) return { changed: false };
2261
+ const jsxLocCall2 = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
2262
+ if (!jsxLocCall2) return { changed: false };
2263
+ value.elements.push(jsxLocCall2);
2264
+ return { changed: true };
2265
+ }
2266
+ const jsxLocCall = parseModule4("const __x = jsxLoc();").$ast.body?.[0]?.declarations?.[0]?.init;
2267
+ if (!jsxLocCall) return { changed: false };
2268
+ const spread = { type: "SpreadElement", argument: value };
2269
+ pluginsProp.value = { type: "ArrayExpression", elements: [spread, jsxLocCall] };
2270
+ return { changed: true };
2271
+ }
2272
+ async function installViteJsxLocPlugin(opts) {
2273
+ const configPath = findViteConfigFile2(opts.projectPath);
2274
+ if (!configPath) return { configFile: null, modified: false };
2275
+ const configFilename = getViteConfigFilename(configPath);
2276
+ const original = readFileSync7(configPath, "utf-8");
2277
+ const isCjs = configPath.endsWith(".cjs");
2278
+ let mod;
2279
+ try {
2280
+ mod = parseModule4(original);
2281
+ } catch {
2282
+ return { configFile: configFilename, modified: false };
2283
+ }
2284
+ const found = findExportedConfigObjectExpression(mod);
2285
+ if (!found) return { configFile: configFilename, modified: false };
2286
+ let changed = false;
2287
+ if (isCjs) {
2288
+ const reqRes = ensureCjsJsxLocRequire(found.program);
2289
+ if (reqRes.changed) changed = true;
2290
+ } else {
2291
+ const impRes = ensureEsmJsxLocImport(found.program);
2292
+ if (impRes.changed) changed = true;
2293
+ }
2294
+ const pluginsRes = ensurePluginsContainsJsxLoc(found.objExpr);
2295
+ if (pluginsRes.changed) changed = true;
2296
+ const updated = changed ? generateCode4(mod).code : original;
2297
+ if (updated !== original) {
2298
+ writeFileSync4(configPath, updated, "utf-8");
2299
+ return { configFile: configFilename, modified: true };
2300
+ }
2301
+ return { configFile: configFilename, modified: false };
2302
+ }
2303
+
2304
+ // src/utils/next-routes.ts
2305
+ import { existsSync as existsSync9 } from "fs";
2306
+ import { mkdir, writeFile } from "fs/promises";
2307
+ import { join as join9 } from "path";
2308
+ var DEV_SOURCE_ROUTE_TS = `/**
2309
+ * Dev-only API route for fetching source files
2310
+ *
2311
+ * This route allows the UILint overlay to fetch and display source code
2312
+ * for components rendered on the page.
2313
+ *
2314
+ * Security:
2315
+ * - Only available in development mode
2316
+ * - Validates file path is within project root
2317
+ * - Only allows specific file extensions
2318
+ */
2319
+
2320
+ import { NextRequest, NextResponse } from "next/server";
2321
+ import { readFileSync, existsSync } from "fs";
2322
+ import { resolve, relative, dirname, extname, sep } from "path";
2323
+ import { fileURLToPath } from "url";
2324
+
2325
+ export const runtime = "nodejs";
2326
+
2327
+ // Allowed file extensions
2328
+ const ALLOWED_EXTENSIONS = new Set([".tsx", ".ts", ".jsx", ".js", ".css"]);
2329
+
2330
+ /**
2331
+ * Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
2332
+ *
2333
+ * Why: In monorepos, process.cwd() might be the workspace root (it also has package.json),
2334
+ * which would incorrectly store/read files under the wrong directory.
2335
+ */
2336
+ function findNextProjectRoot(): string {
2337
+ // Prefer discovering via this route module's on-disk path.
2338
+ // In Next, route code is executed from within ".next/server/...".
2339
+ try {
2340
+ const selfPath = fileURLToPath(import.meta.url);
2341
+ const marker = sep + ".next" + sep;
2342
+ const idx = selfPath.lastIndexOf(marker);
2343
+ if (idx !== -1) {
2344
+ return selfPath.slice(0, idx);
2345
+ }
2346
+ } catch {
2347
+ // ignore
2348
+ }
2349
+
2350
+ // Fallback: walk up from cwd looking for .next/
2351
+ let dir = process.cwd();
2352
+ for (let i = 0; i < 20; i++) {
2353
+ if (existsSync(resolve(dir, ".next"))) return dir;
2354
+ const parent = dirname(dir);
2355
+ if (parent === dir) break;
2356
+ dir = parent;
2357
+ }
2358
+
2359
+ // Final fallback: cwd
2360
+ return process.cwd();
2361
+ }
2362
+
2363
+ /**
2364
+ * Validate that a path is within the allowed directory
2365
+ */
2366
+ function isPathWithinRoot(filePath: string, root: string): boolean {
2367
+ const resolved = resolve(filePath);
2368
+ const resolvedRoot = resolve(root);
2369
+ return resolved.startsWith(resolvedRoot + "/") || resolved === resolvedRoot;
2370
+ }
2371
+
2372
+ /**
2373
+ * Find workspace root by walking up looking for pnpm-workspace.yaml or .git
2374
+ */
2375
+ function findWorkspaceRoot(startDir: string): string {
2376
+ let dir = startDir;
2377
+ for (let i = 0; i < 10; i++) {
2378
+ if (
2379
+ existsSync(resolve(dir, "pnpm-workspace.yaml")) ||
2380
+ existsSync(resolve(dir, ".git"))
2381
+ ) {
2382
+ return dir;
2383
+ }
2384
+ const parent = dirname(dir);
2385
+ if (parent === dir) break;
2386
+ dir = parent;
2387
+ }
2388
+ return startDir;
2389
+ }
2390
+
2391
+ export async function GET(request: NextRequest) {
2392
+ // Block in production
2393
+ if (process.env.NODE_ENV === "production") {
2394
+ return NextResponse.json(
2395
+ { error: "Not available in production" },
2396
+ { status: 404 }
2397
+ );
2398
+ }
2399
+
2400
+ const { searchParams } = new URL(request.url);
2401
+ const filePath = searchParams.get("path");
2402
+
2403
+ if (!filePath) {
2404
+ return NextResponse.json(
2405
+ { error: "Missing 'path' query parameter" },
2406
+ { status: 400 }
2407
+ );
2408
+ }
2409
+
2410
+ // Validate extension
2411
+ const ext = extname(filePath).toLowerCase();
2412
+ if (!ALLOWED_EXTENSIONS.has(ext)) {
2413
+ return NextResponse.json(
2414
+ { error: \`File extension '\${ext}' not allowed\` },
2415
+ { status: 403 }
2416
+ );
2417
+ }
2418
+
2419
+ // Find project root (prefer Next project root over workspace root)
2420
+ const projectRoot = findNextProjectRoot();
2421
+
2422
+ // Resolve the file path
2423
+ const resolvedPath = resolve(filePath);
2424
+
2425
+ // Security check: ensure path is within project root or workspace root
2426
+ const workspaceRoot = findWorkspaceRoot(projectRoot);
2427
+ const isWithinApp = isPathWithinRoot(resolvedPath, projectRoot);
2428
+ const isWithinWorkspace = isPathWithinRoot(resolvedPath, workspaceRoot);
2429
+
2430
+ if (!isWithinApp && !isWithinWorkspace) {
2431
+ return NextResponse.json(
2432
+ { error: "Path outside project directory" },
2433
+ { status: 403 }
2434
+ );
2435
+ }
2436
+
2437
+ // Check file exists
2438
+ if (!existsSync(resolvedPath)) {
2439
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
2440
+ }
2441
+
2442
+ try {
2443
+ const content = readFileSync(resolvedPath, "utf-8");
2444
+ const relativePath = relative(workspaceRoot, resolvedPath);
2445
+
2446
+ return NextResponse.json({
2447
+ content,
2448
+ relativePath,
2449
+ projectRoot,
2450
+ workspaceRoot,
2451
+ });
2452
+ } catch (error) {
2453
+ console.error("[Dev Source API] Error reading file:", error);
2454
+ return NextResponse.json({ error: "Failed to read file" }, { status: 500 });
2455
+ }
2456
+ }
2457
+ `;
2458
+ var SCREENSHOT_ROUTE_TS = `/**
2459
+ * Dev-only API route for saving and retrieving vision analysis screenshots
2460
+ *
2461
+ * This route allows the UILint overlay to:
2462
+ * - POST: Save screenshots and element manifests for vision analysis
2463
+ * - GET: Retrieve screenshots or list available screenshots
2464
+ *
2465
+ * Security:
2466
+ * - Only available in development mode
2467
+ * - Saves to .uilint/screenshots/ directory within project
2468
+ */
2469
+
2470
+ import { NextRequest, NextResponse } from "next/server";
2471
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from "fs";
2472
+ import { resolve, join, dirname, basename, sep } from "path";
2473
+ import { fileURLToPath } from "url";
2474
+
2475
+ export const runtime = "nodejs";
2476
+
2477
+ // Maximum screenshot size (10MB)
2478
+ const MAX_SCREENSHOT_SIZE = 10 * 1024 * 1024;
2479
+
2480
+ /**
2481
+ * Best-effort: resolve the Next.js project root (the dir that owns .next/) even in monorepos.
2482
+ */
2483
+ function findNextProjectRoot(): string {
2484
+ try {
2485
+ const selfPath = fileURLToPath(import.meta.url);
2486
+ const marker = sep + ".next" + sep;
2487
+ const idx = selfPath.lastIndexOf(marker);
2488
+ if (idx !== -1) {
2489
+ return selfPath.slice(0, idx);
2490
+ }
2491
+ } catch {
2492
+ // ignore
2493
+ }
2494
+
2495
+ let dir = process.cwd();
2496
+ for (let i = 0; i < 20; i++) {
2497
+ if (existsSync(resolve(dir, ".next"))) return dir;
2498
+ const parent = dirname(dir);
2499
+ if (parent === dir) break;
2500
+ dir = parent;
2501
+ }
2502
+
2503
+ return process.cwd();
2504
+ }
2505
+
2506
+ /**
2507
+ * Get the screenshots directory path, creating it if needed
2508
+ */
2509
+ function getScreenshotsDir(projectRoot: string): string {
2510
+ const screenshotsDir = join(projectRoot, ".uilint", "screenshots");
2511
+ if (!existsSync(screenshotsDir)) {
2512
+ mkdirSync(screenshotsDir, { recursive: true });
2513
+ }
2514
+ return screenshotsDir;
2515
+ }
2516
+
2517
+ /**
2518
+ * Validate filename to prevent path traversal
2519
+ */
2520
+ function isValidFilename(filename: string): boolean {
2521
+ // Only allow alphanumeric, hyphens, underscores, and dots
2522
+ // Must end with .png, .jpeg, .jpg, or .json
2523
+ const validPattern = /^[a-zA-Z0-9_-]+\\.(png|jpeg|jpg|json)$/;
2524
+ return validPattern.test(filename) && !filename.includes("..");
2525
+ }
2526
+
2527
+ /**
2528
+ * POST: Save a screenshot and optionally its manifest
2529
+ */
2530
+ export async function POST(request: NextRequest) {
2531
+ // Block in production
2532
+ if (process.env.NODE_ENV === "production") {
2533
+ return NextResponse.json(
2534
+ { error: "Not available in production" },
2535
+ { status: 404 }
2536
+ );
2537
+ }
2538
+
2539
+ try {
2540
+ const body = await request.json();
2541
+ const { filename, imageData, manifest, analysisResult } = body;
2542
+
2543
+ if (!filename) {
2544
+ return NextResponse.json({ error: "Missing 'filename'" }, { status: 400 });
2545
+ }
2546
+
2547
+ // Validate filename
2548
+ if (!isValidFilename(filename)) {
2549
+ return NextResponse.json(
2550
+ { error: "Invalid filename format" },
2551
+ { status: 400 }
2552
+ );
2553
+ }
2554
+
2555
+ // Allow "sidecar-only" updates (manifest/analysisResult) without re-sending image bytes.
2556
+ const hasImageData = typeof imageData === "string" && imageData.length > 0;
2557
+ const hasSidecar =
2558
+ typeof manifest !== "undefined" || typeof analysisResult !== "undefined";
2559
+
2560
+ if (!hasImageData && !hasSidecar) {
2561
+ return NextResponse.json(
2562
+ { error: "Nothing to save (provide imageData and/or manifest/analysisResult)" },
2563
+ { status: 400 }
2564
+ );
2565
+ }
2566
+
2567
+ // Check size (image only)
2568
+ if (hasImageData && imageData.length > MAX_SCREENSHOT_SIZE) {
2569
+ return NextResponse.json(
2570
+ { error: "Screenshot too large (max 10MB)" },
2571
+ { status: 413 }
2572
+ );
2573
+ }
2574
+
2575
+ const projectRoot = findNextProjectRoot();
2576
+ const screenshotsDir = getScreenshotsDir(projectRoot);
2577
+
2578
+ const imagePath = join(screenshotsDir, filename);
2579
+
2580
+ // Save the image (base64 data URL) if provided
2581
+ if (hasImageData) {
2582
+ const base64Data = imageData.includes(",")
2583
+ ? imageData.split(",")[1]
2584
+ : imageData;
2585
+ writeFileSync(imagePath, Buffer.from(base64Data, "base64"));
2586
+ }
2587
+
2588
+ // Save manifest and analysis result as JSON sidecar
2589
+ if (hasSidecar) {
2590
+ const jsonFilename = filename.replace(/\\.(png|jpeg|jpg)$/, ".json");
2591
+ const jsonPath = join(screenshotsDir, jsonFilename);
2592
+
2593
+ // If a sidecar already exists, merge updates (lets us POST analysisResult later without re-sending image).
2594
+ let existing: any = null;
2595
+ if (existsSync(jsonPath)) {
2596
+ try {
2597
+ existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
2598
+ } catch {
2599
+ existing = null;
2600
+ }
2601
+ }
2602
+
2603
+ const routeFromAnalysis =
2604
+ analysisResult && typeof analysisResult === "object"
2605
+ ? (analysisResult as any).route
2606
+ : undefined;
2607
+ const issuesFromAnalysis =
2608
+ analysisResult && typeof analysisResult === "object"
2609
+ ? (analysisResult as any).issues
2610
+ : undefined;
2611
+
2612
+ const jsonData = {
2613
+ ...(existing && typeof existing === "object" ? existing : {}),
2614
+ timestamp: Date.now(),
2615
+ filename,
2616
+ screenshotFile: filename,
2617
+ route:
2618
+ typeof routeFromAnalysis === "string"
2619
+ ? routeFromAnalysis
2620
+ : (existing as any)?.route ?? null,
2621
+ issues:
2622
+ Array.isArray(issuesFromAnalysis)
2623
+ ? issuesFromAnalysis
2624
+ : (existing as any)?.issues ?? null,
2625
+ manifest: typeof manifest === "undefined" ? existing?.manifest ?? null : manifest,
2626
+ analysisResult:
2627
+ typeof analysisResult === "undefined"
2628
+ ? existing?.analysisResult ?? null
2629
+ : analysisResult,
2630
+ };
2631
+ writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2));
2632
+ }
2633
+
2634
+ return NextResponse.json({
2635
+ success: true,
2636
+ path: imagePath,
2637
+ projectRoot,
2638
+ screenshotsDir,
2639
+ });
2640
+ } catch (error) {
2641
+ console.error("[Screenshot API] Error saving screenshot:", error);
2642
+ return NextResponse.json(
2643
+ { error: "Failed to save screenshot" },
2644
+ { status: 500 }
2645
+ );
2646
+ }
2647
+ }
2648
+
2649
+ /**
2650
+ * GET: Retrieve a screenshot or list available screenshots
2651
+ */
2652
+ export async function GET(request: NextRequest) {
2653
+ // Block in production
2654
+ if (process.env.NODE_ENV === "production") {
2655
+ return NextResponse.json(
2656
+ { error: "Not available in production" },
2657
+ { status: 404 }
2658
+ );
2659
+ }
2660
+
2661
+ const { searchParams } = new URL(request.url);
2662
+ const filename = searchParams.get("filename");
2663
+ const list = searchParams.get("list");
2664
+
2665
+ const projectRoot = findNextProjectRoot();
2666
+ const screenshotsDir = getScreenshotsDir(projectRoot);
2667
+
2668
+ // List mode: return all screenshots
2669
+ if (list === "true") {
2670
+ try {
2671
+ const files = readdirSync(screenshotsDir);
2672
+ const screenshots = files
2673
+ .filter((f) => /\\.(png|jpeg|jpg)$/.test(f))
2674
+ .map((f) => {
2675
+ const jsonFile = f.replace(/\\.(png|jpeg|jpg)$/, ".json");
2676
+ const jsonPath = join(screenshotsDir, jsonFile);
2677
+ let metadata = null;
2678
+ if (existsSync(jsonPath)) {
2679
+ try {
2680
+ metadata = JSON.parse(readFileSync(jsonPath, "utf-8"));
2681
+ } catch {
2682
+ // Ignore parse errors
2683
+ }
2684
+ }
2685
+ return {
2686
+ filename: f,
2687
+ metadata,
2688
+ };
2689
+ })
2690
+ .sort((a, b) => {
2691
+ // Sort by timestamp descending (newest first)
2692
+ const aTime = a.metadata?.timestamp || 0;
2693
+ const bTime = b.metadata?.timestamp || 0;
2694
+ return bTime - aTime;
2695
+ });
2696
+
2697
+ return NextResponse.json({ screenshots, projectRoot, screenshotsDir });
2698
+ } catch (error) {
2699
+ console.error("[Screenshot API] Error listing screenshots:", error);
2700
+ return NextResponse.json(
2701
+ { error: "Failed to list screenshots" },
2702
+ { status: 500 }
2703
+ );
2704
+ }
2705
+ }
2706
+
2707
+ // Retrieve mode: get specific screenshot
2708
+ if (!filename) {
2709
+ return NextResponse.json(
2710
+ { error: "Missing 'filename' parameter" },
2711
+ { status: 400 }
2712
+ );
2713
+ }
2714
+
2715
+ if (!isValidFilename(filename)) {
2716
+ return NextResponse.json(
2717
+ { error: "Invalid filename format" },
2718
+ { status: 400 }
2719
+ );
2720
+ }
2721
+
2722
+ const filePath = join(screenshotsDir, filename);
2723
+
2724
+ if (!existsSync(filePath)) {
2725
+ return NextResponse.json(
2726
+ { error: "Screenshot not found" },
2727
+ { status: 404 }
2728
+ );
2729
+ }
2730
+
2731
+ try {
2732
+ const content = readFileSync(filePath);
2733
+
2734
+ // Determine content type
2735
+ const ext = filename.split(".").pop()?.toLowerCase();
2736
+ const contentType =
2737
+ ext === "json"
2738
+ ? "application/json"
2739
+ : ext === "png"
2740
+ ? "image/png"
2741
+ : "image/jpeg";
2742
+
2743
+ if (ext === "json") {
2744
+ return NextResponse.json(JSON.parse(content.toString()));
2745
+ }
2746
+
2747
+ return new NextResponse(content, {
2748
+ headers: {
2749
+ "Content-Type": contentType,
2750
+ "Cache-Control": "no-cache",
2751
+ },
2752
+ });
2753
+ } catch (error) {
2754
+ console.error("[Screenshot API] Error reading screenshot:", error);
2755
+ return NextResponse.json(
2756
+ { error: "Failed to read screenshot" },
2757
+ { status: 500 }
2758
+ );
2759
+ }
2760
+ }
2761
+ `;
2762
+ async function writeRouteFile(absPath, relPath, content, opts) {
2763
+ if (existsSync9(absPath) && !opts.force) return;
2764
+ await writeFile(absPath, content, "utf-8");
2765
+ }
2766
+ async function installNextUILintRoutes(opts) {
2767
+ const baseRel = join9(opts.appRoot, "api", ".uilint");
2768
+ const baseAbs = join9(opts.projectPath, baseRel);
2769
+ await mkdir(join9(baseAbs, "source"), { recursive: true });
2770
+ await writeRouteFile(
2771
+ join9(baseAbs, "source", "route.ts"),
2772
+ join9(baseRel, "source", "route.ts"),
2773
+ DEV_SOURCE_ROUTE_TS,
2774
+ opts
2775
+ );
2776
+ await mkdir(join9(baseAbs, "screenshots"), { recursive: true });
2777
+ await writeRouteFile(
2778
+ join9(baseAbs, "screenshots", "route.ts"),
2779
+ join9(baseRel, "screenshots", "route.ts"),
2780
+ SCREENSHOT_ROUTE_TS,
2781
+ opts
2782
+ );
2783
+ }
2784
+
2785
+ // src/utils/prettier.ts
2786
+ import { existsSync as existsSync10 } from "fs";
2787
+ import { spawn as spawn2 } from "child_process";
2788
+ import { join as join10, dirname as dirname3 } from "path";
2789
+ function getPrettierPath(projectPath) {
2790
+ const localPath = join10(projectPath, "node_modules", ".bin", "prettier");
2791
+ if (existsSync10(localPath)) return localPath;
2792
+ let dir = projectPath;
2793
+ for (let i = 0; i < 10; i++) {
2794
+ const binPath = join10(dir, "node_modules", ".bin", "prettier");
2795
+ if (existsSync10(binPath)) return binPath;
2796
+ const parent = dirname3(dir);
2797
+ if (parent === dir) break;
2798
+ dir = parent;
2799
+ }
2800
+ return null;
2801
+ }
2802
+ function getPmRunner(pm) {
2803
+ switch (pm) {
2804
+ case "pnpm":
2805
+ return { command: "pnpm", args: ["exec"] };
2806
+ case "yarn":
2807
+ return { command: "yarn", args: [] };
2808
+ case "bun":
2809
+ return { command: "bunx", args: [] };
2810
+ case "npm":
2811
+ default:
2812
+ return { command: "npx", args: [] };
2813
+ }
2814
+ }
2815
+ async function formatWithPrettier(filePath, projectPath) {
2816
+ const prettierPath = getPrettierPath(projectPath);
2817
+ if (!prettierPath) {
2818
+ const pm = detectPackageManager(projectPath);
2819
+ const runner = getPmRunner(pm);
2820
+ return new Promise((resolve) => {
2821
+ const args = [...runner.args, "prettier", "--write", filePath];
2822
+ const child = spawn2(runner.command, args, {
2823
+ cwd: projectPath,
2824
+ stdio: "pipe",
2825
+ shell: process.platform === "win32"
2826
+ });
2827
+ let stderr = "";
2828
+ child.stderr?.on("data", (data) => {
2829
+ stderr += data.toString();
2830
+ });
2831
+ child.on("error", () => {
2832
+ resolve({ formatted: false, error: "prettier not available" });
2833
+ });
2834
+ child.on("close", (code) => {
2835
+ if (code === 0) {
2836
+ resolve({ formatted: true });
2837
+ } else {
2838
+ resolve({ formatted: false, error: stderr || "prettier failed" });
2839
+ }
2840
+ });
2841
+ });
2842
+ }
2843
+ return new Promise((resolve) => {
2844
+ const child = spawn2(prettierPath, ["--write", filePath], {
2845
+ cwd: projectPath,
2846
+ stdio: "pipe",
2847
+ shell: process.platform === "win32"
2848
+ });
2849
+ let stderr = "";
2850
+ child.stderr?.on("data", (data) => {
2851
+ stderr += data.toString();
2852
+ });
2853
+ child.on("error", (err) => {
2854
+ resolve({ formatted: false, error: err.message });
2855
+ });
2856
+ child.on("close", (code) => {
2857
+ if (code === 0) {
2858
+ resolve({ formatted: true });
2859
+ } else {
2860
+ resolve({ formatted: false, error: stderr || `exit code ${code}` });
2861
+ }
2862
+ });
2863
+ });
2864
+ }
2865
+ async function formatFilesWithPrettier(filePaths, projectPath) {
2866
+ if (filePaths.length === 0) {
2867
+ return { formatted: [], failed: [] };
2868
+ }
2869
+ const prettierPath = getPrettierPath(projectPath);
2870
+ const formatted = [];
2871
+ const failed = [];
2872
+ if (!prettierPath) {
2873
+ const pm = detectPackageManager(projectPath);
2874
+ const runner = getPmRunner(pm);
2875
+ return new Promise((resolve) => {
2876
+ const args = [...runner.args, "prettier", "--write", ...filePaths];
2877
+ const child = spawn2(runner.command, args, {
2878
+ cwd: projectPath,
2879
+ stdio: "pipe",
2880
+ shell: process.platform === "win32"
2881
+ });
2882
+ child.on("error", () => {
2883
+ resolve({ formatted: [], failed: filePaths });
2884
+ });
2885
+ child.on("close", (code) => {
2886
+ if (code === 0) {
2887
+ resolve({ formatted: filePaths, failed: [] });
2888
+ } else {
2889
+ resolve({ formatted: [], failed: filePaths });
2890
+ }
2891
+ });
2892
+ });
2893
+ }
2894
+ return new Promise((resolve) => {
2895
+ const child = spawn2(prettierPath, ["--write", ...filePaths], {
2896
+ cwd: projectPath,
2897
+ stdio: "pipe",
2898
+ shell: process.platform === "win32"
2899
+ });
2900
+ child.on("error", () => {
2901
+ resolve({ formatted: [], failed: filePaths });
2902
+ });
2903
+ child.on("close", (code) => {
2904
+ if (code === 0) {
2905
+ resolve({ formatted: filePaths, failed: [] });
2906
+ } else {
2907
+ Promise.all(
2908
+ filePaths.map(async (fp) => {
2909
+ const result = await formatWithPrettier(fp, projectPath);
2910
+ if (result.formatted) {
2911
+ formatted.push(fp);
2912
+ } else {
2913
+ failed.push(fp);
2914
+ }
2915
+ })
2916
+ ).then(() => {
2917
+ resolve({ formatted, failed });
2918
+ });
2919
+ }
2920
+ });
2921
+ });
2922
+ }
2923
+
2924
+ // src/commands/install/execute.ts
2925
+ async function executeAction(action, options) {
2926
+ const { dryRun = false } = options;
2927
+ try {
2928
+ switch (action.type) {
2929
+ case "create_directory": {
2930
+ if (dryRun) {
2931
+ return {
2932
+ action,
2933
+ success: true,
2934
+ wouldDo: `Create directory: ${action.path}`
2935
+ };
2936
+ }
2937
+ if (!existsSync11(action.path)) {
2938
+ mkdirSync(action.path, { recursive: true });
2939
+ }
2940
+ return { action, success: true };
2941
+ }
2942
+ case "create_file": {
2943
+ if (dryRun) {
2944
+ return {
2945
+ action,
2946
+ success: true,
2947
+ wouldDo: `Create file: ${action.path}${action.permissions ? ` (mode: ${action.permissions.toString(8)})` : ""}`
2948
+ };
2949
+ }
2950
+ const dir = dirname4(action.path);
2951
+ if (!existsSync11(dir)) {
2952
+ mkdirSync(dir, { recursive: true });
2953
+ }
2954
+ writeFileSync5(action.path, action.content, "utf-8");
2955
+ if (action.permissions) {
2956
+ chmodSync(action.path, action.permissions);
2957
+ }
2958
+ return { action, success: true };
2959
+ }
2960
+ case "merge_json": {
2961
+ if (dryRun) {
2962
+ return {
2963
+ action,
2964
+ success: true,
2965
+ wouldDo: `Merge JSON into: ${action.path}`
2966
+ };
2967
+ }
2968
+ let existing = {};
2969
+ if (existsSync11(action.path)) {
2970
+ try {
2971
+ existing = JSON.parse(readFileSync8(action.path, "utf-8"));
2972
+ } catch {
2973
+ }
2974
+ }
2975
+ const merged = deepMerge(existing, action.merge);
2976
+ const dir = dirname4(action.path);
2977
+ if (!existsSync11(dir)) {
2978
+ mkdirSync(dir, { recursive: true });
2979
+ }
2980
+ writeFileSync5(action.path, JSON.stringify(merged, null, 2), "utf-8");
2981
+ return { action, success: true };
2982
+ }
2983
+ case "delete_file": {
2984
+ if (dryRun) {
2985
+ return {
2986
+ action,
2987
+ success: true,
2988
+ wouldDo: `Delete file: ${action.path}`
2989
+ };
2990
+ }
2991
+ if (existsSync11(action.path)) {
2992
+ unlinkSync(action.path);
2993
+ }
2994
+ return { action, success: true };
2995
+ }
2996
+ case "append_to_file": {
2997
+ if (dryRun) {
2998
+ return {
2999
+ action,
3000
+ success: true,
3001
+ wouldDo: `Append to file: ${action.path}`
3002
+ };
3003
+ }
3004
+ if (existsSync11(action.path)) {
3005
+ const content = readFileSync8(action.path, "utf-8");
3006
+ if (action.ifNotContains && content.includes(action.ifNotContains)) {
3007
+ return { action, success: true };
3008
+ }
3009
+ writeFileSync5(action.path, content + action.content, "utf-8");
3010
+ }
3011
+ return { action, success: true };
3012
+ }
3013
+ case "inject_eslint": {
3014
+ return await executeInjectEslint(action, options);
3015
+ }
3016
+ case "inject_react": {
3017
+ return await executeInjectReact(action, options);
3018
+ }
3019
+ case "inject_next_config": {
3020
+ return await executeInjectNextConfig(action, options);
3021
+ }
3022
+ case "inject_vite_config": {
3023
+ return await executeInjectViteConfig(action, options);
3024
+ }
3025
+ case "install_next_routes": {
3026
+ return await executeInstallNextRoutes(action, options);
3027
+ }
3028
+ default: {
3029
+ const _exhaustive = action;
3030
+ return {
3031
+ action: _exhaustive,
3032
+ success: false,
3033
+ error: `Unknown action type`
3034
+ };
3035
+ }
3036
+ }
3037
+ } catch (error) {
3038
+ return {
3039
+ action,
3040
+ success: false,
3041
+ error: error instanceof Error ? error.message : String(error)
3042
+ };
3043
+ }
3044
+ }
3045
+ async function executeInjectEslint(action, options) {
3046
+ const { dryRun = false } = options;
3047
+ if (dryRun) {
3048
+ return {
3049
+ action,
3050
+ success: true,
3051
+ wouldDo: `Inject ESLint rules into: ${action.configPath}`
3052
+ };
3053
+ }
3054
+ const result = await installEslintPlugin({
3055
+ projectPath: action.packagePath,
3056
+ selectedRules: action.rules,
3057
+ force: !action.hasExistingRules,
3058
+ // Don't force if already has rules
3059
+ // Auto-confirm for execute phase (choices were made during planning)
3060
+ confirmAddMissingRules: async () => true
3061
+ });
3062
+ return {
3063
+ action,
3064
+ success: result.configFile !== null && result.configured,
3065
+ error: result.configFile === null ? "No ESLint config found" : result.configured ? void 0 : result.error ?? "Failed to configure uilint in ESLint config"
3066
+ };
3067
+ }
3068
+ async function executeInjectReact(action, options) {
3069
+ const { dryRun = false } = options;
3070
+ if (dryRun) {
3071
+ return {
3072
+ action,
3073
+ success: true,
3074
+ wouldDo: `Inject <uilint-devtools /> into React app: ${action.projectPath}`
3075
+ };
3076
+ }
3077
+ const result = await installReactUILintOverlay({
3078
+ projectPath: action.projectPath,
3079
+ appRoot: action.appRoot,
3080
+ mode: action.mode,
3081
+ force: false,
3082
+ // Auto-select first choice for execute phase
3083
+ confirmFileChoice: async (choices) => choices[0]
3084
+ });
3085
+ const success = result.modified || result.alreadyConfigured === true;
3086
+ return {
3087
+ action,
3088
+ success,
3089
+ error: success ? void 0 : "Failed to configure React overlay"
3090
+ };
3091
+ }
3092
+ async function executeInjectViteConfig(action, options) {
3093
+ const { dryRun = false } = options;
3094
+ if (dryRun) {
3095
+ return {
3096
+ action,
3097
+ success: true,
3098
+ wouldDo: `Inject jsx-loc-plugin into vite.config: ${action.projectPath}`
3099
+ };
3100
+ }
3101
+ const result = await installViteJsxLocPlugin({
3102
+ projectPath: action.projectPath,
3103
+ force: false
3104
+ });
3105
+ return {
3106
+ action,
3107
+ success: result.modified || result.configFile !== null,
3108
+ error: result.configFile === null ? "No vite.config found" : void 0
3109
+ };
3110
+ }
3111
+ async function executeInjectNextConfig(action, options) {
3112
+ const { dryRun = false } = options;
3113
+ if (dryRun) {
3114
+ return {
3115
+ action,
3116
+ success: true,
3117
+ wouldDo: `Inject jsx-loc-plugin into next.config: ${action.projectPath}`
3118
+ };
3119
+ }
3120
+ const result = await installJsxLocPlugin({
3121
+ projectPath: action.projectPath,
3122
+ force: false
3123
+ });
3124
+ return {
3125
+ action,
3126
+ success: result.modified || result.configFile !== null,
3127
+ error: result.configFile === null ? "No next.config found" : void 0
3128
+ };
3129
+ }
3130
+ async function executeInstallNextRoutes(action, options) {
3131
+ const { dryRun = false } = options;
3132
+ if (dryRun) {
3133
+ return {
3134
+ action,
3135
+ success: true,
3136
+ wouldDo: `Install Next.js API routes: ${action.projectPath}`
3137
+ };
3138
+ }
3139
+ await installNextUILintRoutes({
3140
+ projectPath: action.projectPath,
3141
+ appRoot: action.appRoot,
3142
+ force: false
3143
+ });
3144
+ return { action, success: true };
3145
+ }
3146
+ function deepMerge(target, source) {
3147
+ const result = { ...target };
3148
+ for (const key of Object.keys(source)) {
3149
+ const sourceVal = source[key];
3150
+ const targetVal = target[key];
3151
+ if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal) && targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)) {
3152
+ result[key] = deepMerge(
3153
+ targetVal,
3154
+ sourceVal
3155
+ );
3156
+ } else {
3157
+ result[key] = sourceVal;
3158
+ }
3159
+ }
3160
+ return result;
3161
+ }
3162
+ function buildSummary(actionsPerformed, dependencyResults, items) {
3163
+ const filesCreated = [];
3164
+ const filesModified = [];
3165
+ const filesDeleted = [];
3166
+ const eslintTargets = [];
3167
+ let nextApp;
3168
+ let viteApp;
3169
+ for (const result of actionsPerformed) {
3170
+ if (!result.success) continue;
3171
+ const { action } = result;
3172
+ switch (action.type) {
3173
+ case "create_file":
3174
+ filesCreated.push(action.path);
3175
+ break;
3176
+ case "merge_json":
3177
+ case "append_to_file":
3178
+ filesModified.push(action.path);
3179
+ break;
3180
+ case "delete_file":
3181
+ filesDeleted.push(action.path);
3182
+ break;
3183
+ case "inject_eslint":
3184
+ filesModified.push(action.configPath);
3185
+ eslintTargets.push({
3186
+ displayName: action.packagePath,
3187
+ configFile: action.configPath
3188
+ });
3189
+ break;
3190
+ case "inject_react":
3191
+ if (action.mode === "vite") {
3192
+ viteApp = { entryRoot: action.appRoot };
3193
+ } else {
3194
+ nextApp = { appRoot: action.appRoot };
3195
+ }
3196
+ break;
3197
+ case "install_next_routes":
3198
+ nextApp = { appRoot: action.appRoot };
3199
+ break;
3200
+ }
3201
+ }
3202
+ const dependenciesInstalled = [];
3203
+ for (const result of dependencyResults) {
3204
+ if (result.success && !result.skipped) {
3205
+ dependenciesInstalled.push({
3206
+ packagePath: result.install.packagePath,
3207
+ packages: result.install.packages
3208
+ });
3209
+ }
3210
+ }
3211
+ return {
3212
+ installedItems: items,
3213
+ filesCreated,
3214
+ filesModified,
3215
+ filesDeleted,
3216
+ dependenciesInstalled,
3217
+ eslintTargets,
3218
+ nextApp,
3219
+ viteApp
3220
+ };
3221
+ }
3222
+ function collectFormattableFiles(actionsPerformed) {
3223
+ const formattableExtensions = /* @__PURE__ */ new Set([
3224
+ ".ts",
3225
+ ".tsx",
3226
+ ".js",
3227
+ ".jsx",
3228
+ ".mjs",
3229
+ ".cjs",
3230
+ ".json"
3231
+ ]);
3232
+ const files = [];
3233
+ for (const result of actionsPerformed) {
3234
+ if (!result.success) continue;
3235
+ const { action } = result;
3236
+ let filePath;
3237
+ switch (action.type) {
3238
+ case "create_file":
3239
+ filePath = action.path;
3240
+ break;
3241
+ case "merge_json":
3242
+ filePath = action.path;
3243
+ break;
3244
+ case "append_to_file":
3245
+ filePath = action.path;
3246
+ break;
3247
+ case "inject_eslint":
3248
+ filePath = action.configPath;
3249
+ break;
3250
+ case "inject_next_config":
3251
+ case "inject_vite_config":
3252
+ case "inject_react":
3253
+ break;
3254
+ }
3255
+ if (filePath) {
3256
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
3257
+ if (formattableExtensions.has(ext)) {
3258
+ files.push(filePath);
3259
+ }
3260
+ }
3261
+ }
3262
+ return files;
3263
+ }
3264
+ function getProjectPathFromActions(actionsPerformed) {
3265
+ for (const result of actionsPerformed) {
3266
+ if (!result.success) continue;
3267
+ const { action } = result;
3268
+ switch (action.type) {
3269
+ case "inject_eslint":
3270
+ return action.packagePath;
3271
+ case "inject_react":
3272
+ case "inject_next_config":
3273
+ case "inject_vite_config":
3274
+ case "install_next_routes":
3275
+ return action.projectPath;
3276
+ }
3277
+ }
3278
+ return void 0;
3279
+ }
3280
+ async function execute(plan, options = {}) {
3281
+ const {
3282
+ dryRun = false,
3283
+ installDependencies: installDependencies2 = installDependencies,
3284
+ projectPath,
3285
+ skipPrettier = false
3286
+ } = options;
3287
+ const actionsPerformed = [];
3288
+ const dependencyResults = [];
3289
+ for (const action of plan.actions) {
3290
+ const result = await executeAction(action, options);
3291
+ actionsPerformed.push(result);
3292
+ }
3293
+ for (const dep of plan.dependencies) {
3294
+ if (dryRun) {
3295
+ dependencyResults.push({
3296
+ install: dep,
3297
+ success: true,
3298
+ skipped: true
3299
+ });
3300
+ continue;
3301
+ }
3302
+ try {
3303
+ await installDependencies2(
3304
+ dep.packageManager,
3305
+ dep.packagePath,
3306
+ dep.packages
3307
+ );
3308
+ dependencyResults.push({
3309
+ install: dep,
3310
+ success: true
3311
+ });
3312
+ } catch (error) {
3313
+ dependencyResults.push({
3314
+ install: dep,
3315
+ success: false,
3316
+ error: error instanceof Error ? error.message : String(error)
3317
+ });
3318
+ }
3319
+ }
3320
+ if (!dryRun && !skipPrettier) {
3321
+ const filesToFormat = collectFormattableFiles(actionsPerformed);
3322
+ if (filesToFormat.length > 0) {
3323
+ const formatProjectPath = projectPath || plan.dependencies[0]?.packagePath || getProjectPathFromActions(actionsPerformed);
3324
+ if (formatProjectPath) {
3325
+ await formatFilesWithPrettier(filesToFormat, formatProjectPath).catch(
3326
+ () => {
3327
+ }
3328
+ );
3329
+ }
3330
+ }
3331
+ }
3332
+ const actionsFailed = actionsPerformed.filter((r) => !r.success);
3333
+ const depsFailed = dependencyResults.filter((r) => !r.success);
3334
+ const success = actionsFailed.length === 0 && depsFailed.length === 0;
3335
+ const items = [];
3336
+ for (const result of actionsPerformed) {
3337
+ if (!result.success) continue;
3338
+ const { action } = result;
3339
+ if (action.type === "create_file") {
3340
+ if (action.path.includes("genstyleguide.md")) items.push("genstyleguide");
3341
+ if (action.path.includes("/skills/") && action.path.includes("SKILL.md")) items.push("skill");
3342
+ }
3343
+ if (action.type === "inject_eslint") items.push("eslint");
3344
+ if (action.type === "install_next_routes") items.push("next");
3345
+ if (action.type === "inject_react") {
3346
+ items.push(action.mode === "vite" ? "vite" : "next");
3347
+ }
3348
+ if (action.type === "inject_vite_config") items.push("vite");
3349
+ }
3350
+ const uniqueItems = [...new Set(items)];
3351
+ const summary = buildSummary(
3352
+ actionsPerformed,
3353
+ dependencyResults,
3354
+ uniqueItems
3355
+ );
3356
+ return {
3357
+ success,
3358
+ actionsPerformed,
3359
+ dependencyResults,
3360
+ summary
3361
+ };
3362
+ }
3363
+
3364
+ // src/commands/install-ui.tsx
3365
+ import { ruleRegistry as ruleRegistry3 } from "uilint-eslint";
3366
+
3367
+ // src/commands/install/installers/genstyleguide.ts
3368
+ import { join as join11 } from "path";
3369
+ var genstyleguideInstaller = {
3370
+ id: "genstyleguide",
3371
+ name: "/genstyleguide command",
3372
+ description: "Cursor command to generate UI style guides",
3373
+ icon: "\u{1F4DD}",
3374
+ isApplicable(project) {
3375
+ return true;
3376
+ },
3377
+ getTargets(project) {
3378
+ const commandPath = join11(project.cursorDir.path, "commands", "genstyleguide.md");
3379
+ const isInstalled = project.commands.genstyleguide;
3380
+ return [
3381
+ {
3382
+ id: "genstyleguide-command",
3383
+ label: ".cursor/commands/genstyleguide.md",
3384
+ path: commandPath,
3385
+ isInstalled
3386
+ }
3387
+ ];
3388
+ },
3389
+ plan(targets, config, project) {
3390
+ const actions = [];
3391
+ const commandsDir = join11(project.cursorDir.path, "commands");
3392
+ if (!project.cursorDir.exists) {
3393
+ actions.push({
3394
+ type: "create_directory",
3395
+ path: project.cursorDir.path
3396
+ });
3397
+ }
3398
+ actions.push({
3399
+ type: "create_directory",
3400
+ path: commandsDir
3401
+ });
3402
+ actions.push({
3403
+ type: "create_file",
3404
+ path: join11(commandsDir, "genstyleguide.md"),
3405
+ content: GENSTYLEGUIDE_COMMAND_MD
3406
+ });
3407
+ return {
3408
+ actions,
3409
+ dependencies: []
3410
+ };
3411
+ },
3412
+ async *execute(targets, config, project) {
3413
+ yield {
3414
+ type: "start",
3415
+ message: "Installing /genstyleguide command"
3416
+ };
3417
+ yield {
3418
+ type: "progress",
3419
+ message: "Creating .cursor/commands directory"
3420
+ };
3421
+ yield {
3422
+ type: "progress",
3423
+ message: "Writing command file",
3424
+ detail: "\u2192 .cursor/commands/genstyleguide.md"
3425
+ };
3426
+ yield {
3427
+ type: "complete",
3428
+ message: "Installed /genstyleguide command"
3429
+ };
3430
+ }
3431
+ };
3432
+
3433
+ // src/commands/install/installers/skill.ts
3434
+ import { existsSync as existsSync12 } from "fs";
3435
+ import { join as join12 } from "path";
3436
+ var skillInstaller = {
3437
+ id: "skill",
3438
+ name: "UI Consistency Agent skill",
3439
+ description: "Claude Code skill for enforcing UI consistency",
3440
+ icon: "\u26A1",
3441
+ isApplicable(project) {
3442
+ return true;
3443
+ },
3444
+ getTargets(project) {
3445
+ const skillsDir = join12(project.cursorDir.path, "skills", "ui-consistency-enforcer");
3446
+ const skillMdPath = join12(skillsDir, "SKILL.md");
3447
+ const isInstalled = existsSync12(skillMdPath);
3448
+ return [
3449
+ {
3450
+ id: "ui-consistency-skill",
3451
+ label: ".cursor/skills/ui-consistency-enforcer",
3452
+ path: skillsDir,
3453
+ isInstalled,
3454
+ hint: "Agent skill for UI consistency checks"
3455
+ }
3456
+ ];
3457
+ },
3458
+ plan(targets, config, project) {
3459
+ const actions = [];
3460
+ if (!project.cursorDir.exists) {
3461
+ actions.push({
3462
+ type: "create_directory",
3463
+ path: project.cursorDir.path
3464
+ });
3465
+ }
3466
+ const skillsDir = join12(project.cursorDir.path, "skills");
3467
+ actions.push({
3468
+ type: "create_directory",
3469
+ path: skillsDir
3470
+ });
3471
+ try {
3472
+ const skill = loadSkill("ui-consistency-enforcer");
3473
+ const skillDir = join12(skillsDir, skill.name);
3474
+ actions.push({
3475
+ type: "create_directory",
3476
+ path: skillDir
3477
+ });
3478
+ for (const file of skill.files) {
3479
+ const filePath = join12(skillDir, file.relativePath);
3480
+ const fileDir = join12(
3481
+ skillDir,
3482
+ file.relativePath.split("/").slice(0, -1).join("/")
3483
+ );
3484
+ if (fileDir !== skillDir && file.relativePath.includes("/")) {
3485
+ actions.push({
3486
+ type: "create_directory",
3487
+ path: fileDir
3488
+ });
3489
+ }
3490
+ actions.push({
3491
+ type: "create_file",
3492
+ path: filePath,
3493
+ content: file.content
3494
+ });
3495
+ }
3496
+ } catch (error) {
3497
+ console.warn("Failed to load ui-consistency-enforcer skill:", error);
3498
+ }
3499
+ return {
3500
+ actions,
3501
+ dependencies: []
3502
+ };
3503
+ },
3504
+ async *execute(targets, config, project) {
3505
+ yield {
3506
+ type: "start",
3507
+ message: "Installing UI Consistency Agent skill"
3508
+ };
3509
+ yield {
3510
+ type: "progress",
3511
+ message: "Creating .cursor/skills directory"
3512
+ };
3513
+ try {
3514
+ const skill = loadSkill("ui-consistency-enforcer");
3515
+ for (const file of skill.files) {
3516
+ yield {
3517
+ type: "progress",
3518
+ message: "Writing skill file",
3519
+ detail: `\u2192 ${file.relativePath}`
3520
+ };
3521
+ }
3522
+ yield {
3523
+ type: "complete",
3524
+ message: "Installed UI Consistency Agent skill"
3525
+ };
3526
+ } catch (error) {
3527
+ yield {
3528
+ type: "error",
3529
+ message: "Failed to install skill",
3530
+ error: error instanceof Error ? error.message : String(error)
3531
+ };
3532
+ }
3533
+ }
3534
+ };
3535
+
3536
+ // src/commands/install/installers/eslint.ts
3537
+ import { join as join13 } from "path";
3538
+ import { ruleRegistry as ruleRegistry2, getRulesByCategory as getRulesByCategory2 } from "uilint-eslint";
3539
+ function toInstallSpecifier(pkgName) {
3540
+ return pkgName;
3541
+ }
3542
+ function formatRuleOption(rule) {
3543
+ const severityBadge = rule.defaultSeverity === "error" ? pc.red("error") : pc.yellow("warn");
3544
+ return {
3545
+ value: rule.id,
3546
+ label: `${rule.name}`,
3547
+ hint: `${rule.description} [${severityBadge}]`
3548
+ };
3549
+ }
3550
+ var eslintInstaller = {
3551
+ id: "eslint",
3552
+ name: "ESLint plugin",
3553
+ description: "Lint UI consistency with ESLint rules",
3554
+ icon: "\u{1F50D}",
3555
+ isApplicable(project) {
3556
+ return project.packages.some((pkg) => pkg.eslintConfigPath !== null);
3557
+ },
3558
+ getTargets(project) {
3559
+ return project.packages.filter((pkg) => pkg.eslintConfigPath !== null).map((pkg) => ({
3560
+ id: `eslint-${pkg.name}`,
3561
+ label: pkg.name,
3562
+ path: pkg.path,
3563
+ hint: pkg.eslintConfigFilename || "ESLint config detected",
3564
+ isInstalled: pkg.hasUilintRules
3565
+ }));
3566
+ },
3567
+ async configure(targets, project) {
3568
+ const staticRules = getRulesByCategory2("static");
3569
+ const semanticRules = getRulesByCategory2("semantic");
3570
+ const installMode = await select({
3571
+ message: "How would you like to configure UILint ESLint rules?",
3572
+ options: [
3573
+ {
3574
+ value: "recommended",
3575
+ label: "Recommended (static rules only)",
3576
+ hint: "Best for most projects - fast and reliable"
3577
+ },
3578
+ {
3579
+ value: "strict",
3580
+ label: "Strict (all rules including semantic)",
3581
+ hint: "Includes LLM-powered analysis - requires Ollama"
3582
+ },
3583
+ {
3584
+ value: "custom",
3585
+ label: "Custom selection",
3586
+ hint: "Choose individual rules and configure severity"
3587
+ }
3588
+ ]
3589
+ });
3590
+ let selectedRuleIds;
3591
+ let configuredRules;
3592
+ if (installMode === "recommended") {
3593
+ configuredRules = staticRules.map((rule) => ({
3594
+ rule,
3595
+ severity: rule.defaultSeverity,
3596
+ options: rule.defaultOptions
3597
+ }));
3598
+ } else if (installMode === "strict") {
3599
+ configuredRules = ruleRegistry2.map((rule) => ({
3600
+ rule,
3601
+ severity: rule.defaultSeverity,
3602
+ options: rule.defaultOptions
3603
+ }));
3604
+ } else {
3605
+ log(pc.dim("\n\u{1F4CB} Static rules (pattern-based, fast):"));
3606
+ const selectedStaticIds = await multiselect({
3607
+ message: "Select static rules to enable:",
3608
+ options: staticRules.map(formatRuleOption),
3609
+ initialValues: staticRules.map((r) => r.id)
3610
+ });
3611
+ const includeSemanticRules = await confirm({
3612
+ message: `Include semantic rules? ${pc.dim(
3613
+ "(requires Ollama for LLM analysis)"
3614
+ )}`,
3615
+ initialValue: false
3616
+ });
3617
+ let selectedSemanticIds = [];
3618
+ if (includeSemanticRules) {
3619
+ log(
3620
+ pc.dim("\n\u{1F9E0} Semantic rules (LLM-powered, slower):")
3621
+ );
3622
+ selectedSemanticIds = await multiselect({
3623
+ message: "Select semantic rules:",
3624
+ options: semanticRules.map(formatRuleOption),
3625
+ initialValues: semanticRules.map((r) => r.id)
3626
+ });
3627
+ }
3628
+ selectedRuleIds = [...selectedStaticIds, ...selectedSemanticIds];
3629
+ const configureSeverity = await confirm({
3630
+ message: "Would you like to customize severity levels?",
3631
+ initialValue: false
3632
+ });
3633
+ configuredRules = [];
3634
+ for (const ruleId of selectedRuleIds) {
3635
+ const rule = ruleRegistry2.find((r) => r.id === ruleId);
3636
+ if (configureSeverity) {
3637
+ const severity = await select({
3638
+ message: `${rule.name} - severity:`,
3639
+ options: [
3640
+ {
3641
+ value: rule.defaultSeverity,
3642
+ label: `${rule.defaultSeverity} (default)`
3643
+ },
3644
+ ...rule.defaultSeverity !== "error" ? [{ value: "error", label: "error" }] : [],
3645
+ ...rule.defaultSeverity !== "warn" ? [{ value: "warn", label: "warn" }] : []
3646
+ ],
3647
+ initialValue: rule.defaultSeverity
3648
+ });
3649
+ configuredRules.push({
3650
+ rule,
3651
+ severity,
3652
+ options: rule.defaultOptions
3653
+ });
3654
+ } else {
3655
+ configuredRules.push({
3656
+ rule,
3657
+ severity: rule.defaultSeverity,
3658
+ options: rule.defaultOptions
3659
+ });
3660
+ }
3661
+ }
3662
+ }
3663
+ const errorCount = configuredRules.filter(
3664
+ (r) => r.severity === "error"
3665
+ ).length;
3666
+ const warnCount = configuredRules.filter(
3667
+ (r) => r.severity === "warn"
3668
+ ).length;
3669
+ log("");
3670
+ note(
3671
+ configuredRules.map(
3672
+ (cr) => `${cr.severity === "error" ? "\u{1F534}" : "\u{1F7E1}"} ${cr.rule.name} (${cr.severity})`
3673
+ ).join("\n"),
3674
+ `Selected ${configuredRules.length} rules (${errorCount} errors, ${warnCount} warnings)`
3675
+ );
3676
+ return { configuredRules };
3677
+ },
3678
+ plan(targets, config, project) {
3679
+ const actions = [];
3680
+ const dependencies = [];
3681
+ const eslintConfig = config;
3682
+ const { configuredRules } = eslintConfig;
3683
+ const rulesWithSeverity = configuredRules.map((cr) => ({
3684
+ ...cr.rule,
3685
+ defaultSeverity: cr.severity,
3686
+ defaultOptions: cr.options
3687
+ }));
3688
+ for (const target of targets) {
3689
+ const pkgInfo = project.packages.find((p) => p.path === target.path);
3690
+ if (!pkgInfo || !pkgInfo.eslintConfigPath) continue;
3691
+ const rulesDir = join13(target.path, ".uilint", "rules");
3692
+ actions.push({
3693
+ type: "create_directory",
3694
+ path: rulesDir
3695
+ });
3696
+ dependencies.push({
3697
+ packagePath: target.path,
3698
+ packageManager: project.packageManager,
3699
+ packages: [toInstallSpecifier("uilint-eslint"), "typescript-eslint"]
3700
+ });
3701
+ actions.push({
3702
+ type: "inject_eslint",
3703
+ packagePath: target.path,
3704
+ configPath: pkgInfo.eslintConfigPath,
3705
+ rules: rulesWithSeverity,
3706
+ hasExistingRules: pkgInfo.hasUilintRules
3707
+ });
3708
+ }
3709
+ const gitignorePath = join13(project.workspaceRoot, ".gitignore");
3710
+ actions.push({
3711
+ type: "append_to_file",
3712
+ path: gitignorePath,
3713
+ content: "\n# UILint cache\n.uilint/.cache\n",
3714
+ ifNotContains: ".uilint/.cache"
3715
+ });
3716
+ return { actions, dependencies };
3717
+ },
3718
+ async *execute(targets, config, project) {
3719
+ const eslintConfig = config;
3720
+ yield {
3721
+ type: "start",
3722
+ message: "Installing ESLint plugin"
3723
+ };
3724
+ for (const target of targets) {
3725
+ yield {
3726
+ type: "progress",
3727
+ message: `Configuring ESLint in ${target.label}`,
3728
+ detail: "\u2192 Adding uilint-eslint to dependencies"
3729
+ };
3730
+ yield {
3731
+ type: "progress",
3732
+ message: `Injecting ${eslintConfig.configuredRules.length} rules`,
3733
+ detail: `\u2192 ${target.hint}`
3734
+ };
3735
+ }
3736
+ yield {
3737
+ type: "complete",
3738
+ message: `ESLint plugin installed in ${targets.length} package(s)`
3739
+ };
3740
+ }
3741
+ };
3742
+
3743
+ // src/commands/install/installers/next-overlay.ts
3744
+ var nextOverlayInstaller = {
3745
+ id: "next",
3746
+ name: "Next.js overlay",
3747
+ description: "Alt+Click UI inspector for Next.js App Router",
3748
+ icon: "\u{1F537}",
3749
+ isApplicable(project) {
3750
+ return project.nextApps.length > 0;
3751
+ },
3752
+ getTargets(project) {
3753
+ return project.nextApps.map((app) => ({
3754
+ id: `next-${app.projectPath}`,
3755
+ label: app.projectPath.split("/").pop() || app.projectPath,
3756
+ path: app.projectPath,
3757
+ hint: "App Router",
3758
+ isInstalled: false
3759
+ // TODO: Detect if already installed
3760
+ }));
3761
+ },
3762
+ plan(targets, config, project) {
3763
+ const actions = [];
3764
+ const dependencies = [];
3765
+ if (targets.length === 0) return { actions, dependencies };
3766
+ const target = targets[0];
3767
+ const appInfo = project.nextApps.find((app) => app.projectPath === target.path);
3768
+ if (!appInfo) return { actions, dependencies };
3769
+ const { projectPath, detection } = appInfo;
3770
+ actions.push({
3771
+ type: "install_next_routes",
3772
+ projectPath,
3773
+ appRoot: detection.appRoot
3774
+ });
3775
+ dependencies.push({
3776
+ packagePath: projectPath,
3777
+ packageManager: project.packageManager,
3778
+ packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
3779
+ });
3780
+ actions.push({
3781
+ type: "inject_react",
3782
+ projectPath,
3783
+ appRoot: detection.appRoot,
3784
+ mode: "next"
3785
+ });
3786
+ actions.push({
3787
+ type: "inject_next_config",
3788
+ projectPath
3789
+ });
3790
+ return { actions, dependencies };
3791
+ },
3792
+ async *execute(targets, config, project) {
3793
+ if (targets.length === 0) return;
3794
+ const target = targets[0];
3795
+ yield {
3796
+ type: "start",
3797
+ message: "Installing Next.js overlay"
3798
+ };
3799
+ yield {
3800
+ type: "progress",
3801
+ message: `Installing in ${target.label}`,
3802
+ detail: "\u2192 Adding API routes"
3803
+ };
3804
+ yield {
3805
+ type: "progress",
3806
+ message: "Installing dependencies",
3807
+ detail: "\u2192 uilint-react, uilint-core, jsx-loc-plugin"
3808
+ };
3809
+ yield {
3810
+ type: "progress",
3811
+ message: "Injecting devtools component",
3812
+ detail: "\u2192 <uilint-devtools /> in root layout"
3813
+ };
3814
+ yield {
3815
+ type: "progress",
3816
+ message: "Configuring jsx-loc-plugin",
3817
+ detail: "\u2192 next.config.js"
3818
+ };
3819
+ yield {
3820
+ type: "complete",
3821
+ message: "Next.js overlay installed"
3822
+ };
3823
+ }
3824
+ };
3825
+
3826
+ // src/commands/install/installers/vite-overlay.ts
3827
+ var viteOverlayInstaller = {
3828
+ id: "vite",
3829
+ name: "Vite overlay",
3830
+ description: "Alt+Click UI inspector for Vite + React apps",
3831
+ icon: "\u26A1",
3832
+ isApplicable(project) {
3833
+ return project.viteApps.length > 0;
3834
+ },
3835
+ getTargets(project) {
3836
+ return project.viteApps.map((app) => ({
3837
+ id: `vite-${app.projectPath}`,
3838
+ label: app.projectPath.split("/").pop() || app.projectPath,
3839
+ path: app.projectPath,
3840
+ hint: "React + Vite",
3841
+ isInstalled: false
3842
+ // TODO: Detect if already installed
3843
+ }));
3844
+ },
3845
+ plan(targets, config, project) {
3846
+ const actions = [];
3847
+ const dependencies = [];
3848
+ if (targets.length === 0) return { actions, dependencies };
3849
+ const target = targets[0];
3850
+ const appInfo = project.viteApps.find((app) => app.projectPath === target.path);
3851
+ if (!appInfo) return { actions, dependencies };
3852
+ const { projectPath, detection } = appInfo;
3853
+ dependencies.push({
3854
+ packagePath: projectPath,
3855
+ packageManager: project.packageManager,
3856
+ packages: ["uilint-react", "uilint-core", "jsx-loc-plugin"]
3857
+ });
3858
+ actions.push({
3859
+ type: "inject_react",
3860
+ projectPath,
3861
+ appRoot: detection.entryRoot,
3862
+ mode: "vite"
3863
+ });
3864
+ actions.push({
3865
+ type: "inject_vite_config",
3866
+ projectPath
3867
+ });
3868
+ return { actions, dependencies };
3869
+ },
3870
+ async *execute(targets, config, project) {
3871
+ if (targets.length === 0) return;
3872
+ const target = targets[0];
3873
+ yield {
3874
+ type: "start",
3875
+ message: "Installing Vite overlay"
3876
+ };
3877
+ yield {
3878
+ type: "progress",
3879
+ message: `Installing in ${target.label}`,
3880
+ detail: "\u2192 Adding dependencies"
3881
+ };
3882
+ yield {
3883
+ type: "progress",
3884
+ message: "Installing dependencies",
3885
+ detail: "\u2192 uilint-react, uilint-core, jsx-loc-plugin"
3886
+ };
3887
+ yield {
3888
+ type: "progress",
3889
+ message: "Injecting devtools component",
3890
+ detail: "\u2192 <uilint-devtools /> in entry file"
3891
+ };
3892
+ yield {
3893
+ type: "progress",
3894
+ message: "Configuring jsx-loc-plugin",
3895
+ detail: "\u2192 vite.config.ts"
3896
+ };
3897
+ yield {
3898
+ type: "complete",
3899
+ message: "Vite overlay installed"
3900
+ };
3901
+ }
3902
+ };
3903
+
3904
+ // src/commands/install/installers/index.ts
3905
+ registerInstaller(genstyleguideInstaller);
3906
+ registerInstaller(skillInstaller);
3907
+ registerInstaller(eslintInstaller);
3908
+ registerInstaller(nextOverlayInstaller);
3909
+ registerInstaller(viteOverlayInstaller);
3910
+
3911
+ // src/commands/install-ui.tsx
3912
+ import { jsx as jsx6 } from "react/jsx-runtime";
3913
+ function selectionsToUserChoices(selections, project, eslintRules) {
3914
+ const items = [];
3915
+ const choices = { items };
3916
+ for (const selection of selections) {
3917
+ if (!selection.selected || selection.targets.length === 0) continue;
3918
+ const { installer, targets } = selection;
3919
+ if (installer.id === "genstyleguide") {
3920
+ items.push("genstyleguide");
3921
+ } else if (installer.id === "skill") {
3922
+ items.push("skill");
3923
+ } else if (installer.id === "eslint") {
3924
+ items.push("eslint");
3925
+ choices.eslint = {
3926
+ packagePaths: targets.map((t) => t.path),
3927
+ // Use configured rules if provided, otherwise fall back to all rules
3928
+ selectedRules: eslintRules ? eslintRules.map((cr) => ({
3929
+ ...cr.rule,
3930
+ // Override severity with user's selection
3931
+ defaultSeverity: cr.severity,
3932
+ defaultOptions: cr.options
3933
+ })) : ruleRegistry3
3934
+ };
3935
+ } else if (installer.id === "next") {
3936
+ items.push("next");
3937
+ const target = targets[0];
3938
+ const appInfo = project.nextApps.find(
3939
+ (app) => app.projectPath === target?.path
3940
+ );
3941
+ if (appInfo) {
3942
+ choices.next = {
3943
+ projectPath: appInfo.projectPath,
3944
+ detection: appInfo.detection
3945
+ };
3946
+ }
3947
+ } else if (installer.id === "vite") {
3948
+ items.push("vite");
3949
+ const target = targets[0];
3950
+ const appInfo = project.viteApps.find(
3951
+ (app) => app.projectPath === target?.path
3952
+ );
3953
+ if (appInfo) {
3954
+ choices.vite = {
3955
+ projectPath: appInfo.projectPath,
3956
+ detection: appInfo.detection
3957
+ };
3958
+ }
3959
+ }
3960
+ }
3961
+ return choices;
3962
+ }
3963
+ function isInteractiveTerminal() {
3964
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
3965
+ }
3966
+ async function installUI(options = {}, executeOptions = {}) {
3967
+ const projectPath = process.cwd();
3968
+ if (!isInteractiveTerminal()) {
3969
+ console.error("\n\u2717 Interactive mode requires a TTY terminal.");
3970
+ console.error("Run uilint install in an interactive terminal.\n");
3971
+ process.exit(1);
3972
+ }
3973
+ const projectPromise = analyze(projectPath);
3974
+ const { waitUntilExit } = render(
3975
+ /* @__PURE__ */ jsx6(
3976
+ InstallApp,
3977
+ {
3978
+ projectPromise,
3979
+ onComplete: async (selections, eslintRules) => {
3980
+ const project = await projectPromise;
3981
+ const choices = selectionsToUserChoices(selections, project, eslintRules);
3982
+ if (choices.items.length === 0) {
3983
+ console.log("\nNo items selected for installation");
3984
+ process.exit(0);
3985
+ }
3986
+ const { createPlan } = await import("./plan-PX7FFJ25.js");
3987
+ const plan = createPlan(project, choices, { force: options.force });
3988
+ const result = await execute(plan, {
3989
+ ...executeOptions,
3990
+ projectPath: project.projectPath
3991
+ });
3992
+ if (result.success) {
3993
+ console.log("\n\u2713 Installation completed successfully!");
3994
+ } else {
3995
+ console.log("\n\u26A0 Installation completed with errors");
3996
+ }
3997
+ process.exit(result.success ? 0 : 1);
3998
+ },
3999
+ onError: (error) => {
4000
+ console.error("\n\u2717 Error:", error.message);
4001
+ process.exit(1);
4002
+ }
4003
+ }
4004
+ )
4005
+ );
4006
+ await waitUntilExit();
4007
+ }
4008
+ export {
4009
+ installUI
4010
+ };
4011
+ //# sourceMappingURL=install-ui-KI7YHOVZ.js.map