uilint 0.2.8 → 0.2.9

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