@inspecto-dev/cli 0.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1306 @@
1
+ // src/utils/logger.ts
2
+ import pc from "picocolors";
3
+ var log = {
4
+ /** Section header */
5
+ header(text) {
6
+ console.log();
7
+ console.log(` ${pc.bold(pc.cyan("\u2726"))} ${pc.bold(text)}`);
8
+ console.log();
9
+ },
10
+ /** Info item (used for actionable but not fully successful states) */
11
+ info(text) {
12
+ console.log(` ${pc.blue("\u2139")} ${text}`);
13
+ },
14
+ success(text) {
15
+ console.log(` ${pc.green("\u2714")} ${text}`);
16
+ },
17
+ /** Warning item */
18
+ warn(text) {
19
+ console.log(` ${pc.yellow("\u26A0")} ${text}`);
20
+ },
21
+ /** Error item */
22
+ error(text) {
23
+ console.log(` ${pc.red("\u2718")} ${text}`);
24
+ },
25
+ /** Indented hint line */
26
+ hint(text) {
27
+ console.log(` ${pc.dim("\u2192")} ${text}`);
28
+ },
29
+ /** Blank line */
30
+ blank() {
31
+ console.log();
32
+ },
33
+ /** Final ready message */
34
+ ready(text) {
35
+ console.log();
36
+ console.log(` ${pc.bold(pc.green("\u26A1"))} ${pc.bold(text)}`);
37
+ console.log();
38
+ },
39
+ /** Code block for manual instructions */
40
+ codeBlock(lines) {
41
+ console.log();
42
+ console.log(` ${pc.dim("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510")}`);
43
+ for (const line of lines) {
44
+ console.log(` ${pc.dim("\u2502")} ${line.padEnd(48)}${pc.dim("\u2502")}`);
45
+ }
46
+ console.log(` ${pc.dim("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518")}`);
47
+ console.log();
48
+ },
49
+ /** Dry-run prefix */
50
+ dryRun(text) {
51
+ console.log(` ${pc.blue("[dry-run]")} ${text}`);
52
+ }
53
+ };
54
+
55
+ // src/commands/init.ts
56
+ import path9 from "path";
57
+
58
+ // src/utils/fs.ts
59
+ import fs from "fs/promises";
60
+ import path from "path";
61
+ async function exists(filePath) {
62
+ try {
63
+ await fs.access(filePath);
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+ async function readFile(filePath) {
70
+ try {
71
+ return await fs.readFile(filePath, "utf-8");
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+ async function writeFile(filePath, content) {
77
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
78
+ await fs.writeFile(filePath, content, "utf-8");
79
+ }
80
+ async function copyFile(src, dest) {
81
+ await fs.copyFile(src, dest);
82
+ }
83
+ async function removeFile(filePath) {
84
+ try {
85
+ await fs.unlink(filePath);
86
+ } catch {
87
+ }
88
+ }
89
+ async function removeDir(dirPath) {
90
+ try {
91
+ await fs.rm(dirPath, { recursive: true, force: true });
92
+ } catch {
93
+ }
94
+ }
95
+ async function readJSON(filePath) {
96
+ const text = await readFile(filePath);
97
+ if (!text) return null;
98
+ try {
99
+ return JSON.parse(text);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ async function writeJSON(filePath, data) {
105
+ await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
106
+ }
107
+
108
+ // src/utils/exec.ts
109
+ import { execFile, exec as execCb } from "child_process";
110
+ import { promisify } from "util";
111
+ var execFileAsync = promisify(execFile);
112
+ var execAsync = promisify(execCb);
113
+ async function run(command, args, cwd) {
114
+ const result = await execFileAsync(command, args, {
115
+ cwd,
116
+ timeout: 6e4,
117
+ env: { ...process.env }
118
+ });
119
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
120
+ }
121
+ async function shell(command, cwd) {
122
+ const result = await execAsync(command, {
123
+ cwd,
124
+ timeout: 6e4,
125
+ env: { ...process.env }
126
+ });
127
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
128
+ }
129
+ async function which(bin) {
130
+ try {
131
+ const cmd = process.platform === "win32" ? "where" : "which";
132
+ await run(cmd, [bin]);
133
+ return true;
134
+ } catch {
135
+ return false;
136
+ }
137
+ }
138
+
139
+ // src/detect/package-manager.ts
140
+ import path2 from "path";
141
+ async function detectPackageManager(root) {
142
+ const checks = [
143
+ ["bun.lockb", "bun"],
144
+ ["bun.lock", "bun"],
145
+ ["pnpm-lock.yaml", "pnpm"],
146
+ ["yarn.lock", "yarn"],
147
+ ["package-lock.json", "npm"]
148
+ ];
149
+ for (const [file, pm] of checks) {
150
+ if (await exists(path2.join(root, file))) {
151
+ return pm;
152
+ }
153
+ }
154
+ return "npm";
155
+ }
156
+ function getInstallCommand(pm, pkg) {
157
+ switch (pm) {
158
+ case "bun":
159
+ return `bun add -D ${pkg}`;
160
+ case "pnpm":
161
+ return `pnpm add -D ${pkg}`;
162
+ case "yarn":
163
+ return `yarn add -D ${pkg}`;
164
+ case "npm":
165
+ return `npm install -D ${pkg}`;
166
+ }
167
+ }
168
+ function getUninstallCommand(pm, pkg) {
169
+ switch (pm) {
170
+ case "bun":
171
+ return `bun remove ${pkg}`;
172
+ case "pnpm":
173
+ return `pnpm remove ${pkg}`;
174
+ case "yarn":
175
+ return `yarn remove ${pkg}`;
176
+ case "npm":
177
+ return `npm uninstall ${pkg}`;
178
+ }
179
+ }
180
+
181
+ // src/detect/build-tool.ts
182
+ import path3 from "path";
183
+ var SUPPORTED_PATTERNS = [
184
+ {
185
+ tool: "vite",
186
+ files: ["vite.config.ts", "vite.config.js", "vite.config.mts", "vite.config.mjs"],
187
+ label: "Vite"
188
+ },
189
+ {
190
+ tool: "rspack",
191
+ files: ["rspack.config.js", "rspack.config.ts", "rspack.config.mjs"],
192
+ label: "Rspack"
193
+ },
194
+ {
195
+ tool: "rsbuild",
196
+ files: ["rsbuild.config.js", "rsbuild.config.ts", "rsbuild.config.mjs"],
197
+ label: "Rsbuild"
198
+ },
199
+ {
200
+ tool: "webpack",
201
+ files: ["webpack.config.js", "webpack.config.ts", "webpack.config.mjs", "webpack.config.cjs"],
202
+ label: "Webpack"
203
+ },
204
+ {
205
+ tool: "esbuild",
206
+ files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs"],
207
+ label: "esbuild"
208
+ },
209
+ {
210
+ tool: "rollup",
211
+ files: ["rollup.config.js", "rollup.config.ts", "rollup.config.mjs"],
212
+ label: "Rollup"
213
+ }
214
+ ];
215
+ var UNSUPPORTED_META = [
216
+ { name: "Next.js", dep: "next", files: ["next.config.mjs", "next.config.js", "next.config.ts"] },
217
+ { name: "Nuxt", dep: "nuxt", files: ["nuxt.config.ts", "nuxt.config.js"] },
218
+ { name: "Remix", dep: "@remix-run/dev", files: ["remix.config.js", "remix.config.ts"] },
219
+ { name: "Astro", dep: "astro", files: ["astro.config.mjs", "astro.config.ts"] },
220
+ { name: "SvelteKit", dep: "@sveltejs/kit", files: ["svelte.config.js", "svelte.config.ts"] }
221
+ ];
222
+ async function detectBuildTools(root) {
223
+ const supported = [];
224
+ const unsupported = [];
225
+ const pkg = await readJSON(path3.join(root, "package.json"));
226
+ const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
227
+ for (const pattern of SUPPORTED_PATTERNS) {
228
+ for (const file of pattern.files) {
229
+ if (await exists(path3.join(root, file))) {
230
+ let isLegacyRspack = false;
231
+ if (pattern.tool === "rspack") {
232
+ const version = allDeps["@rspack/cli"] || allDeps["@rspack/core"];
233
+ if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
234
+ isLegacyRspack = true;
235
+ }
236
+ }
237
+ supported.push({
238
+ tool: pattern.tool,
239
+ configPath: file,
240
+ label: `${pattern.label} (${file})${isLegacyRspack ? " [Legacy]" : ""}`,
241
+ isLegacyRspack
242
+ });
243
+ break;
244
+ }
245
+ }
246
+ }
247
+ for (const meta of UNSUPPORTED_META) {
248
+ if (!(meta.dep in allDeps)) continue;
249
+ for (const file of meta.files) {
250
+ if (await exists(path3.join(root, file))) {
251
+ unsupported.push(meta.name);
252
+ break;
253
+ }
254
+ }
255
+ }
256
+ return { supported, unsupported };
257
+ }
258
+ function resolveInjectionTarget(detections) {
259
+ if (detections.length === 0) return null;
260
+ if (detections.length === 1) return detections[0];
261
+ return "ambiguous";
262
+ }
263
+
264
+ // src/detect/framework.ts
265
+ import path4 from "path";
266
+ var SUPPORTED_FRAMEWORKS = [
267
+ { framework: "react", deps: ["react", "react-dom"] },
268
+ { framework: "vue", deps: ["vue"] }
269
+ ];
270
+ var UNSUPPORTED_FRAMEWORKS = [
271
+ { name: "Solid", dep: "solid-js" },
272
+ { name: "Svelte", dep: "svelte" },
273
+ { name: "Angular", dep: "@angular/core" },
274
+ { name: "Preact", dep: "preact" },
275
+ { name: "Lit", dep: "lit" }
276
+ ];
277
+ async function detectFrameworks(root) {
278
+ const pkg = await readJSON(path4.join(root, "package.json"));
279
+ if (!pkg) return { supported: [], unsupported: [] };
280
+ const allDeps = {
281
+ ...pkg.dependencies,
282
+ ...pkg.devDependencies
283
+ };
284
+ const supported = [];
285
+ for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
286
+ if (deps.some((dep) => dep in allDeps)) {
287
+ supported.push(framework);
288
+ }
289
+ }
290
+ const unsupported = [];
291
+ for (const fw of UNSUPPORTED_FRAMEWORKS) {
292
+ if (fw.dep in allDeps) {
293
+ unsupported.push(fw);
294
+ }
295
+ }
296
+ return { supported, unsupported };
297
+ }
298
+
299
+ // src/detect/ide.ts
300
+ import path5 from "path";
301
+ var SUPPORTED_IDE = "vscode";
302
+ async function detectIDE(root) {
303
+ const detected = /* @__PURE__ */ new Map();
304
+ if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
305
+ detected.set("Cursor", { ide: "Cursor", supported: false });
306
+ }
307
+ if (process.env.TRAE_APP_DIR || process.env.__CFBundleIdentifier === "com.byteocean.trae" || process.env.COCO_IDE_PLUGIN_TYPE === "Trae" || process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes("trae")) {
308
+ detected.set("Trae", { ide: "Trae", supported: false });
309
+ }
310
+ if (process.env.ZED_TERM) {
311
+ detected.set("Zed", { ide: "Zed", supported: false });
312
+ }
313
+ if (process.env.TERM_PROGRAM === "vscode") {
314
+ if (!detected.has("Trae") && !detected.has("Cursor")) {
315
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
316
+ }
317
+ }
318
+ if (await exists(path5.join(root, ".trae"))) {
319
+ detected.set("Trae", { ide: "Trae", supported: false });
320
+ }
321
+ if (await exists(path5.join(root, ".cursor"))) {
322
+ detected.set("Cursor", { ide: "Cursor", supported: false });
323
+ }
324
+ if (await exists(path5.join(root, ".vscode"))) {
325
+ if (!detected.has("vscode")) {
326
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
327
+ }
328
+ }
329
+ if (await exists(path5.join(root, ".idea"))) {
330
+ detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
331
+ }
332
+ return {
333
+ detected: Array.from(detected.values())
334
+ };
335
+ }
336
+
337
+ // src/detect/ai-tool.ts
338
+ import path6 from "path";
339
+ var KNOWN_CLI_TOOLS = [
340
+ { id: "claude-code", bin: "claude", label: "Claude Code", supported: true },
341
+ { id: "coco", bin: "coco", label: "Trae CLI (Coco)", supported: true },
342
+ { id: "codex", bin: "codex", label: "Codex CLI", supported: true },
343
+ { id: "gemini", bin: "gemini", label: "Gemini CLI", supported: true }
344
+ ];
345
+ var KNOWN_IDE_PLUGINS = [
346
+ { id: "claude-code", extId: "anthropic.claude-code", label: "Claude Code", supported: true },
347
+ { id: "github-copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
348
+ { id: "codex", extId: "openai.chatgpt", label: "Codex (ChatGPT)", supported: true },
349
+ { id: "gemini", extId: "google.geminicodeassist", label: "Gemini Code Assist", supported: true }
350
+ ];
351
+ async function detectAITools(root) {
352
+ const detectedMap = /* @__PURE__ */ new Map();
353
+ for (const tool of KNOWN_CLI_TOOLS) {
354
+ if (await which(tool.bin)) {
355
+ detectedMap.set(tool.id, {
356
+ id: tool.id,
357
+ label: tool.label,
358
+ supported: tool.supported,
359
+ toolModes: ["cli"],
360
+ preferredMode: "cli"
361
+ });
362
+ }
363
+ }
364
+ const extensionsJsonPath = path6.join(root, ".vscode", "extensions.json");
365
+ let recommendedExts = [];
366
+ if (await exists(extensionsJsonPath)) {
367
+ try {
368
+ const extData = await readJSON(extensionsJsonPath);
369
+ if (extData && Array.isArray(extData.recommendations)) {
370
+ recommendedExts = extData.recommendations.map((e) => e.toLowerCase());
371
+ }
372
+ } catch {
373
+ }
374
+ }
375
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
376
+ const globalExtDir = path6.join(homeDir, ".vscode", "extensions");
377
+ const globalExtExists = await exists(globalExtDir);
378
+ for (const plugin of KNOWN_IDE_PLUGINS) {
379
+ let isInstalled = false;
380
+ if (recommendedExts.includes(plugin.extId.toLowerCase())) {
381
+ isInstalled = true;
382
+ } else if (globalExtExists) {
383
+ try {
384
+ const { readdir } = await import("fs/promises");
385
+ const folders = await readdir(globalExtDir);
386
+ if (folders.some((f) => {
387
+ const lower = f.toLowerCase();
388
+ return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
389
+ })) {
390
+ isInstalled = true;
391
+ }
392
+ } catch {
393
+ }
394
+ }
395
+ if (isInstalled) {
396
+ const existing = detectedMap.get(plugin.id);
397
+ if (existing) {
398
+ existing.toolModes.push("plugin");
399
+ existing.preferredMode = "plugin";
400
+ } else {
401
+ detectedMap.set(plugin.id, {
402
+ id: plugin.id,
403
+ label: plugin.label,
404
+ supported: plugin.supported,
405
+ toolModes: ["plugin"],
406
+ preferredMode: "plugin"
407
+ });
408
+ }
409
+ }
410
+ }
411
+ return { detected: Array.from(detectedMap.values()) };
412
+ }
413
+
414
+ // src/inject/ast-injector.ts
415
+ import path7 from "path";
416
+ var IMPORT_MAP = {
417
+ vite: `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
418
+ webpack: `import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'`,
419
+ rspack: `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
420
+ rsbuild: `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
421
+ esbuild: `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
422
+ rollup: `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`
423
+ };
424
+ function getImportStatement(tool, isLegacyRspack) {
425
+ if (tool === "rspack" && isLegacyRspack) {
426
+ return `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin/legacy/rspack'`;
427
+ }
428
+ return IMPORT_MAP[tool];
429
+ }
430
+ function getPluginExpression(isLegacyRspack) {
431
+ if (isLegacyRspack) {
432
+ return `process.env.NODE_ENV !== 'production' && inspecto({
433
+ pathType: 'absolute',
434
+ escapeTags: ['Transition', 'AnimatePresence'],
435
+ }) as any`;
436
+ }
437
+ return `process.env.NODE_ENV !== 'production' && inspecto()`;
438
+ }
439
+ function printManualInstructions(tool, configPath, reason, isLegacyRspack) {
440
+ const isRsbuild = tool === "rsbuild";
441
+ log.warn(`Could not safely auto-inject into ${configPath}`);
442
+ log.hint(`(reason: ${reason})`);
443
+ log.blank();
444
+ log.hint("Please add the following manually:");
445
+ if (isRsbuild) {
446
+ log.codeBlock([
447
+ getImportStatement(tool, isLegacyRspack),
448
+ "",
449
+ "// Add to tools.rspack:",
450
+ `tools: {`,
451
+ ` rspack: {`,
452
+ ` plugins: [`,
453
+ ` ${getPluginExpression(isLegacyRspack)},`,
454
+ ` ]`,
455
+ ` }`,
456
+ `}`
457
+ ]);
458
+ } else {
459
+ log.codeBlock([
460
+ getImportStatement(tool, isLegacyRspack),
461
+ "",
462
+ "// Add to your plugins array:",
463
+ `plugins: [`,
464
+ ` ${getPluginExpression(isLegacyRspack)},`,
465
+ ` ...otherPlugins`,
466
+ `].filter(Boolean)`
467
+ ]);
468
+ }
469
+ }
470
+ function isAlreadyInjected(content) {
471
+ return content.includes("@inspecto-dev/plugin") || content.includes("inspecto()") || content.includes("aiDevInspector");
472
+ }
473
+ function injectImport(content, importStmt) {
474
+ const importRegex = /^import\s.+$/gm;
475
+ let lastImportEnd = 0;
476
+ let match;
477
+ while ((match = importRegex.exec(content)) !== null) {
478
+ const lineEnd = content.indexOf("\n", match.index);
479
+ if (lineEnd > lastImportEnd) {
480
+ lastImportEnd = lineEnd;
481
+ }
482
+ }
483
+ if (lastImportEnd === 0) {
484
+ const requireRegex = /^(?:const|let|var)\s.+=\s*require\(.+\).*$/gm;
485
+ while ((match = requireRegex.exec(content)) !== null) {
486
+ const lineEnd = content.indexOf("\n", match.index);
487
+ if (lineEnd > lastImportEnd) {
488
+ lastImportEnd = lineEnd;
489
+ }
490
+ }
491
+ }
492
+ if (lastImportEnd > 0) {
493
+ return content.slice(0, lastImportEnd) + "\n" + importStmt + content.slice(lastImportEnd);
494
+ }
495
+ return importStmt + "\n\n" + content;
496
+ }
497
+ function injectIntoPluginsArray(content, detection) {
498
+ const tool = detection.tool;
499
+ if (tool === "rsbuild") {
500
+ if (!content.includes("tools:") && !content.includes("rspack:")) {
501
+ const exportRegex = /(export default defineConfig\(\{)/;
502
+ const match2 = exportRegex.exec(content);
503
+ if (match2) {
504
+ const insertPos2 = match2.index + match2[0].length;
505
+ const pluginExpr2 = `
506
+ tools: {
507
+ rspack: {
508
+ plugins: [
509
+ ${getPluginExpression(detection.isLegacyRspack)}
510
+ ]
511
+ }
512
+ },`;
513
+ return content.slice(0, insertPos2) + pluginExpr2 + content.slice(insertPos2);
514
+ }
515
+ }
516
+ return null;
517
+ }
518
+ const pluginsRegex = /(plugins\s*:\s*\[)/;
519
+ const match = pluginsRegex.exec(content);
520
+ if (!match) return null;
521
+ const insertPos = match.index + match[0].length;
522
+ const pluginExpr = `
523
+ ${getPluginExpression(detection.isLegacyRspack)},`;
524
+ return content.slice(0, insertPos) + pluginExpr + content.slice(insertPos);
525
+ }
526
+ function validateBrackets(content) {
527
+ const openBraces = (content.match(/\{/g) || []).length;
528
+ const closeBraces = (content.match(/\}/g) || []).length;
529
+ const openBrackets = (content.match(/\[/g) || []).length;
530
+ const closeBrackets = (content.match(/\]/g) || []).length;
531
+ return Math.abs(openBraces - closeBraces) <= 1 && Math.abs(openBrackets - closeBrackets) <= 1;
532
+ }
533
+ async function injectPlugin(root, detection, dryRun) {
534
+ const configPath = path7.join(root, detection.configPath);
535
+ const backupPath = configPath + ".bak";
536
+ const mutations = [];
537
+ const content = await readFile(configPath);
538
+ if (!content) {
539
+ printManualInstructions(
540
+ detection.tool,
541
+ detection.configPath,
542
+ "config file not readable",
543
+ detection.isLegacyRspack
544
+ );
545
+ return { success: false, mutations, failureReason: "config file not readable" };
546
+ }
547
+ if (isAlreadyInjected(content)) {
548
+ log.success(`Plugin already injected in ${detection.configPath} (skipped)`);
549
+ if (await exists(backupPath)) {
550
+ mutations.push({
551
+ type: "file_modified",
552
+ path: detection.configPath,
553
+ backup: detection.configPath + ".bak",
554
+ description: "Previously injected inspecto() plugin"
555
+ });
556
+ } else {
557
+ mutations.push({
558
+ type: "file_modified",
559
+ path: detection.configPath,
560
+ description: "Previously injected inspecto() plugin (no backup)"
561
+ });
562
+ }
563
+ return { success: true, mutations };
564
+ }
565
+ if (!dryRun) {
566
+ await copyFile(configPath, backupPath);
567
+ mutations.push({
568
+ type: "file_modified",
569
+ path: detection.configPath,
570
+ backup: detection.configPath + ".bak",
571
+ description: "Injected inspecto() plugin"
572
+ });
573
+ }
574
+ log.success(`Backed up ${detection.configPath} \u2192 ${detection.configPath}.bak`);
575
+ const injected = injectIntoPluginsArray(content, detection);
576
+ if (!injected) {
577
+ printManualInstructions(
578
+ detection.tool,
579
+ detection.configPath,
580
+ "could not locate plugins array \u2014 file may use dynamic config, function wrapper, or non-standard export",
581
+ detection.isLegacyRspack
582
+ );
583
+ return {
584
+ success: false,
585
+ mutations,
586
+ failureReason: "could not locate plugins array"
587
+ };
588
+ }
589
+ const importStmt = getImportStatement(detection.tool, detection.isLegacyRspack);
590
+ const modifiedContent = injectImport(injected, importStmt);
591
+ if (!validateBrackets(modifiedContent)) {
592
+ log.error("Syntax validation failed after injection");
593
+ if (!dryRun) {
594
+ await copyFile(backupPath, configPath);
595
+ log.success(`Restored ${detection.configPath} from backup`);
596
+ }
597
+ printManualInstructions(
598
+ detection.tool,
599
+ detection.configPath,
600
+ "injection produced unbalanced brackets",
601
+ detection.isLegacyRspack
602
+ );
603
+ return {
604
+ success: false,
605
+ mutations: [],
606
+ // Rolled back
607
+ failureReason: "injection produced invalid syntax"
608
+ };
609
+ }
610
+ if (dryRun) {
611
+ log.dryRun(`Would inject plugin into ${detection.configPath}`);
612
+ } else {
613
+ await writeFile(configPath, modifiedContent);
614
+ log.success(`Injected plugin into ${detection.configPath}`);
615
+ }
616
+ return { success: true, mutations };
617
+ }
618
+
619
+ // src/inject/gitignore.ts
620
+ import path8 from "path";
621
+ var DEFAULT_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
622
+ var SHARED_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
623
+ async function updateGitignore(root, shared, dryRun) {
624
+ const gitignorePath = path8.join(root, ".gitignore");
625
+ let content = await readFile(gitignorePath) ?? "";
626
+ const desiredRules = shared ? SHARED_RULES : DEFAULT_RULES;
627
+ const hasGlobalRule = content.match(/^\.inspecto\/\s*$/m) !== null;
628
+ if (hasGlobalRule) {
629
+ content = content.replace(/^\.inspecto\/\s*$/gm, SHARED_RULES.join("\n"));
630
+ if (!dryRun) {
631
+ await writeFile(gitignorePath, content);
632
+ }
633
+ log.success("Updated .gitignore: .inspecto/settings.json is now trackable");
634
+ return;
635
+ }
636
+ const missingRules = desiredRules.filter((rule) => !content.includes(rule));
637
+ if (missingRules.length === 0) {
638
+ return;
639
+ }
640
+ const section = "\n# Inspecto\n" + missingRules.join("\n") + "\n";
641
+ content = content.trimEnd() + "\n" + section;
642
+ if (dryRun) {
643
+ log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
644
+ } else {
645
+ await writeFile(gitignorePath, content);
646
+ log.success("Updated .gitignore");
647
+ }
648
+ }
649
+ async function cleanGitignore(root) {
650
+ const gitignorePath = path8.join(root, ".gitignore");
651
+ const content = await readFile(gitignorePath);
652
+ if (!content) return;
653
+ const cleaned = content.replace(/^# Inspecto\s*$/m, "").replace(/^\.inspecto\/?\s*$/gm, "").replace(/^\.inspecto\/install\.lock\s*$/gm, "").replace(/^\.inspecto\/cache\.json\s*$/gm, "").replace(/^\.inspecto\/\*\.local\.json\s*$/gm, "").replace(/\n{3,}/g, "\n\n");
654
+ await writeFile(gitignorePath, cleaned);
655
+ }
656
+
657
+ // src/inject/extension.ts
658
+ var EXTENSION_ID = "inspecto.inspecto";
659
+ var VSCODE_PATHS = {
660
+ darwin: [
661
+ "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
662
+ "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
663
+ `${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`
664
+ ],
665
+ linux: ["/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code", "/usr/bin/code-insiders"],
666
+ win32: [
667
+ `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
668
+ `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
669
+ `${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`
670
+ ]
671
+ };
672
+ async function findVSCodeBinary() {
673
+ const platform = process.platform;
674
+ const candidates = VSCODE_PATHS[platform] || [];
675
+ for (const candidate of candidates) {
676
+ if (await exists(candidate)) {
677
+ return candidate;
678
+ }
679
+ }
680
+ if (await which("code-insiders")) {
681
+ return "code-insiders";
682
+ }
683
+ return null;
684
+ }
685
+ async function tryOpenURI(uri) {
686
+ try {
687
+ const platform = process.platform;
688
+ const openCmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
689
+ await shell(`${openCmd} "${uri}"`);
690
+ return true;
691
+ } catch {
692
+ return false;
693
+ }
694
+ }
695
+ async function installExtension(dryRun) {
696
+ if (dryRun) {
697
+ log.dryRun("Would attempt to install VS Code extension");
698
+ return null;
699
+ }
700
+ if (await which("code")) {
701
+ try {
702
+ await run("code", ["--install-extension", EXTENSION_ID]);
703
+ log.success("VS Code extension installed via CLI");
704
+ return { type: "extension_installed", id: EXTENSION_ID };
705
+ } catch {
706
+ }
707
+ }
708
+ const codePath = await findVSCodeBinary();
709
+ if (codePath) {
710
+ try {
711
+ await run(codePath, ["--install-extension", EXTENSION_ID]);
712
+ log.success("VS Code extension installed via binary path");
713
+ log.info('Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future');
714
+ return { type: "extension_installed", id: EXTENSION_ID };
715
+ } catch {
716
+ }
717
+ }
718
+ const uri = `vscode:extension/${EXTENSION_ID}`;
719
+ if (await tryOpenURI(uri)) {
720
+ log.warn("Opened extension page in VS Code");
721
+ log.hint('Please click "Install" in the opened VS Code window to complete setup.');
722
+ return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
723
+ }
724
+ log.warn("Could not auto-install VS Code extension");
725
+ log.hint("Please install it manually to enable Inspector features:");
726
+ log.hint(" 1. Open VS Code");
727
+ log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
728
+ log.hint(' 3. Search for "Inspecto"');
729
+ log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
730
+ return null;
731
+ }
732
+ async function isExtensionInstalled() {
733
+ try {
734
+ if (await which("code")) {
735
+ const { stdout } = await run("code", ["--list-extensions"]);
736
+ return stdout.toLowerCase().includes(EXTENSION_ID);
737
+ }
738
+ const codePath = await findVSCodeBinary();
739
+ if (codePath) {
740
+ const { stdout } = await run(codePath, ["--list-extensions"]);
741
+ return stdout.toLowerCase().includes(EXTENSION_ID);
742
+ }
743
+ return false;
744
+ } catch {
745
+ return false;
746
+ }
747
+ }
748
+
749
+ // src/commands/init.ts
750
+ async function promptIDEChoice(detections) {
751
+ if (!process.stdin.isTTY) {
752
+ log.warn("Multiple IDEs detected but stdin is not interactive");
753
+ log.hint(`Using: ${detections[0].ide} (first match)`);
754
+ return detections[0];
755
+ }
756
+ console.log();
757
+ console.log(" ? Detected multiple IDEs:");
758
+ detections.forEach((d, i) => {
759
+ const status = d.supported ? " (supported)" : " (unsupported/limited)";
760
+ console.log(` ${i + 1}. ${d.ide}${status}`);
761
+ });
762
+ console.log();
763
+ return new Promise((resolve) => {
764
+ process.stdout.write(" > Your choice: ");
765
+ process.stdin.resume();
766
+ process.stdin.setEncoding("utf-8");
767
+ const onData = (data) => {
768
+ const choice = parseInt(String(data).trim(), 10);
769
+ process.stdin.off("data", onData);
770
+ process.stdin.pause();
771
+ if (choice >= 1 && choice <= detections.length) {
772
+ resolve(detections[choice - 1]);
773
+ } else {
774
+ resolve(null);
775
+ }
776
+ };
777
+ process.stdin.on("data", onData);
778
+ });
779
+ }
780
+ async function promptAIToolChoice(detections) {
781
+ if (!process.stdin.isTTY) {
782
+ log.warn("Multiple AI tools detected but stdin is not interactive");
783
+ log.hint(`Using: ${detections[0].label} (first match)`);
784
+ return detections[0];
785
+ }
786
+ console.log();
787
+ console.log(" ? Detected multiple AI tools:");
788
+ detections.forEach((d, i) => {
789
+ const modeLabels = d.toolModes.map(
790
+ (mode) => mode === "plugin" ? "VS Code Extension" : "Terminal CLI"
791
+ );
792
+ const modeStr = modeLabels.join(" & ");
793
+ const status = d.supported ? ` (supported ${modeStr})` : ` (unsupported/limited)`;
794
+ console.log(` ${i + 1}. ${d.label}${status}`);
795
+ });
796
+ console.log();
797
+ return new Promise((resolve) => {
798
+ process.stdout.write(" > Your choice: ");
799
+ process.stdin.resume();
800
+ process.stdin.setEncoding("utf-8");
801
+ const onData = (data) => {
802
+ const choice = parseInt(String(data).trim(), 10);
803
+ process.stdin.off("data", onData);
804
+ process.stdin.pause();
805
+ if (choice >= 1 && choice <= detections.length) {
806
+ resolve(detections[choice - 1]);
807
+ } else {
808
+ resolve(null);
809
+ }
810
+ };
811
+ process.stdin.on("data", onData);
812
+ });
813
+ }
814
+ async function promptConfigChoice(detections) {
815
+ if (!process.stdin.isTTY) {
816
+ log.warn("Multiple config files detected but stdin is not interactive");
817
+ log.hint(`Using: ${detections[0].label} (first match)`);
818
+ return detections[0];
819
+ }
820
+ console.log();
821
+ console.log(" ? Detected multiple build tool configs:");
822
+ detections.forEach((d, i) => {
823
+ console.log(` ${i + 1}. ${d.label}`);
824
+ });
825
+ console.log(` ${detections.length + 1}. Skip (I'll configure manually)`);
826
+ console.log();
827
+ return new Promise((resolve) => {
828
+ process.stdout.write(" > Your choice: ");
829
+ process.stdin.resume();
830
+ process.stdin.setEncoding("utf-8");
831
+ const onData = (data) => {
832
+ const choice = parseInt(String(data).trim(), 10);
833
+ process.stdin.off("data", onData);
834
+ process.stdin.pause();
835
+ if (choice >= 1 && choice <= detections.length) {
836
+ resolve(detections[choice - 1]);
837
+ } else {
838
+ resolve(null);
839
+ }
840
+ };
841
+ process.stdin.on("data", onData);
842
+ });
843
+ }
844
+ async function init(options) {
845
+ const root = process.cwd();
846
+ const mutations = [];
847
+ log.header("Inspecto Setup");
848
+ if (!await exists(path9.join(root, "package.json"))) {
849
+ log.error("No package.json found in current directory");
850
+ log.hint("Run this command from your project root");
851
+ return;
852
+ }
853
+ const pm = await detectPackageManager(root);
854
+ log.success(`Detected package manager: ${pm}`);
855
+ const frameworkResult = await detectFrameworks(root);
856
+ if (frameworkResult.supported.length > 0) {
857
+ log.success(`Detected framework: ${frameworkResult.supported.join(", ")}`);
858
+ }
859
+ if (frameworkResult.unsupported.length > 0) {
860
+ const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
861
+ log.warn(`Detected ${names} \u2014 not supported in v1 (React / Vue only)`);
862
+ log.hint("Inspecto may still work but is not tested for this framework");
863
+ }
864
+ if (frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0) {
865
+ log.warn("No frontend framework detected");
866
+ log.hint("Inspecto v1 supports React and Vue projects");
867
+ }
868
+ const buildResult = await detectBuildTools(root);
869
+ if (buildResult.supported.length > 0) {
870
+ buildResult.supported.forEach((bt) => log.success(`Detected: ${bt.label}`));
871
+ }
872
+ if (buildResult.unsupported.length > 0) {
873
+ const names = buildResult.unsupported.join(", ");
874
+ log.warn(`Detected ${names} \u2014 not supported in v1`);
875
+ log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
876
+ log.hint("Meta-framework support (Next.js, Nuxt, etc.) is planned for v2");
877
+ }
878
+ if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
879
+ log.warn("No recognized build tool detected");
880
+ log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
881
+ log.hint("Dependency will be installed but plugin injection will be skipped");
882
+ }
883
+ const ideProbe = await detectIDE(root);
884
+ let selectedIDE = null;
885
+ if (ideProbe.detected.length === 0) {
886
+ log.error("No IDE detected in current project");
887
+ log.hint("Please open this project in a supported IDE (like VS Code)");
888
+ } else if (ideProbe.detected.length === 1) {
889
+ selectedIDE = ideProbe.detected[0];
890
+ } else {
891
+ selectedIDE = await promptIDEChoice(ideProbe.detected);
892
+ }
893
+ if (selectedIDE) {
894
+ if (selectedIDE.supported) {
895
+ log.success(`Selected IDE: ${selectedIDE.ide}`);
896
+ } else {
897
+ log.warn(`Selected IDE: ${selectedIDE.ide}`);
898
+ log.hint(
899
+ `Note: Inspecto currently requires VS Code (or compatible forks) to function properly.`
900
+ );
901
+ log.hint(`Features may be severely limited or unavailable in ${selectedIDE.ide}.`);
902
+ }
903
+ }
904
+ const aiToolProbe = await detectAITools(root);
905
+ let selectedAITool = null;
906
+ if (!options.prefer) {
907
+ if (aiToolProbe.detected.length === 0) {
908
+ log.warn("No supported AI tools detected");
909
+ log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
910
+ } else if (aiToolProbe.detected.length === 1) {
911
+ selectedAITool = aiToolProbe.detected[0];
912
+ if (selectedAITool.supported) {
913
+ log.success(`Detected AI tool: ${selectedAITool.label}`);
914
+ }
915
+ } else {
916
+ selectedAITool = await promptAIToolChoice(aiToolProbe.detected);
917
+ if (selectedAITool) {
918
+ log.success(`Selected AI tool: ${selectedAITool.label}`);
919
+ }
920
+ }
921
+ }
922
+ let installFailed = false;
923
+ if (options.skipInstall) {
924
+ log.warn("Skipping dependency installation (--skip-install)");
925
+ } else {
926
+ const installCmd = getInstallCommand(pm, "@inspecto-dev/plugin");
927
+ if (options.dryRun) {
928
+ log.dryRun(`Would run: ${installCmd}`);
929
+ } else {
930
+ try {
931
+ const result = await shell(installCmd, root);
932
+ if (result.stderr && result.stderr.toLowerCase().includes("error")) {
933
+ throw new Error(result.stderr);
934
+ }
935
+ log.success("Installed @inspecto-dev/plugin as devDependency");
936
+ mutations.push({
937
+ type: "dependency_added",
938
+ name: "@inspecto-dev/plugin",
939
+ dev: true
940
+ });
941
+ } catch (err) {
942
+ installFailed = true;
943
+ log.error(`Failed to install dependency: ${err?.message || "Unknown error"}`);
944
+ log.hint(`Run manually: ${installCmd}`);
945
+ }
946
+ }
947
+ }
948
+ let injectionFailed = false;
949
+ if (buildResult.supported.length > 0) {
950
+ let target = resolveInjectionTarget(buildResult.supported);
951
+ if (target === "ambiguous") {
952
+ target = await promptConfigChoice(buildResult.supported);
953
+ }
954
+ if (target) {
955
+ const result = await injectPlugin(root, target, options.dryRun);
956
+ if (result.success) {
957
+ mutations.push(...result.mutations);
958
+ } else {
959
+ injectionFailed = true;
960
+ }
961
+ } else {
962
+ injectionFailed = true;
963
+ log.warn("Skipping plugin injection (manual configuration required)");
964
+ }
965
+ }
966
+ const settingsDir = path9.join(root, ".inspecto");
967
+ const settingsPath = path9.join(settingsDir, "settings.json");
968
+ const promptsPath = path9.join(settingsDir, "prompts.json");
969
+ if (await exists(settingsPath)) {
970
+ const existingSettings = await readJSON(settingsPath);
971
+ if (existingSettings === null) {
972
+ log.warn(".inspecto/settings.json exists but contains invalid JSON");
973
+ log.hint("Please fix the syntax errors manually, or delete it and re-run init");
974
+ } else {
975
+ log.success(".inspecto/settings.json already exists (skipped)");
976
+ }
977
+ } else {
978
+ const defaultSettings = {};
979
+ if (selectedIDE && selectedIDE.supported) {
980
+ defaultSettings.ide = selectedIDE.ide === "vscode" ? "vscode" : selectedIDE.ide;
981
+ }
982
+ if (options.prefer) {
983
+ defaultSettings.prefer = options.prefer;
984
+ } else if (selectedAITool) {
985
+ defaultSettings.prefer = selectedAITool.id;
986
+ if (selectedAITool.preferredMode) {
987
+ defaultSettings.providers = {
988
+ [selectedAITool.id]: {
989
+ type: selectedAITool.preferredMode
990
+ }
991
+ };
992
+ }
993
+ }
994
+ if (options.dryRun) {
995
+ log.dryRun("Would create .inspecto/settings.json");
996
+ } else {
997
+ await writeJSON(settingsPath, defaultSettings);
998
+ log.success("Created .inspecto/settings.json");
999
+ mutations.push({ type: "file_created", path: ".inspecto/settings.json" });
1000
+ }
1001
+ }
1002
+ if (await exists(promptsPath)) {
1003
+ log.success(".inspecto/prompts.json already exists (skipped)");
1004
+ } else {
1005
+ const defaultPrompts = [
1006
+ { id: "code-review", enabled: false },
1007
+ { id: "generate-test", enabled: false },
1008
+ { id: "performance", enabled: false }
1009
+ ];
1010
+ if (options.dryRun) {
1011
+ log.dryRun("Would create .inspecto/prompts.json");
1012
+ } else {
1013
+ await writeJSON(promptsPath, defaultPrompts);
1014
+ log.success("Created .inspecto/prompts.json (disabling low-frequency intents)");
1015
+ mutations.push({ type: "file_created", path: ".inspecto/prompts.json" });
1016
+ }
1017
+ }
1018
+ if (!options.dryRun) {
1019
+ await updateGitignore(root, options.shared, options.dryRun);
1020
+ mutations.push({
1021
+ type: "file_modified",
1022
+ path: ".gitignore",
1023
+ description: "Appended .inspecto/ ignore rules"
1024
+ });
1025
+ } else {
1026
+ log.dryRun("Would update .gitignore");
1027
+ }
1028
+ if (!options.dryRun && mutations.length > 0) {
1029
+ const lock = {
1030
+ version: "1.0.0",
1031
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
1032
+ mutations
1033
+ };
1034
+ await writeJSON(path9.join(settingsDir, "install.lock"), lock);
1035
+ }
1036
+ const shouldInstallExt = !options.noExtension && (!selectedIDE || selectedIDE && selectedIDE.supported);
1037
+ let manualExtensionInstallNeeded = false;
1038
+ if (options.noExtension) {
1039
+ log.warn("Skipping VS Code extension (--no-extension)");
1040
+ } else if (!shouldInstallExt) {
1041
+ } else {
1042
+ const extMutation = await installExtension(options.dryRun);
1043
+ if (extMutation && !options.dryRun) {
1044
+ mutations.push(extMutation);
1045
+ if (extMutation.manual_action_required) {
1046
+ manualExtensionInstallNeeded = true;
1047
+ }
1048
+ const lockPath = path9.join(settingsDir, "install.lock");
1049
+ const lock = await readJSON(lockPath);
1050
+ if (lock) {
1051
+ lock.mutations = mutations;
1052
+ await writeJSON(lockPath, lock);
1053
+ }
1054
+ } else if (extMutation === null && !options.dryRun) {
1055
+ manualExtensionInstallNeeded = true;
1056
+ }
1057
+ }
1058
+ if (options.dryRun) {
1059
+ log.blank();
1060
+ log.warn("Dry run complete. No files were modified.");
1061
+ } else if (installFailed || injectionFailed || manualExtensionInstallNeeded) {
1062
+ log.blank();
1063
+ log.warn("Setup completed with some manual steps required.");
1064
+ log.hint("Please check the logs above and complete the manual steps.");
1065
+ log.blank();
1066
+ } else {
1067
+ log.ready("Ready! Hold Alt + Click any element to inspect.");
1068
+ }
1069
+ }
1070
+
1071
+ // src/commands/doctor.ts
1072
+ import path10 from "path";
1073
+ async function doctor() {
1074
+ const root = process.cwd();
1075
+ const result = { errors: 0, warnings: 0 };
1076
+ log.header("Inspecto Doctor");
1077
+ if (!await exists(path10.join(root, "package.json"))) {
1078
+ log.error("No package.json found");
1079
+ log.hint("Run this command from your project root");
1080
+ return;
1081
+ }
1082
+ const ideProbe = await detectIDE(root);
1083
+ if (ideProbe.detected.length === 0) {
1084
+ log.warn("IDE: not detected");
1085
+ result.warnings++;
1086
+ } else {
1087
+ const hasSupported = ideProbe.detected.some((d) => d.supported);
1088
+ if (hasSupported) {
1089
+ log.success(
1090
+ `IDE: ${ideProbe.detected.filter((d) => d.supported).map((d) => d.ide).join(", ")}`
1091
+ );
1092
+ } else {
1093
+ const names = ideProbe.detected.map((d) => d.ide).join(", ");
1094
+ log.warn(`IDE: ${names} (not supported in v1, VS Code only)`);
1095
+ result.warnings++;
1096
+ }
1097
+ }
1098
+ const frameworkResult = await detectFrameworks(root);
1099
+ if (frameworkResult.supported.length > 0) {
1100
+ log.success(`Framework: ${frameworkResult.supported.join(", ")}`);
1101
+ } else if (frameworkResult.unsupported.length > 0) {
1102
+ const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1103
+ log.warn(`Framework: ${names} (not supported in v1, React/Vue only)`);
1104
+ result.warnings++;
1105
+ } else {
1106
+ log.warn("Framework: not detected (React / Vue expected)");
1107
+ result.warnings++;
1108
+ }
1109
+ const aiProbe = await detectAITools(root);
1110
+ if (aiProbe.detected.length === 0) {
1111
+ log.warn("AI Tool: none detected");
1112
+ log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
1113
+ result.warnings++;
1114
+ } else {
1115
+ const aiNames = aiProbe.detected.map((d) => {
1116
+ const modeLabels = d.toolModes.map(
1117
+ (mode) => mode === "plugin" ? "VS Code Extension" : "Terminal CLI"
1118
+ );
1119
+ return `${d.label} (${modeLabels.join(" & ")})`;
1120
+ }).join(", ");
1121
+ log.success(`AI Tool: ${aiNames}`);
1122
+ }
1123
+ const pluginPath = path10.join(root, "node_modules", "@inspecto", "plugin");
1124
+ if (await exists(pluginPath)) {
1125
+ const pkgJson = await readJSON(path10.join(pluginPath, "package.json"));
1126
+ const version = pkgJson?.version ?? "unknown";
1127
+ log.success(`@inspecto-dev/plugin@${version} installed`);
1128
+ } else {
1129
+ log.error("@inspecto-dev/plugin not installed");
1130
+ const pm = await detectPackageManager(root);
1131
+ log.hint(`Fix: ${getInstallCommand(pm, "@inspecto-dev/plugin")}`);
1132
+ result.errors++;
1133
+ }
1134
+ const buildResult = await detectBuildTools(root);
1135
+ if (buildResult.supported.length > 0) {
1136
+ let injected = false;
1137
+ for (const bt of buildResult.supported) {
1138
+ const content = await readFile(path10.join(root, bt.configPath));
1139
+ if (content && content.includes("@inspecto-dev/plugin")) {
1140
+ log.success(`Plugin injected in ${bt.configPath}`);
1141
+ injected = true;
1142
+ break;
1143
+ }
1144
+ }
1145
+ if (!injected) {
1146
+ log.error("Plugin not injected in any build config");
1147
+ log.hint("Fix: npx inspecto init");
1148
+ result.errors++;
1149
+ }
1150
+ } else if (buildResult.unsupported.length > 0) {
1151
+ const names = buildResult.unsupported.join(", ");
1152
+ log.warn(`Build tool: ${names} (not supported in v1)`);
1153
+ log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
1154
+ result.warnings++;
1155
+ } else {
1156
+ log.warn("No recognized build config found");
1157
+ result.warnings++;
1158
+ }
1159
+ const extInstalled = await isExtensionInstalled();
1160
+ if (extInstalled) {
1161
+ log.success("VS Code extension detected");
1162
+ } else {
1163
+ const hasSupported = ideProbe.detected.some((d) => d.supported);
1164
+ if (ideProbe.detected.length > 0 && !hasSupported) {
1165
+ log.warn("VS Code extension not applicable (non-VS Code IDE)");
1166
+ } else {
1167
+ log.error("VS Code extension not found");
1168
+ log.hint("Fix: code --install-extension inspecto.inspecto");
1169
+ log.hint("Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto");
1170
+ result.errors++;
1171
+ }
1172
+ }
1173
+ const settingsPath = path10.join(root, ".inspecto", "settings.json");
1174
+ if (await exists(settingsPath)) {
1175
+ const settings = await readJSON(settingsPath);
1176
+ if (settings) {
1177
+ log.success(".inspecto/settings.json valid");
1178
+ } else {
1179
+ log.error(".inspecto/settings.json has invalid JSON");
1180
+ log.hint(
1181
+ "Fix: Manually correct the syntax errors, or delete the file and re-run npx inspecto init"
1182
+ );
1183
+ result.errors++;
1184
+ }
1185
+ } else {
1186
+ log.warn(".inspecto/settings.json not found (using defaults)");
1187
+ log.hint("Optional: npx inspecto init");
1188
+ result.warnings++;
1189
+ }
1190
+ const gitignoreContent = await readFile(path10.join(root, ".gitignore"));
1191
+ if (gitignoreContent) {
1192
+ const hasLockIgnore = gitignoreContent.includes(".inspecto/install.lock") || gitignoreContent.includes(".inspecto/");
1193
+ if (!hasLockIgnore) {
1194
+ log.warn(".inspecto/install.lock not in .gitignore");
1195
+ log.hint("install.lock contains local machine state and should not be committed");
1196
+ result.warnings++;
1197
+ }
1198
+ }
1199
+ log.blank();
1200
+ if (result.errors === 0 && result.warnings === 0) {
1201
+ log.success("All checks passed. Hold Alt + Click to start!");
1202
+ } else {
1203
+ const parts = [];
1204
+ if (result.errors > 0) parts.push(`${result.errors} error(s)`);
1205
+ if (result.warnings > 0) parts.push(`${result.warnings} warning(s)`);
1206
+ console.log(
1207
+ ` ${parts.join(", ")}. ${result.errors > 0 ? "Fix the errors above to get started." : ""}`
1208
+ );
1209
+ }
1210
+ log.blank();
1211
+ }
1212
+
1213
+ // src/commands/teardown.ts
1214
+ import path11 from "path";
1215
+ async function teardown() {
1216
+ const root = process.cwd();
1217
+ log.header("Inspecto Teardown");
1218
+ const lockPath = path11.join(root, ".inspecto", "install.lock");
1219
+ const lock = await readJSON(lockPath);
1220
+ if (!lock) {
1221
+ log.warn("No .inspecto/install.lock found. Running in best-effort mode.");
1222
+ log.blank();
1223
+ const pm = await detectPackageManager(root);
1224
+ try {
1225
+ const cmd = getUninstallCommand(pm, "@inspecto-dev/plugin");
1226
+ await shell(cmd, root);
1227
+ log.success("Removed @inspecto-dev/plugin from devDependencies");
1228
+ } catch {
1229
+ log.warn("Could not remove @inspecto-dev/plugin (may not be installed)");
1230
+ }
1231
+ if (await exists(path11.join(root, ".inspecto"))) {
1232
+ await removeDir(path11.join(root, ".inspecto"));
1233
+ log.success("Deleted .inspecto/ directory");
1234
+ }
1235
+ await cleanGitignore(root);
1236
+ log.success("Cleaned .gitignore entries");
1237
+ log.warn("Cannot restore build config (no backup reference)");
1238
+ log.hint("Please manually remove the inspecto() plugin from your build config");
1239
+ log.blank();
1240
+ return;
1241
+ }
1242
+ log.success("Reading .inspecto/install.lock...");
1243
+ log.blank();
1244
+ for (const mutation of lock.mutations) {
1245
+ switch (mutation.type) {
1246
+ case "file_modified": {
1247
+ if (mutation.backup && mutation.path) {
1248
+ const backupPath = path11.join(root, mutation.backup);
1249
+ const targetPath = path11.join(root, mutation.path);
1250
+ if (mutation.path === ".gitignore") {
1251
+ await cleanGitignore(root);
1252
+ log.success("Cleaned .gitignore entries");
1253
+ await removeFile(backupPath);
1254
+ } else if (await exists(backupPath)) {
1255
+ await copyFile(backupPath, targetPath);
1256
+ await removeFile(backupPath);
1257
+ log.success(`Restored ${mutation.path} from backup`);
1258
+ } else {
1259
+ log.warn(`Backup not found: ${mutation.backup}`);
1260
+ log.hint(`Please manually remove inspecto from ${mutation.path}`);
1261
+ }
1262
+ } else if (mutation.path && mutation.path !== ".gitignore") {
1263
+ log.warn(`Cannot auto-restore ${mutation.path} (no backup recorded)`);
1264
+ log.hint(`Please manually remove the inspecto() plugin from ${mutation.path}`);
1265
+ }
1266
+ break;
1267
+ }
1268
+ case "file_created": {
1269
+ break;
1270
+ }
1271
+ case "dependency_added": {
1272
+ if (mutation.name) {
1273
+ const pm = await detectPackageManager(root);
1274
+ try {
1275
+ const cmd = getUninstallCommand(pm, mutation.name);
1276
+ await shell(cmd, root);
1277
+ log.success(`Removed ${mutation.name} from devDependencies`);
1278
+ } catch {
1279
+ log.warn(`Could not remove ${mutation.name}`);
1280
+ }
1281
+ }
1282
+ break;
1283
+ }
1284
+ case "extension_installed": {
1285
+ if (mutation.id) {
1286
+ log.warn(`VS Code extension not auto-uninstalled`);
1287
+ log.hint(`Run: code --uninstall-extension ${mutation.id}`);
1288
+ }
1289
+ break;
1290
+ }
1291
+ }
1292
+ }
1293
+ await removeDir(path11.join(root, ".inspecto"));
1294
+ log.success("Deleted .inspecto/ directory");
1295
+ await cleanGitignore(root);
1296
+ log.blank();
1297
+ log.success("Done. All Inspecto traces removed.");
1298
+ log.blank();
1299
+ }
1300
+
1301
+ export {
1302
+ log,
1303
+ init,
1304
+ doctor,
1305
+ teardown
1306
+ };