@inspecto-dev/cli 0.2.0-alpha.5 → 0.2.0-alpha.6

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.
@@ -46,14 +46,55 @@ var log = {
46
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
47
  console.log();
48
48
  },
49
+ /** Copy-friendly code block without box characters */
50
+ copyableCodeBlock(lines) {
51
+ console.log();
52
+ for (const line of lines) {
53
+ console.log(` ${line}`);
54
+ }
55
+ console.log();
56
+ },
49
57
  /** Dry-run prefix */
50
58
  dryRun(text) {
51
59
  console.log(` ${pc.blue("[dry-run]")} ${text}`);
52
60
  }
53
61
  };
54
62
 
55
- // src/commands/init.ts
56
- import path9 from "path";
63
+ // src/utils/output.ts
64
+ function writeCommandOutput(result, json, renderText) {
65
+ if (json) {
66
+ console.log(JSON.stringify(result, null, 2));
67
+ return result;
68
+ }
69
+ renderText(result);
70
+ return result;
71
+ }
72
+ function reportCommandError(error, options = {}) {
73
+ const message2 = error instanceof Error ? error.message : String(error);
74
+ const stack = error instanceof Error ? error.stack : void 0;
75
+ if (options.json) {
76
+ const payload = {
77
+ status: "error",
78
+ error: {
79
+ message: message2,
80
+ ...options.debug && stack ? { stack } : {}
81
+ }
82
+ };
83
+ console.error(JSON.stringify(payload, null, 2));
84
+ return;
85
+ }
86
+ log.error(message2);
87
+ if (options.debug && stack) {
88
+ console.error(stack);
89
+ }
90
+ }
91
+
92
+ // src/onboarding/apply.ts
93
+ import path5 from "path";
94
+ import ora from "ora";
95
+
96
+ // src/detect/package-manager.ts
97
+ import path2 from "path";
57
98
 
58
99
  // src/utils/fs.ts
59
100
  import fs from "fs/promises";
@@ -85,7 +126,8 @@ async function removeDir(dirPath) {
85
126
  }
86
127
  async function readJSON(filePath) {
87
128
  const text = await readFile(filePath);
88
- if (!text) return null;
129
+ if (text === null) return null;
130
+ if (text.trim() === "") return {};
89
131
  try {
90
132
  return JSON.parse(text);
91
133
  } catch {
@@ -96,39 +138,7 @@ async function writeJSON(filePath, data) {
96
138
  await writeFile(filePath, JSON.stringify(data, null, 2) + "\n");
97
139
  }
98
140
 
99
- // src/utils/exec.ts
100
- import { execFile, exec as execCb } from "child_process";
101
- import { promisify } from "util";
102
- var execFileAsync = promisify(execFile);
103
- var execAsync = promisify(execCb);
104
- async function run(command, args, cwd) {
105
- const result = await execFileAsync(command, args, {
106
- cwd,
107
- timeout: 6e4,
108
- env: { ...process.env }
109
- });
110
- return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
111
- }
112
- async function shell(command, cwd) {
113
- const result = await execAsync(command, {
114
- cwd,
115
- timeout: 6e4,
116
- env: { ...process.env }
117
- });
118
- return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
119
- }
120
- async function which(bin) {
121
- try {
122
- const cmd = process.platform === "win32" ? "where" : "which";
123
- await run(cmd, [bin]);
124
- return true;
125
- } catch {
126
- return false;
127
- }
128
- }
129
-
130
141
  // src/detect/package-manager.ts
131
- import path2 from "path";
132
142
  async function detectPackageManager(root) {
133
143
  const checks = [
134
144
  ["bun.lockb", "bun"],
@@ -175,861 +185,1584 @@ function getUninstallCommand(pm, pkg) {
175
185
  }
176
186
  }
177
187
 
178
- // src/detect/build-tool.ts
188
+ // src/inject/ast-injector.ts
179
189
  import path3 from "path";
180
- import { createRequire } from "module";
181
- function isPackageResolvable(pkgName, root) {
182
- try {
183
- const require2 = createRequire(path3.join(root, "package.json"));
184
- try {
185
- require2.resolve(`${pkgName}/package.json`, { paths: [root] });
186
- return true;
187
- } catch {
188
- require2.resolve(pkgName, { paths: [root] });
189
- return true;
190
- }
191
- } catch {
192
- return false;
193
- }
194
- }
195
- async function getResolvedPackageVersion(pkgName, root) {
196
- try {
197
- const require2 = createRequire(path3.join(root, "package.json"));
198
- const pkgJsonPath = require2.resolve(`${pkgName}/package.json`, { paths: [root] });
199
- const pkg = await readJSON(pkgJsonPath);
200
- return pkg?.version || null;
201
- } catch {
202
- return null;
190
+ import { loadFile, writeFile as writeAstFile } from "magicast";
191
+
192
+ // src/inject/strategies/vite.ts
193
+ import { addVitePlugin } from "magicast/helpers";
194
+ var ViteStrategy = class {
195
+ name = "Vite";
196
+ supports(tool) {
197
+ return tool === "vite";
203
198
  }
204
- }
205
- var SUPPORTED_PATTERNS = [
206
- {
207
- tool: "vite",
208
- files: [
209
- "vite.config.ts",
210
- "vite.config.js",
211
- "vite.config.mts",
212
- "vite.config.mjs",
213
- "vite.config.cjs",
214
- "vite.config.cts"
215
- ],
216
- label: "Vite"
217
- },
218
- {
219
- tool: "rspack",
220
- files: ["rspack.config.js", "rspack.config.ts", "rspack.config.mjs"],
221
- label: "Rspack"
222
- },
223
- {
224
- tool: "rsbuild",
225
- files: ["rsbuild.config.js", "rsbuild.config.ts", "rsbuild.config.mjs"],
226
- label: "Rsbuild"
227
- },
228
- {
229
- tool: "webpack",
230
- files: ["webpack.config.js", "webpack.config.ts", "webpack.config.mjs", "webpack.config.cjs"],
231
- label: "Webpack"
232
- },
233
- {
234
- tool: "esbuild",
235
- files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs", "build.js", "build.ts"],
236
- label: "esbuild"
237
- },
238
- {
239
- tool: "rollup",
240
- files: ["rollup.config.js", "rollup.config.ts", "rollup.config.mjs"],
241
- label: "Rollup"
199
+ inject({ mod, detection }) {
200
+ addVitePlugin(mod, {
201
+ from: "@inspecto-dev/plugin",
202
+ constructor: "inspecto",
203
+ imported: "vitePlugin"
204
+ });
242
205
  }
243
- ];
244
- var UNSUPPORTED_META = [
245
- { name: "Next.js", dep: "next", files: ["next.config.mjs", "next.config.js", "next.config.ts"] },
246
- { name: "Nuxt", dep: "nuxt", files: ["nuxt.config.ts", "nuxt.config.js"] },
247
- { name: "Remix", dep: "@remix-run/dev", files: ["remix.config.js", "remix.config.ts"] },
248
- { name: "Astro", dep: "astro", files: ["astro.config.mjs", "astro.config.ts"] },
249
- { name: "SvelteKit", dep: "@sveltejs/kit", files: ["svelte.config.js", "svelte.config.ts"] }
250
- ];
251
- function normalizeRelativePath(root, filePath) {
252
- const relative = path3.relative(root, filePath);
253
- const normalized = relative.split(path3.sep).join("/");
254
- return normalized || path3.basename(filePath);
255
- }
256
- function createTargets(root, packagePaths) {
257
- if (!packagePaths || packagePaths.length === 0) {
258
- return [{ packagePath: "", absolutePath: root }];
206
+ getManualInstructions(detection, reason) {
207
+ return [
208
+ `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
209
+ "",
210
+ "// Add to your plugins array:",
211
+ `plugins: [`,
212
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
213
+ ` ...otherPlugins`,
214
+ `].filter(Boolean)`
215
+ ];
259
216
  }
260
- return packagePaths.map((pkg) => ({
261
- packagePath: pkg,
262
- absolutePath: pkg ? path3.join(root, pkg) : root
263
- }));
264
- }
265
- async function detectBuildTools(root, packagePaths) {
266
- const supported = [];
267
- const unsupported = /* @__PURE__ */ new Set();
268
- const targets = createTargets(root, packagePaths);
269
- for (const target of targets) {
270
- const pkg = await readJSON(path3.join(target.absolutePath, "package.json"));
271
- const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
272
- const scripts = pkg?.scripts || {};
273
- const supportedChecks = SUPPORTED_PATTERNS.map(
274
- (pattern) => detectPattern({
275
- pattern,
276
- workspaceRoot: root,
277
- targetRoot: target.absolutePath,
278
- packagePath: target.packagePath,
279
- allDeps,
280
- scripts
281
- })
282
- );
283
- const supportedResults = await Promise.all(supportedChecks);
284
- for (const result of supportedResults) {
285
- if (result) {
286
- supported.push(result);
287
- }
288
- }
289
- const unsupportedChecks = UNSUPPORTED_META.map(async (meta) => {
290
- if (!(meta.dep in allDeps)) return null;
291
- for (const file of meta.files) {
292
- if (await exists(path3.join(target.absolutePath, file))) {
293
- return meta.name;
294
- }
295
- }
296
- return null;
297
- });
298
- const unsupportedResults = await Promise.all(unsupportedChecks);
299
- for (const result of unsupportedResults) {
300
- if (result) {
301
- unsupported.add(result);
302
- }
303
- }
217
+ };
218
+
219
+ // src/inject/strategies/webpack.ts
220
+ var WebpackStrategy = class {
221
+ name = "Webpack";
222
+ supports(tool) {
223
+ return tool === "webpack";
304
224
  }
305
- return { supported, unsupported: Array.from(unsupported) };
306
- }
307
- async function detectPattern({
308
- pattern,
309
- workspaceRoot,
310
- targetRoot,
311
- packagePath,
312
- allDeps,
313
- scripts
314
- }) {
315
- let hasDep;
316
- let resolvedVersion = null;
317
- if (pattern.tool === "rspack") {
318
- const depName = allDeps["@rspack/cli"] ? "@rspack/cli" : "@rspack/core";
319
- hasDep = !!allDeps["@rspack/cli"] || !!allDeps["@rspack/core"] || isPackageResolvable("@rspack/core", targetRoot);
320
- if (hasDep) {
321
- resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("@rspack/core", targetRoot);
322
- }
323
- } else if (pattern.tool === "webpack") {
324
- const depName = allDeps["webpack"] ? "webpack" : "webpack-cli";
325
- hasDep = !!allDeps["webpack"] || !!allDeps["webpack-cli"] || isPackageResolvable("webpack", targetRoot);
326
- if (hasDep) {
327
- resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("webpack", targetRoot);
328
- }
329
- } else if (pattern.tool === "rsbuild") {
330
- hasDep = !!allDeps["@rsbuild/core"] || isPackageResolvable("@rsbuild/core", targetRoot);
331
- } else {
332
- hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, targetRoot);
225
+ inject(options) {
226
+ throw new Error("Webpack requires manual plugin configuration");
333
227
  }
334
- let detectedFile = "";
335
- if (pattern.tool === "esbuild" && !hasDep) {
336
- return null;
228
+ getManualInstructions(detection, reason) {
229
+ const importPkg = detection.isLegacyWebpack ? "@inspecto-dev/plugin/legacy/webpack4" : "@inspecto-dev/plugin";
230
+ const pluginName = detection.isLegacyWebpack ? "webpack4Plugin" : "webpackPlugin";
231
+ const pluginCall = detection.isLegacyWebpack ? `process.env.NODE_ENV !== 'production' && inspecto({
232
+ pathType: 'absolute',
233
+ escapeTags: ['Transition', 'AnimatePresence'],
234
+ })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
235
+ return [
236
+ `import { ${pluginName} as inspecto } from '${importPkg}'`,
237
+ "",
238
+ "// Add to your plugins array:",
239
+ `plugins: [`,
240
+ ` ${pluginCall},`,
241
+ ` ...otherPlugins`,
242
+ `].filter(Boolean)`
243
+ ];
337
244
  }
338
- for (const file of pattern.files) {
339
- if (await exists(path3.join(targetRoot, file))) {
340
- detectedFile = file;
341
- break;
342
- }
245
+ };
246
+
247
+ // src/inject/strategies/rspack.ts
248
+ var RspackStrategy = class {
249
+ name = "Rspack";
250
+ supports(tool) {
251
+ return tool === "rspack";
343
252
  }
344
- if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "rsbuild")) {
345
- for (const cmd of Object.values(scripts)) {
346
- if (cmd.includes("node ")) {
347
- const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
348
- if (match && match[1]) {
349
- if (await exists(path3.join(targetRoot, match[1]))) {
350
- if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
351
- detectedFile = match[1];
352
- break;
353
- }
354
- }
355
- }
356
- } else if (cmd.includes(`${pattern.tool} `)) {
357
- if (pattern.tool === "webpack" || pattern.tool === "rspack") {
358
- const configMatch = cmd.match(/--config\s+([^\s]+)/);
359
- if (configMatch && configMatch[1]) {
360
- if (await exists(path3.join(targetRoot, configMatch[1]))) {
361
- detectedFile = configMatch[1];
362
- break;
363
- }
364
- }
365
- }
366
- if (!detectedFile) {
367
- detectedFile = "package.json (scripts)";
368
- break;
369
- }
370
- }
371
- }
253
+ inject(options) {
254
+ throw new Error("Rspack requires manual plugin configuration");
372
255
  }
373
- if (!detectedFile) {
374
- return null;
256
+ getManualInstructions(detection, reason) {
257
+ const importPkg = detection.isLegacyRspack ? "@inspecto-dev/plugin/legacy/rspack" : "@inspecto-dev/plugin";
258
+ const pluginCall = detection.isLegacyRspack ? `process.env.NODE_ENV !== 'production' && inspecto({
259
+ pathType: 'absolute',
260
+ escapeTags: ['Transition', 'AnimatePresence'],
261
+ })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
262
+ return [
263
+ `import { rspackPlugin as inspecto } from '${importPkg}'`,
264
+ "",
265
+ "// Add to your plugins array:",
266
+ `plugins: [`,
267
+ ` ${pluginCall},`,
268
+ ` ...otherPlugins`,
269
+ `].filter(Boolean)`
270
+ ];
375
271
  }
376
- let isLegacyRspack = false;
377
- let isLegacyWebpack = false;
378
- if (pattern.tool === "rspack") {
379
- const version = resolvedVersion;
380
- if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
381
- isLegacyRspack = true;
382
- }
383
- } else if (pattern.tool === "webpack") {
384
- const version = resolvedVersion;
385
- if (version && version.includes("^4") || version?.startsWith("4.")) {
386
- isLegacyWebpack = true;
272
+ };
273
+
274
+ // src/inject/strategies/rsbuild.ts
275
+ var RsbuildStrategy = class {
276
+ name = "Rsbuild";
277
+ supports(tool) {
278
+ return tool === "rsbuild";
279
+ }
280
+ inject(options) {
281
+ throw new Error("Rsbuild requires manual plugin configuration due to nested structure");
282
+ }
283
+ getManualInstructions(detection, reason) {
284
+ return [
285
+ `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
286
+ "",
287
+ "// Add to tools.rspack:",
288
+ `tools: {`,
289
+ ` rspack: {`,
290
+ ` plugins: [`,
291
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
292
+ ` ]`,
293
+ ` }`,
294
+ `}`
295
+ ];
296
+ }
297
+ };
298
+
299
+ // src/inject/strategies/esbuild.ts
300
+ var EsbuildStrategy = class {
301
+ name = "esbuild";
302
+ supports(tool) {
303
+ return tool === "esbuild";
304
+ }
305
+ inject(options) {
306
+ throw new Error("Esbuild requires manual plugin configuration");
307
+ }
308
+ getManualInstructions(detection, reason) {
309
+ return [
310
+ `1. Update your esbuild config (${detection.configPath}):`,
311
+ `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
312
+ "",
313
+ "// Add to your plugins array:",
314
+ `plugins: [`,
315
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
316
+ ` ...otherPlugins`,
317
+ `].filter(Boolean)`,
318
+ "",
319
+ "2. Initialize the client in your app entry (e.g., main.js / index.js):",
320
+ `import { mountInspector } from '@inspecto-dev/core'`,
321
+ "",
322
+ "// Call this before your app renders",
323
+ `if (process.env.NODE_ENV !== 'production') {`,
324
+ ` mountInspector()`,
325
+ `}`
326
+ ];
327
+ }
328
+ };
329
+
330
+ // src/inject/strategies/rollup.ts
331
+ var RollupStrategy = class {
332
+ name = "Rollup";
333
+ supports(tool) {
334
+ return tool === "rollup";
335
+ }
336
+ inject(options) {
337
+ throw new Error("Rollup requires manual plugin configuration");
338
+ }
339
+ getManualInstructions(detection, reason) {
340
+ return [
341
+ `1. Update your rollup config (${detection.configPath}):`,
342
+ `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
343
+ "",
344
+ "// Add to your plugins array:",
345
+ `plugins: [`,
346
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
347
+ ` ...otherPlugins`,
348
+ `].filter(Boolean)`,
349
+ "",
350
+ "2. Initialize the client in your app entry (e.g., main.js / index.js):",
351
+ `import { mountInspector } from '@inspecto-dev/core'`,
352
+ "",
353
+ "// Call this before your app renders",
354
+ `if (process.env.NODE_ENV !== 'production') {`,
355
+ ` mountInspector()`,
356
+ `}`
357
+ ];
358
+ }
359
+ };
360
+
361
+ // src/inject/strategies/index.ts
362
+ var STRATEGIES = [
363
+ new ViteStrategy(),
364
+ new WebpackStrategy(),
365
+ new RspackStrategy(),
366
+ new RsbuildStrategy(),
367
+ new EsbuildStrategy(),
368
+ new RollupStrategy()
369
+ ];
370
+
371
+ // src/inject/ast-injector.ts
372
+ function printManualInstructions(strategy, detection, reason) {
373
+ log.warn(`Could not automatically configure ${detection.configPath}`);
374
+ log.hint(`(reason: ${reason})`);
375
+ log.blank();
376
+ log.hint("Please add the following manually:");
377
+ if (strategy) {
378
+ const instructions = strategy.getManualInstructions(detection, reason);
379
+ log.copyableCodeBlock(instructions);
380
+ } else {
381
+ log.error(`Unsupported build tool: ${detection.tool}`);
382
+ }
383
+ }
384
+ function isAlreadyInjected(content) {
385
+ const normalized = content.replace(/\s+/g, " ");
386
+ const importPlugin = /import\s+(.+?)\s+from\s+['"]@inspecto-dev\/plugin['"]/g;
387
+ const requirePlugin = /require\(['"]@inspecto-dev\/plugin['"]\)/;
388
+ const legacyImport = /import\s+.*ai-dev-inspector/.test(normalized);
389
+ const legacyRequire = /require\(['"]ai-dev-inspector['"]\)/.test(normalized);
390
+ if (legacyImport || legacyRequire || requirePlugin.test(normalized)) return true;
391
+ let match;
392
+ importPlugin.lastIndex = 0;
393
+ while (match = importPlugin.exec(normalized)) {
394
+ const importClause = match[1] || "";
395
+ if (/inspecto/.test(importClause) || /vitePlugin/.test(importClause)) {
396
+ return true;
387
397
  }
388
398
  }
389
- const absoluteConfig = path3.join(targetRoot, detectedFile);
390
- const relativeConfig = normalizeRelativePath(workspaceRoot, absoluteConfig);
391
- return {
392
- tool: pattern.tool,
393
- configPath: relativeConfig,
394
- label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? " [Legacy]" : ""}${isLegacyWebpack ? " [Webpack 4]" : ""}`,
395
- isLegacyRspack,
396
- isLegacyWebpack,
397
- packagePath: packagePath || void 0
398
- };
399
+ return false;
399
400
  }
400
- function resolveInjectionTarget(detections) {
401
- if (detections.length === 0) return null;
402
- if (detections.length === 1) return detections[0];
403
- return "ambiguous";
401
+ async function injectPlugin(root, detection, dryRun) {
402
+ const configPath = path3.join(root, detection.configPath);
403
+ const mutations = [];
404
+ const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
405
+ const content = await readFile(configPath);
406
+ if (!content) {
407
+ printManualInstructions(strategy, detection, "config file not readable");
408
+ return { success: false, mutations, failureReason: "config file not readable" };
409
+ }
410
+ if (isAlreadyInjected(content)) {
411
+ log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
412
+ mutations.push({
413
+ type: "file_modified",
414
+ path: detection.configPath,
415
+ description: "Previously configured inspecto() plugin"
416
+ });
417
+ return { success: true, mutations };
418
+ }
419
+ if (!strategy) {
420
+ printManualInstructions(
421
+ strategy,
422
+ detection,
423
+ `No injection strategy found for ${detection.tool}`
424
+ );
425
+ return { success: false, mutations, failureReason: "No strategy found" };
426
+ }
427
+ if (dryRun) {
428
+ log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
429
+ return { success: true, mutations: [] };
430
+ }
431
+ try {
432
+ const mod = await loadFile(configPath);
433
+ strategy.inject({
434
+ mod,
435
+ detection
436
+ });
437
+ await writeAstFile(mod, configPath);
438
+ mutations.push({
439
+ type: "file_modified",
440
+ path: detection.configPath,
441
+ description: "Automatically configured inspecto() plugin"
442
+ });
443
+ log.success(`Configured plugin in ${detection.configPath}`);
444
+ return { success: true, mutations };
445
+ } catch (err) {
446
+ printManualInstructions(
447
+ strategy,
448
+ detection,
449
+ `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`
450
+ );
451
+ return {
452
+ success: false,
453
+ mutations,
454
+ failureReason: "Automatic configuration unavailable"
455
+ };
456
+ }
404
457
  }
405
458
 
406
- // src/detect/framework.ts
407
- import path4 from "path";
408
- import { createRequire as createRequire2 } from "module";
409
- var META_FRAMEWORK_MAP = {
410
- next: "react",
411
- nuxt: "vue",
412
- "@remix-run/react": "react",
413
- "@remix-run/dev": "react",
414
- "@vue/nuxt": "vue",
415
- "vite-plugin-vue": "vue",
416
- "@vitejs/plugin-vue": "vue",
417
- "@vitejs/plugin-react": "react",
418
- "@vitejs/plugin-react-swc": "react"
459
+ // src/utils/exec.ts
460
+ import { execFile, exec as execCb } from "child_process";
461
+ import { promisify } from "util";
462
+ var execFileAsync = promisify(execFile);
463
+ var execAsync = promisify(execCb);
464
+ async function run(command, args, cwd) {
465
+ const result = await execFileAsync(command, args, {
466
+ cwd,
467
+ timeout: 6e4,
468
+ env: { ...process.env }
469
+ });
470
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
471
+ }
472
+ async function shell(command, cwd) {
473
+ const result = await execAsync(command, {
474
+ cwd,
475
+ timeout: 6e4,
476
+ env: { ...process.env }
477
+ });
478
+ return { stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
479
+ }
480
+ async function which(bin) {
481
+ try {
482
+ const cmd = process.platform === "win32" ? "where" : "which";
483
+ await run(cmd, [bin]);
484
+ return true;
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+
490
+ // src/inject/extension.ts
491
+ var EXTENSION_ID = "inspecto.inspecto";
492
+ var VSCODE_PATHS = {
493
+ darwin: [
494
+ "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
495
+ "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
496
+ `${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`
497
+ ],
498
+ linux: ["/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code", "/usr/bin/code-insiders"],
499
+ win32: [
500
+ `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
501
+ `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
502
+ `${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`
503
+ ]
419
504
  };
420
- var SUPPORTED_FRAMEWORKS = [
421
- { framework: "react", deps: ["react", "react-dom"] },
422
- { framework: "vue", deps: ["vue"] }
423
- ];
424
- var UNSUPPORTED_FRAMEWORKS = [
425
- { name: "Solid", dep: "solid-js" },
426
- { name: "Svelte", dep: "svelte" },
427
- { name: "Angular", dep: "@angular/core" },
428
- { name: "Preact", dep: "preact" },
429
- { name: "Lit", dep: "lit" }
430
- ];
431
- function isPackageResolvable2(pkgName, root) {
505
+ async function findVSCodeBinary() {
506
+ const platform = process.platform;
507
+ const candidates = VSCODE_PATHS[platform] || [];
508
+ for (const candidate of candidates) {
509
+ if (await exists(candidate)) {
510
+ return candidate;
511
+ }
512
+ }
513
+ if (await which("code-insiders")) {
514
+ return "code-insiders";
515
+ }
516
+ return null;
517
+ }
518
+ async function tryOpenURI(uri) {
519
+ try {
520
+ const platform = process.platform;
521
+ if (platform === "win32") {
522
+ await shell(`cmd /c start "" "${uri}"`);
523
+ } else {
524
+ const openCmd = platform === "darwin" ? "open" : "xdg-open";
525
+ await shell(`${openCmd} "${uri}"`);
526
+ }
527
+ return true;
528
+ } catch {
529
+ return false;
530
+ }
531
+ }
532
+ async function installExtension(dryRun, ide) {
533
+ if (dryRun) {
534
+ log.dryRun("Would attempt to install VS Code extension");
535
+ return null;
536
+ }
537
+ const isVSCode = !ide || ide === "vscode";
538
+ if (isVSCode) {
539
+ if (await which("code")) {
540
+ try {
541
+ await run("code", ["--install-extension", EXTENSION_ID]);
542
+ log.success("VS Code extension installed via CLI");
543
+ return { type: "extension_installed", id: EXTENSION_ID };
544
+ } catch {
545
+ }
546
+ }
547
+ const codePath = await findVSCodeBinary();
548
+ if (codePath) {
549
+ try {
550
+ await run(codePath, ["--install-extension", EXTENSION_ID]);
551
+ log.success("VS Code extension installed via binary path");
552
+ log.info(
553
+ 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
554
+ );
555
+ return { type: "extension_installed", id: EXTENSION_ID };
556
+ } catch {
557
+ }
558
+ }
559
+ const uri = `vscode:extension/${EXTENSION_ID}`;
560
+ if (await tryOpenURI(uri)) {
561
+ log.warn("Opened extension page in VS Code");
562
+ log.hint('Please click "Install" in the opened VS Code window to complete setup.');
563
+ return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
564
+ }
565
+ log.warn("Could not auto-install VS Code extension");
566
+ log.hint("Please install it manually to enable Inspector features:");
567
+ log.hint(" 1. Open VS Code");
568
+ log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
569
+ log.hint(' 3. Search for "Inspecto"');
570
+ log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
571
+ return null;
572
+ }
573
+ log.warn(`Could not auto-install extension for ${ide}`);
574
+ log.hint("Please install it manually to enable Inspector features:");
575
+ log.hint(" 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)");
576
+ log.hint(` 2. Open ${ide}`);
577
+ log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
578
+ log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
579
+ log.hint(" 5. Select the downloaded .vsix file");
580
+ return null;
581
+ }
582
+ async function isExtensionInstalled() {
432
583
  try {
433
- const require2 = createRequire2(path4.join(root, "package.json"));
434
- try {
435
- require2.resolve(`${pkgName}/package.json`, { paths: [root] });
436
- return true;
437
- } catch {
438
- require2.resolve(pkgName, { paths: [root] });
439
- return true;
584
+ if (await which("code")) {
585
+ const { stdout } = await run("code", ["--list-extensions"]);
586
+ return stdout.toLowerCase().includes(EXTENSION_ID);
587
+ }
588
+ const codePath = await findVSCodeBinary();
589
+ if (codePath) {
590
+ const { stdout } = await run(codePath, ["--list-extensions"]);
591
+ return stdout.toLowerCase().includes(EXTENSION_ID);
440
592
  }
593
+ return false;
441
594
  } catch {
442
595
  return false;
443
596
  }
444
597
  }
445
- async function detectFrameworks(root) {
446
- const pkg = await readJSON(path4.join(root, "package.json"));
447
- const allDeps = {
448
- ...pkg?.dependencies || {},
449
- ...pkg?.devDependencies || {},
450
- ...pkg?.peerDependencies || {}
451
- };
452
- const supportedSet = /* @__PURE__ */ new Set();
453
- const unsupported = [];
454
- const isTest = root.includes("/mock/root");
455
- for (const [metaPkg, framework] of Object.entries(META_FRAMEWORK_MAP)) {
456
- if (metaPkg in allDeps || !isTest && isPackageResolvable2(metaPkg, root)) {
457
- supportedSet.add(framework);
598
+
599
+ // src/inject/gitignore.ts
600
+ import path4 from "path";
601
+ var DEFAULT_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
602
+ var SHARED_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
603
+ async function updateGitignore(root, shared, dryRun) {
604
+ const gitignorePath = path4.join(root, ".gitignore");
605
+ let content = await readFile(gitignorePath) ?? "";
606
+ const desiredRules = shared ? SHARED_RULES : DEFAULT_RULES;
607
+ const hasGlobalRule = content.match(/^\.inspecto\/\s*$/m) !== null;
608
+ if (hasGlobalRule) {
609
+ content = content.replace(/^\.inspecto\/\s*$/gm, SHARED_RULES.join("\n"));
610
+ if (!dryRun) {
611
+ await writeFile(gitignorePath, content);
458
612
  }
613
+ log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
614
+ return;
459
615
  }
460
- for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
461
- if (supportedSet.has(framework)) continue;
462
- for (const dep of deps) {
463
- if (dep in allDeps || !isTest && isPackageResolvable2(dep, root)) {
464
- supportedSet.add(framework);
465
- break;
466
- }
467
- }
616
+ const missingRules = desiredRules.filter((rule) => !content.includes(rule));
617
+ if (missingRules.length === 0) {
618
+ return;
468
619
  }
469
- for (const fw of UNSUPPORTED_FRAMEWORKS) {
470
- if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
471
- unsupported.push(fw);
472
- }
620
+ const section = "\n# Inspecto\n" + missingRules.join("\n") + "\n";
621
+ content = content.trimEnd() + "\n" + section;
622
+ if (dryRun) {
623
+ log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
624
+ } else {
625
+ await writeFile(gitignorePath, content);
626
+ log.success("Updated .gitignore");
473
627
  }
474
- return {
475
- supported: Array.from(supportedSet),
476
- unsupported
477
- };
628
+ }
629
+ async function cleanGitignore(root) {
630
+ const gitignorePath = path4.join(root, ".gitignore");
631
+ const content = await readFile(gitignorePath);
632
+ if (!content) return;
633
+ 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");
634
+ await writeFile(gitignorePath, cleaned);
478
635
  }
479
636
 
480
- // src/detect/ide.ts
481
- import path5 from "path";
482
- var SUPPORTED_IDE = "vscode";
483
- async function detectIDE(root) {
484
- const detected = /* @__PURE__ */ new Map();
485
- if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
486
- detected.set("Cursor", { ide: "cursor", supported: true });
487
- }
488
- 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")) {
489
- detected.set("Trae", { ide: "trae", supported: true });
490
- }
491
- if (process.env.ZED_TERM) {
492
- detected.set("Zed", { ide: "Zed", supported: false });
493
- }
494
- if (process.env.WINDSURF_APP_DIR || process.env.WINDSURF_CHANNEL || process.env.__CFBundleIdentifier === "com.codeium.windsurf" || process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes("windsurf")) {
495
- detected.set("Windsurf", { ide: "Windsurf", supported: false });
496
- }
497
- const [hasTrae, hasCursor, hasVscode, hasIdea] = await Promise.all([
498
- exists(path5.join(root, ".trae")),
499
- exists(path5.join(root, ".cursor")),
500
- exists(path5.join(root, ".vscode")),
501
- exists(path5.join(root, ".idea"))
502
- ]);
503
- if (hasTrae && !detected.has("Trae")) {
504
- detected.set("Trae", { ide: "trae", supported: true });
505
- }
506
- if (hasCursor && !detected.has("Cursor")) {
507
- detected.set("Cursor", { ide: "cursor", supported: true });
508
- }
509
- if (hasVscode && !detected.has("vscode")) {
510
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
511
- }
512
- if (hasIdea && !detected.has("JetBrains IDE")) {
513
- detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
514
- }
515
- if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
516
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
637
+ // src/onboarding/apply.ts
638
+ function resultStatus(nextSteps) {
639
+ return nextSteps.length > 0 ? "warning" : "ok";
640
+ }
641
+ function manualPlanSteps(plan2) {
642
+ return [
643
+ ...plan2.blockers.map((blocker) => blocker.message),
644
+ ...plan2.actions.filter((action) => action.type === "manual_step").map((action) => action.description)
645
+ ];
646
+ }
647
+ async function applyOnboardingPlan(input) {
648
+ return applyOnboardingPlanInternal(input);
649
+ }
650
+ function createReporter(quiet = false) {
651
+ if (quiet) {
652
+ return {
653
+ warn() {
654
+ },
655
+ success() {
656
+ },
657
+ error() {
658
+ },
659
+ hint() {
660
+ },
661
+ dryRun() {
662
+ }
663
+ };
517
664
  }
518
665
  return {
519
- detected: Array.from(detected.values())
666
+ warn(text) {
667
+ log.warn(text);
668
+ },
669
+ success(text) {
670
+ log.success(text);
671
+ },
672
+ error(text) {
673
+ log.error(text);
674
+ },
675
+ hint(text) {
676
+ log.hint(text);
677
+ },
678
+ dryRun(text) {
679
+ log.dryRun(text);
680
+ }
520
681
  };
521
682
  }
522
-
523
- // src/detect/provider.ts
524
- import path6 from "path";
525
- var KNOWN_CLI_TOOLS = [
526
- { id: "claude-code", bin: "claude", label: "Claude Code", supported: true },
527
- { id: "coco", bin: "coco", label: "Trae CLI (Coco)", supported: true },
528
- { id: "codex", bin: "codex", label: "Codex CLI", supported: true },
529
- { id: "gemini", bin: "gemini", label: "Gemini CLI", supported: true }
530
- ];
531
- var KNOWN_IDE_PLUGINS = [
532
- { id: "claude-code", extId: "anthropic.claude-code", label: "Claude Code", supported: true },
533
- { id: "copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
534
- { id: "codex", extId: "openai.chatgpt", label: "Codex (ChatGPT)", supported: true },
535
- { id: "gemini", extId: "google.geminicodeassist", label: "Gemini Code Assist", supported: true }
536
- ];
537
- async function detectProviders(root) {
538
- const detectedMap = /* @__PURE__ */ new Map();
539
- const cliChecks = KNOWN_CLI_TOOLS.map(async (tool) => {
540
- if (await which(tool.bin)) {
541
- detectedMap.set(tool.id, {
542
- id: tool.id,
543
- label: tool.label,
544
- supported: tool.supported,
545
- providerModes: ["cli"],
546
- preferredMode: "cli"
547
- });
548
- }
549
- });
550
- await Promise.all(cliChecks);
551
- const extensionsJsonPath = path6.join(root, ".vscode", "extensions.json");
552
- let recommendedExts = [];
553
- if (await exists(extensionsJsonPath)) {
554
- try {
555
- const extData = await readJSON(extensionsJsonPath);
556
- if (extData && Array.isArray(extData.recommendations)) {
557
- recommendedExts = extData.recommendations.map((e) => e.toLowerCase());
683
+ function createSpinner(text, quiet = false) {
684
+ if (quiet) {
685
+ return {
686
+ start() {
687
+ },
688
+ succeed() {
689
+ },
690
+ fail() {
558
691
  }
559
- } catch {
692
+ };
693
+ }
694
+ const spinner = ora(text);
695
+ return {
696
+ start() {
697
+ spinner.start();
698
+ },
699
+ succeed(successText) {
700
+ spinner.succeed(successText);
701
+ },
702
+ fail(failureText) {
703
+ spinner.fail(failureText);
560
704
  }
705
+ };
706
+ }
707
+ async function applyOnboardingPlanInternal(input) {
708
+ const reporter = createReporter(input.options.quiet);
709
+ if (input.plan && input.plan.strategy !== "supported" && !input.allowManualPlanApply) {
710
+ return {
711
+ status: input.plan.status,
712
+ mutations: [],
713
+ postInstall: {
714
+ installFailed: false,
715
+ injectionFailed: false,
716
+ manualExtensionInstallNeeded: false,
717
+ nextSteps: manualPlanSteps(input.plan)
718
+ }
719
+ };
561
720
  }
562
- const homeDir = process.env.HOME || process.env.USERPROFILE || "";
563
- const globalExtDir = path6.join(homeDir, ".vscode", "extensions");
564
- const globalExtExists = await exists(globalExtDir);
565
- let installedExtensionFolders = [];
566
- if (globalExtExists) {
721
+ const mutations = [];
722
+ const settingsDir = path5.join(input.projectRoot, ".inspecto");
723
+ const settingsFileName = input.options.shared ? "settings.json" : "settings.local.json";
724
+ const promptsFileName = input.options.shared ? "prompts.json" : "prompts.local.json";
725
+ const settingsPath = path5.join(settingsDir, settingsFileName);
726
+ const promptsPath = path5.join(settingsDir, promptsFileName);
727
+ const installCmd = getInstallCommand(
728
+ input.packageManager,
729
+ "@inspecto-dev/plugin @inspecto-dev/core"
730
+ );
731
+ const nextSteps = [];
732
+ let installFailed = false;
733
+ if (input.options.skipInstall) {
734
+ reporter.warn("Skipping dependency installation (--skip-install)");
735
+ } else if (input.options.dryRun) {
736
+ reporter.dryRun(`Would run: ${installCmd}`);
737
+ } else {
738
+ const spinner = createSpinner(
739
+ `Installing devDependencies via: ${installCmd}`,
740
+ input.options.quiet
741
+ );
567
742
  try {
568
- const { readdir } = await import("fs/promises");
569
- installedExtensionFolders = await readdir(globalExtDir);
570
- const obsoletePath = path6.join(globalExtDir, ".obsolete");
571
- if (await exists(obsoletePath)) {
572
- try {
573
- const obsoleteData = await readJSON(obsoletePath);
574
- if (obsoleteData) {
575
- const obsoleteKeys = Object.keys(obsoleteData);
576
- installedExtensionFolders = installedExtensionFolders.filter((folder) => {
577
- return !obsoleteKeys.includes(folder);
578
- });
579
- }
580
- } catch {
581
- }
582
- }
583
- } catch {
743
+ spinner.start();
744
+ await shell(installCmd, input.projectRoot);
745
+ spinner.succeed("Dependencies installed successfully");
746
+ reporter.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
747
+ mutations.push({ type: "dependency_added", name: "@inspecto-dev/plugin", dev: true });
748
+ mutations.push({ type: "dependency_added", name: "@inspecto-dev/core", dev: true });
749
+ } catch (error) {
750
+ spinner.fail("Dependency installation failed");
751
+ installFailed = true;
752
+ reporter.error(`Failed to install dependency: ${error?.message || "Unknown error"}`);
753
+ reporter.hint(`Run manually in ${input.projectRoot}: ${installCmd}`);
754
+ reporter.hint(
755
+ "Setup will continue without dependencies, but Inspecto may not run until installation succeeds."
756
+ );
584
757
  }
585
758
  }
586
- for (const plugin of KNOWN_IDE_PLUGINS) {
587
- let isInstalled = false;
588
- if (recommendedExts.includes(plugin.extId.toLowerCase())) {
589
- isInstalled = true;
590
- } else if (installedExtensionFolders.some((f) => {
591
- const lower = f.toLowerCase();
592
- return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
593
- })) {
594
- isInstalled = true;
759
+ let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig);
760
+ for (const target of input.supportedBuildTargets) {
761
+ const result = await injectPlugin(input.repoRoot, target, input.options.dryRun);
762
+ if (result.success) {
763
+ mutations.push(...result.mutations);
764
+ } else {
765
+ injectionFailed = true;
595
766
  }
596
- if (isInstalled) {
597
- const existing = detectedMap.get(plugin.id);
598
- if (existing) {
599
- existing.providerModes.push("extension");
600
- existing.preferredMode = "extension";
601
- } else {
602
- detectedMap.set(plugin.id, {
603
- id: plugin.id,
604
- label: plugin.label,
605
- supported: plugin.supported,
606
- providerModes: ["extension"],
607
- preferredMode: "extension"
608
- });
767
+ }
768
+ if (await exists(settingsPath)) {
769
+ const existingSettings = await readJSON(settingsPath);
770
+ if (existingSettings === null) {
771
+ reporter.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`);
772
+ reporter.hint("Please fix the syntax errors manually, or delete it and re-run init");
773
+ nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`);
774
+ } else {
775
+ reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`);
776
+ }
777
+ } else {
778
+ const defaultSettings = {};
779
+ if (input.selectedIDE?.supported) {
780
+ defaultSettings.ide = input.selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : input.selectedIDE.ide.toLowerCase();
781
+ }
782
+ if (input.providerDefault) {
783
+ defaultSettings["provider.default"] = input.providerDefault;
784
+ }
785
+ if (input.options.dryRun) {
786
+ reporter.dryRun(`Would create .inspecto/${settingsFileName}`);
787
+ } else {
788
+ await writeJSON(settingsPath, defaultSettings);
789
+ reporter.success(`Created .inspecto/${settingsFileName}`);
790
+ mutations.push({ type: "file_created", path: `.inspecto/${settingsFileName}` });
791
+ }
792
+ }
793
+ if (await exists(promptsPath)) {
794
+ reporter.success(`.inspecto/${promptsFileName} already exists (skipped)`);
795
+ } else if (input.options.dryRun) {
796
+ reporter.dryRun(`Would create .inspecto/${promptsFileName}`);
797
+ } else {
798
+ await writeJSON(promptsPath, []);
799
+ reporter.success(`Created .inspecto/${promptsFileName}`);
800
+ mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
801
+ }
802
+ if (!input.options.dryRun) {
803
+ await updateGitignore(input.projectRoot, input.options.shared, input.options.dryRun);
804
+ mutations.push({
805
+ type: "file_modified",
806
+ path: ".gitignore",
807
+ description: "Appended .inspecto/ ignore rules"
808
+ });
809
+ } else {
810
+ reporter.dryRun("Would update .gitignore");
811
+ }
812
+ if (!input.options.dryRun && mutations.length > 0) {
813
+ const lock = {
814
+ version: "1.0.0",
815
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
816
+ mutations
817
+ };
818
+ await writeJSON(path5.join(settingsDir, "install.lock"), lock);
819
+ }
820
+ const shouldInstallExt = !input.options.noExtension && (!input.selectedIDE || input.selectedIDE.supported);
821
+ let manualExtensionInstallNeeded = false;
822
+ if (input.options.noExtension) {
823
+ reporter.warn("Skipping IDE extension (--no-extension)");
824
+ } else if (shouldInstallExt) {
825
+ const extMutation = await installExtension(input.options.dryRun, input.selectedIDE?.ide);
826
+ if (extMutation && !input.options.dryRun) {
827
+ mutations.push(extMutation);
828
+ if (extMutation.manual_action_required) {
829
+ manualExtensionInstallNeeded = true;
830
+ }
831
+ const lockPath = path5.join(settingsDir, "install.lock");
832
+ const lock = await readJSON(lockPath);
833
+ if (lock) {
834
+ lock.mutations = mutations;
835
+ await writeJSON(lockPath, lock);
609
836
  }
837
+ } else if (extMutation === null && !input.options.dryRun) {
838
+ manualExtensionInstallNeeded = true;
610
839
  }
611
840
  }
612
- return { detected: Array.from(detectedMap.values()) };
841
+ if (!input.options.dryRun) {
842
+ if (installFailed) {
843
+ nextSteps.push(`Install dependencies manually in ${input.projectRoot}: ${installCmd}`);
844
+ }
845
+ if (injectionFailed) {
846
+ nextSteps.push(
847
+ "Plugin injection skipped. Follow manual instructions printed above to update your config."
848
+ );
849
+ }
850
+ if (manualExtensionInstallNeeded) {
851
+ nextSteps.push("Install the Inspecto IDE extension manually");
852
+ }
853
+ if (input.manualConfigRequiredFor === "Nuxt") {
854
+ nextSteps.push(
855
+ "Nuxt detected\u2014please follow the Nuxt instructions printed above to finish setup."
856
+ );
857
+ } else if (input.manualConfigRequiredFor === "Next.js") {
858
+ nextSteps.push(
859
+ "Next.js detected\u2014please follow the Next.js instructions printed above to finish setup."
860
+ );
861
+ }
862
+ }
863
+ return {
864
+ status: resultStatus(nextSteps),
865
+ mutations,
866
+ postInstall: {
867
+ installFailed,
868
+ injectionFailed,
869
+ manualExtensionInstallNeeded,
870
+ nextSteps
871
+ }
872
+ };
613
873
  }
614
874
 
615
- // src/inject/ast-injector.ts
616
- import path7 from "path";
617
- import { loadFile, writeFile as writeAstFile } from "magicast";
618
-
619
- // src/inject/strategies/vite.ts
620
- import { addVitePlugin } from "magicast/helpers";
621
- var ViteStrategy = class {
622
- name = "Vite";
623
- supports(tool) {
624
- return tool === "vite";
875
+ // src/detect/build-tool.ts
876
+ import path6 from "path";
877
+ import fs2 from "fs/promises";
878
+ import { createRequire } from "module";
879
+ function isPackageResolvable(pkgName, root) {
880
+ try {
881
+ const require2 = createRequire(path6.join(root, "package.json"));
882
+ try {
883
+ require2.resolve(`${pkgName}/package.json`, { paths: [root] });
884
+ return true;
885
+ } catch {
886
+ require2.resolve(pkgName, { paths: [root] });
887
+ return true;
888
+ }
889
+ } catch {
890
+ return false;
625
891
  }
626
- inject({ mod, detection }) {
627
- addVitePlugin(mod, {
628
- from: "@inspecto-dev/plugin",
629
- constructor: "inspecto",
630
- imported: "vitePlugin"
631
- });
892
+ }
893
+ async function getResolvedPackageVersion(pkgName, root) {
894
+ try {
895
+ const require2 = createRequire(path6.join(root, "package.json"));
896
+ const pkgJsonPath = require2.resolve(`${pkgName}/package.json`, { paths: [root] });
897
+ const pkg = await readJSON(pkgJsonPath);
898
+ return pkg?.version || null;
899
+ } catch {
900
+ return null;
632
901
  }
633
- getManualInstructions(detection, reason) {
634
- return [
635
- `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
636
- "",
637
- "// Add to your plugins array:",
638
- `plugins: [`,
639
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
640
- ` ...otherPlugins`,
641
- `].filter(Boolean)`
642
- ];
902
+ }
903
+ var SUPPORTED_PATTERNS = [
904
+ {
905
+ tool: "vite",
906
+ files: [
907
+ "vite.config.ts",
908
+ "vite.config.js",
909
+ "vite.config.mts",
910
+ "vite.config.mjs",
911
+ "vite.config.cjs",
912
+ "vite.config.cts"
913
+ ],
914
+ label: "Vite"
915
+ },
916
+ {
917
+ tool: "rspack",
918
+ files: ["rspack.config.js", "rspack.config.ts", "rspack.config.mjs"],
919
+ label: "Rspack"
920
+ },
921
+ {
922
+ tool: "rsbuild",
923
+ files: ["rsbuild.config.js", "rsbuild.config.ts", "rsbuild.config.mjs"],
924
+ label: "Rsbuild"
925
+ },
926
+ {
927
+ tool: "webpack",
928
+ files: ["webpack.config.js", "webpack.config.ts", "webpack.config.mjs", "webpack.config.cjs"],
929
+ label: "Webpack"
930
+ },
931
+ {
932
+ tool: "esbuild",
933
+ files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs", "build.js", "build.ts"],
934
+ label: "esbuild"
935
+ },
936
+ {
937
+ tool: "rollup",
938
+ files: ["rollup.config.js", "rollup.config.ts", "rollup.config.mjs"],
939
+ label: "Rollup"
643
940
  }
644
- };
645
-
646
- // src/inject/strategies/webpack.ts
647
- var WebpackStrategy = class {
648
- name = "Webpack";
649
- supports(tool) {
650
- return tool === "webpack";
941
+ ];
942
+ var UNSUPPORTED_META = [
943
+ { name: "Next.js", dep: "next", files: ["next.config.mjs", "next.config.js", "next.config.ts"] },
944
+ { name: "Nuxt", dep: "nuxt", files: ["nuxt.config.ts", "nuxt.config.js"] },
945
+ { name: "Remix", dep: "@remix-run/dev", files: ["remix.config.js", "remix.config.ts"] },
946
+ { name: "Astro", dep: "astro", files: ["astro.config.mjs", "astro.config.ts"] },
947
+ { name: "SvelteKit", dep: "@sveltejs/kit", files: ["svelte.config.js", "svelte.config.ts"] }
948
+ ];
949
+ function normalizeRelativePath(root, filePath) {
950
+ const relative = path6.relative(root, filePath);
951
+ const normalized = relative.split(path6.sep).join("/");
952
+ return normalized || path6.basename(filePath);
953
+ }
954
+ function createTargets(root, packagePaths) {
955
+ if (!packagePaths || packagePaths.length === 0) {
956
+ return [{ packagePath: "", absolutePath: root }];
651
957
  }
652
- inject(options) {
653
- throw new Error("Webpack requires manual plugin configuration");
958
+ return packagePaths.map((pkg) => ({
959
+ packagePath: pkg,
960
+ absolutePath: pkg ? path6.join(root, pkg) : root
961
+ }));
962
+ }
963
+ async function getWorkspacePackagePatterns(root) {
964
+ const pkg = await readJSON(
965
+ path6.join(root, "package.json")
966
+ );
967
+ const workspaces = pkg?.workspaces;
968
+ if (Array.isArray(workspaces)) {
969
+ return workspaces;
970
+ }
971
+ if (workspaces && Array.isArray(workspaces.packages)) {
972
+ return workspaces.packages;
973
+ }
974
+ const pnpmWorkspace = await readFile(path6.join(root, "pnpm-workspace.yaml"));
975
+ if (!pnpmWorkspace) {
976
+ return [];
977
+ }
978
+ const patterns = [];
979
+ for (const line of pnpmWorkspace.split("\n")) {
980
+ const match = line.match(/^\s*-\s*['"]?([^'"]+)['"]?\s*$/);
981
+ if (match?.[1]) {
982
+ patterns.push(match[1]);
983
+ }
654
984
  }
655
- getManualInstructions(detection, reason) {
656
- const importPkg = detection.isLegacyWebpack ? "@inspecto-dev/plugin/legacy/webpack4" : "@inspecto-dev/plugin";
657
- const pluginName = detection.isLegacyWebpack ? "webpack4Plugin" : "webpackPlugin";
658
- const pluginCall = detection.isLegacyWebpack ? `process.env.NODE_ENV !== 'production' && inspecto({
659
- pathType: 'absolute',
660
- escapeTags: ['Transition', 'AnimatePresence'],
661
- })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
662
- return [
663
- `import { ${pluginName} as inspecto } from '${importPkg}'`,
664
- "",
665
- "// Add to your plugins array:",
666
- `plugins: [`,
667
- ` ${pluginCall},`,
668
- ` ...otherPlugins`,
669
- `].filter(Boolean)`
670
- ];
985
+ return patterns;
986
+ }
987
+ async function expandWorkspacePattern(root, pattern) {
988
+ const normalized = pattern.replace(/\\/g, "/").replace(/\/$/, "");
989
+ if (!normalized || normalized.startsWith("!")) {
990
+ return [];
671
991
  }
672
- };
673
-
674
- // src/inject/strategies/rspack.ts
675
- var RspackStrategy = class {
676
- name = "Rspack";
677
- supports(tool) {
678
- return tool === "rspack";
992
+ if (!normalized.includes("*")) {
993
+ return await exists(path6.join(root, normalized)) ? [normalized] : [];
679
994
  }
680
- inject(options) {
681
- throw new Error("Rspack requires manual plugin configuration");
995
+ const starIndex = normalized.indexOf("*");
996
+ const baseDir = normalized.slice(0, starIndex).replace(/\/$/, "");
997
+ const suffix = normalized.slice(starIndex + 1);
998
+ if (suffix && suffix !== "") {
999
+ return [];
682
1000
  }
683
- getManualInstructions(detection, reason) {
684
- const importPkg = detection.isLegacyRspack ? "@inspecto-dev/plugin/legacy/rspack" : "@inspecto-dev/plugin";
685
- const pluginCall = detection.isLegacyRspack ? `process.env.NODE_ENV !== 'production' && inspecto({
686
- pathType: 'absolute',
687
- escapeTags: ['Transition', 'AnimatePresence'],
688
- })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
689
- return [
690
- `import { rspackPlugin as inspecto } from '${importPkg}'`,
691
- "",
692
- "// Add to your plugins array:",
693
- `plugins: [`,
694
- ` ${pluginCall},`,
695
- ` ...otherPlugins`,
696
- `].filter(Boolean)`
697
- ];
1001
+ const absoluteBaseDir = path6.join(root, baseDir);
1002
+ if (!await exists(absoluteBaseDir)) {
1003
+ return [];
698
1004
  }
699
- };
700
-
701
- // src/inject/strategies/rsbuild.ts
702
- var RsbuildStrategy = class {
703
- name = "Rsbuild";
704
- supports(tool) {
705
- return tool === "rsbuild";
1005
+ try {
1006
+ const entries = await fs2.readdir(absoluteBaseDir, { withFileTypes: true });
1007
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => path6.posix.join(baseDir, entry.name));
1008
+ } catch {
1009
+ return [];
706
1010
  }
707
- inject(options) {
708
- throw new Error("Rsbuild requires manual plugin configuration due to nested structure");
1011
+ }
1012
+ async function detectWorkspaceTargets(root) {
1013
+ const patterns = await getWorkspacePackagePatterns(root);
1014
+ if (patterns.length === 0) {
1015
+ return [];
1016
+ }
1017
+ const packagePaths = /* @__PURE__ */ new Set();
1018
+ for (const pattern of patterns) {
1019
+ const expanded = await expandWorkspacePattern(root, pattern);
1020
+ for (const packagePath of expanded) {
1021
+ packagePaths.add(packagePath);
1022
+ }
709
1023
  }
710
- getManualInstructions(detection, reason) {
711
- return [
712
- `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
713
- "",
714
- "// Add to tools.rspack:",
715
- `tools: {`,
716
- ` rspack: {`,
717
- ` plugins: [`,
718
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
719
- ` ]`,
720
- ` }`,
721
- `}`
722
- ];
1024
+ return Array.from(packagePaths).map((packagePath) => ({
1025
+ packagePath,
1026
+ absolutePath: path6.join(root, packagePath)
1027
+ }));
1028
+ }
1029
+ async function detectBuildTools(root, packagePaths) {
1030
+ const supported = [];
1031
+ const unsupported = /* @__PURE__ */ new Set();
1032
+ const explicitTargets = createTargets(root, packagePaths);
1033
+ const workspaceTargets = !packagePaths || packagePaths.length === 0 ? await detectWorkspaceTargets(root) : [];
1034
+ const targets = workspaceTargets.length > 0 ? workspaceTargets : explicitTargets;
1035
+ for (const target of targets) {
1036
+ const pkg = await readJSON(path6.join(target.absolutePath, "package.json"));
1037
+ const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
1038
+ const scripts = pkg?.scripts || {};
1039
+ const supportedChecks = SUPPORTED_PATTERNS.map(
1040
+ (pattern) => detectPattern({
1041
+ pattern,
1042
+ workspaceRoot: root,
1043
+ targetRoot: target.absolutePath,
1044
+ packagePath: target.packagePath,
1045
+ allDeps,
1046
+ scripts
1047
+ })
1048
+ );
1049
+ const supportedResults = await Promise.all(supportedChecks);
1050
+ for (const result of supportedResults) {
1051
+ if (result) {
1052
+ supported.push(result);
1053
+ }
1054
+ }
1055
+ const unsupportedChecks = UNSUPPORTED_META.map(async (meta) => {
1056
+ if (!(meta.dep in allDeps)) return null;
1057
+ for (const file of meta.files) {
1058
+ if (await exists(path6.join(target.absolutePath, file))) {
1059
+ return meta.name;
1060
+ }
1061
+ }
1062
+ return null;
1063
+ });
1064
+ const unsupportedResults = await Promise.all(unsupportedChecks);
1065
+ for (const result of unsupportedResults) {
1066
+ if (result) {
1067
+ unsupported.add(result);
1068
+ }
1069
+ }
723
1070
  }
724
- };
725
-
726
- // src/inject/strategies/esbuild.ts
727
- var EsbuildStrategy = class {
728
- name = "esbuild";
729
- supports(tool) {
730
- return tool === "esbuild";
1071
+ return { supported, unsupported: Array.from(unsupported) };
1072
+ }
1073
+ async function detectPattern({
1074
+ pattern,
1075
+ workspaceRoot,
1076
+ targetRoot,
1077
+ packagePath,
1078
+ allDeps,
1079
+ scripts
1080
+ }) {
1081
+ let hasDep;
1082
+ let resolvedVersion = null;
1083
+ if (pattern.tool === "rspack") {
1084
+ const depName = allDeps["@rspack/cli"] ? "@rspack/cli" : "@rspack/core";
1085
+ hasDep = !!allDeps["@rspack/cli"] || !!allDeps["@rspack/core"] || isPackageResolvable("@rspack/core", targetRoot);
1086
+ if (hasDep) {
1087
+ resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("@rspack/core", targetRoot);
1088
+ }
1089
+ } else if (pattern.tool === "webpack") {
1090
+ const depName = allDeps["webpack"] ? "webpack" : "webpack-cli";
1091
+ hasDep = !!allDeps["webpack"] || !!allDeps["webpack-cli"] || isPackageResolvable("webpack", targetRoot);
1092
+ if (hasDep) {
1093
+ resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("webpack", targetRoot);
1094
+ }
1095
+ } else if (pattern.tool === "rsbuild") {
1096
+ hasDep = !!allDeps["@rsbuild/core"] || isPackageResolvable("@rsbuild/core", targetRoot);
1097
+ } else {
1098
+ hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, targetRoot);
731
1099
  }
732
- inject(options) {
733
- throw new Error("Esbuild requires manual plugin configuration");
1100
+ let detectedFile = "";
1101
+ let inferredFromScripts = false;
1102
+ if (pattern.tool === "esbuild" && !hasDep) {
1103
+ return null;
734
1104
  }
735
- getManualInstructions(detection, reason) {
736
- return [
737
- `1. Update your esbuild config (${detection.configPath}):`,
738
- `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
739
- "",
740
- "// Add to your plugins array:",
741
- `plugins: [`,
742
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
743
- ` ...otherPlugins`,
744
- `].filter(Boolean)`,
745
- "",
746
- "2. Initialize the client in your app entry (e.g., main.js / index.js):",
747
- `import { mountInspector } from '@inspecto-dev/core'`,
748
- "",
749
- "// Call this before your app renders",
750
- `if (process.env.NODE_ENV !== 'production') {`,
751
- ` mountInspector()`,
752
- `}`
753
- ];
1105
+ for (const file of pattern.files) {
1106
+ if (await exists(path6.join(targetRoot, file))) {
1107
+ detectedFile = file;
1108
+ break;
1109
+ }
754
1110
  }
755
- };
756
-
757
- // src/inject/strategies/rollup.ts
758
- var RollupStrategy = class {
759
- name = "Rollup";
760
- supports(tool) {
761
- return tool === "rollup";
1111
+ if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "rsbuild")) {
1112
+ for (const cmd of Object.values(scripts)) {
1113
+ if (cmd.includes("node ")) {
1114
+ const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
1115
+ if (match && match[1]) {
1116
+ if (await exists(path6.join(targetRoot, match[1]))) {
1117
+ if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
1118
+ detectedFile = match[1];
1119
+ break;
1120
+ }
1121
+ }
1122
+ }
1123
+ } else if (cmd.includes(`${pattern.tool} `)) {
1124
+ if (pattern.tool === "webpack" || pattern.tool === "rspack") {
1125
+ const configMatch = cmd.match(/--config\s+([^\s]+)/);
1126
+ if (configMatch && configMatch[1]) {
1127
+ if (await exists(path6.join(targetRoot, configMatch[1]))) {
1128
+ detectedFile = configMatch[1];
1129
+ break;
1130
+ }
1131
+ }
1132
+ }
1133
+ if (!detectedFile) {
1134
+ inferredFromScripts = true;
1135
+ detectedFile = "package.json (scripts)";
1136
+ break;
1137
+ }
1138
+ }
1139
+ }
762
1140
  }
763
- inject(options) {
764
- throw new Error("Rollup requires manual plugin configuration");
1141
+ if (!detectedFile) {
1142
+ if (hasDep && (pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "esbuild")) {
1143
+ return {
1144
+ tool: pattern.tool,
1145
+ configPath: "package.json (dependency)",
1146
+ label: `${pattern.label} (detected via dependency)`,
1147
+ packagePath: packagePath || void 0
1148
+ };
1149
+ }
1150
+ return null;
765
1151
  }
766
- getManualInstructions(detection, reason) {
767
- return [
768
- `1. Update your rollup config (${detection.configPath}):`,
769
- `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
770
- "",
771
- "// Add to your plugins array:",
772
- `plugins: [`,
773
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
774
- ` ...otherPlugins`,
775
- `].filter(Boolean)`,
776
- "",
777
- "2. Initialize the client in your app entry (e.g., main.js / index.js):",
778
- `import { mountInspector } from '@inspecto-dev/core'`,
779
- "",
780
- "// Call this before your app renders",
781
- `if (process.env.NODE_ENV !== 'production') {`,
782
- ` mountInspector()`,
783
- `}`
784
- ];
1152
+ let isLegacyRspack = false;
1153
+ let isLegacyWebpack = false;
1154
+ if (pattern.tool === "rspack") {
1155
+ const version = resolvedVersion;
1156
+ if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
1157
+ isLegacyRspack = true;
1158
+ }
1159
+ } else if (pattern.tool === "webpack") {
1160
+ const version = resolvedVersion;
1161
+ if (version && version.includes("^4") || version?.startsWith("4.")) {
1162
+ isLegacyWebpack = true;
1163
+ }
785
1164
  }
786
- };
1165
+ const absoluteConfig = path6.join(targetRoot, detectedFile);
1166
+ const relativeConfig = normalizeRelativePath(workspaceRoot, absoluteConfig);
1167
+ return {
1168
+ tool: pattern.tool,
1169
+ configPath: relativeConfig,
1170
+ label: `${pattern.label} (${relativeConfig})${isLegacyRspack ? " [Legacy]" : ""}${isLegacyWebpack ? " [Webpack 4]" : ""}${inferredFromScripts ? " [Scripts Detected]" : ""}`,
1171
+ isLegacyRspack,
1172
+ isLegacyWebpack,
1173
+ packagePath: packagePath || void 0
1174
+ };
1175
+ }
1176
+ function resolveInjectionTarget(detections) {
1177
+ if (detections.length === 0) return null;
1178
+ if (detections.length === 1) return detections[0];
1179
+ return "ambiguous";
1180
+ }
787
1181
 
788
- // src/inject/strategies/index.ts
789
- var STRATEGIES = [
790
- new ViteStrategy(),
791
- new WebpackStrategy(),
792
- new RspackStrategy(),
793
- new RsbuildStrategy(),
794
- new EsbuildStrategy(),
795
- new RollupStrategy()
1182
+ // src/detect/framework.ts
1183
+ import path7 from "path";
1184
+ import { createRequire as createRequire2 } from "module";
1185
+ var META_FRAMEWORK_MAP = {
1186
+ next: "react",
1187
+ nuxt: "vue",
1188
+ "@remix-run/react": "react",
1189
+ "@remix-run/dev": "react",
1190
+ "@vue/nuxt": "vue",
1191
+ "vite-plugin-vue": "vue",
1192
+ "@vitejs/plugin-vue": "vue",
1193
+ "@vitejs/plugin-react": "react",
1194
+ "@vitejs/plugin-react-swc": "react"
1195
+ };
1196
+ var SUPPORTED_FRAMEWORKS = [
1197
+ { framework: "react", deps: ["react", "react-dom"] },
1198
+ { framework: "vue", deps: ["vue"] }
796
1199
  ];
797
-
798
- // src/inject/ast-injector.ts
799
- function printManualInstructions(strategy, detection, reason) {
800
- log.warn(`Could not automatically configure ${detection.configPath}`);
801
- log.hint(`(reason: ${reason})`);
802
- log.blank();
803
- log.hint("Please add the following manually:");
804
- if (strategy) {
805
- const instructions = strategy.getManualInstructions(detection, reason);
806
- log.codeBlock(instructions);
807
- } else {
808
- log.error(`Unsupported build tool: ${detection.tool}`);
1200
+ var UNSUPPORTED_FRAMEWORKS = [
1201
+ { name: "Solid", dep: "solid-js" },
1202
+ { name: "Svelte", dep: "svelte" },
1203
+ { name: "Angular", dep: "@angular/core" },
1204
+ { name: "Preact", dep: "preact" },
1205
+ { name: "Lit", dep: "lit" }
1206
+ ];
1207
+ function isPackageResolvable2(pkgName, root) {
1208
+ try {
1209
+ const require2 = createRequire2(path7.join(root, "package.json"));
1210
+ try {
1211
+ require2.resolve(`${pkgName}/package.json`, { paths: [root] });
1212
+ return true;
1213
+ } catch {
1214
+ require2.resolve(pkgName, { paths: [root] });
1215
+ return true;
1216
+ }
1217
+ } catch {
1218
+ return false;
809
1219
  }
810
1220
  }
811
- function isAlreadyInjected(content) {
812
- const normalized = content.replace(/\s+/g, " ");
813
- const importPlugin = /import\s+(.+?)\s+from\s+['"]@inspecto-dev\/plugin['"]/g;
814
- const requirePlugin = /require\(['"]@inspecto-dev\/plugin['"]\)/;
815
- const legacyImport = /import\s+.*ai-dev-inspector/.test(normalized);
816
- const legacyRequire = /require\(['"]ai-dev-inspector['"]\)/.test(normalized);
817
- if (legacyImport || legacyRequire || requirePlugin.test(normalized)) return true;
818
- let match;
819
- importPlugin.lastIndex = 0;
820
- while (match = importPlugin.exec(normalized)) {
821
- const importClause = match[1] || "";
822
- if (/inspecto/.test(importClause) || /vitePlugin/.test(importClause)) {
823
- return true;
1221
+ async function detectFrameworks(root) {
1222
+ const pkg = await readJSON(path7.join(root, "package.json"));
1223
+ const allDeps = {
1224
+ ...pkg?.dependencies || {},
1225
+ ...pkg?.devDependencies || {},
1226
+ ...pkg?.peerDependencies || {}
1227
+ };
1228
+ const supportedSet = /* @__PURE__ */ new Set();
1229
+ const unsupported = [];
1230
+ const isTest = root.includes("/mock/root");
1231
+ for (const [metaPkg, framework] of Object.entries(META_FRAMEWORK_MAP)) {
1232
+ if (metaPkg in allDeps || !isTest && isPackageResolvable2(metaPkg, root)) {
1233
+ supportedSet.add(framework);
824
1234
  }
825
1235
  }
826
- return false;
1236
+ for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
1237
+ if (supportedSet.has(framework)) continue;
1238
+ for (const dep of deps) {
1239
+ if (dep in allDeps || !isTest && isPackageResolvable2(dep, root)) {
1240
+ supportedSet.add(framework);
1241
+ break;
1242
+ }
1243
+ }
1244
+ }
1245
+ for (const fw of UNSUPPORTED_FRAMEWORKS) {
1246
+ if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
1247
+ unsupported.push(fw);
1248
+ }
1249
+ }
1250
+ return {
1251
+ supported: Array.from(supportedSet),
1252
+ unsupported
1253
+ };
827
1254
  }
828
- async function injectPlugin(root, detection, dryRun) {
829
- const configPath = path7.join(root, detection.configPath);
830
- const mutations = [];
831
- const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
832
- const content = await readFile(configPath);
833
- if (!content) {
834
- printManualInstructions(strategy, detection, "config file not readable");
835
- return { success: false, mutations, failureReason: "config file not readable" };
1255
+
1256
+ // src/detect/ide.ts
1257
+ import path8 from "path";
1258
+ var SUPPORTED_IDE = "vscode";
1259
+ async function detectIDE(root) {
1260
+ const detected = /* @__PURE__ */ new Map();
1261
+ if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
1262
+ detected.set("Cursor", { ide: "cursor", supported: true });
836
1263
  }
837
- if (isAlreadyInjected(content)) {
838
- log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
839
- mutations.push({
840
- type: "file_modified",
841
- path: detection.configPath,
842
- description: "Previously configured inspecto() plugin"
843
- });
844
- return { success: true, mutations };
1264
+ 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")) {
1265
+ detected.set("Trae", { ide: "trae", supported: true });
1266
+ }
1267
+ if (process.env.ZED_TERM) {
1268
+ detected.set("Zed", { ide: "Zed", supported: false });
1269
+ }
1270
+ if (process.env.WINDSURF_APP_DIR || process.env.WINDSURF_CHANNEL || process.env.__CFBundleIdentifier === "com.codeium.windsurf" || process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes("windsurf")) {
1271
+ detected.set("Windsurf", { ide: "Windsurf", supported: false });
1272
+ }
1273
+ const [hasTrae, hasCursor, hasVscode, hasIdea] = await Promise.all([
1274
+ exists(path8.join(root, ".trae")),
1275
+ exists(path8.join(root, ".cursor")),
1276
+ exists(path8.join(root, ".vscode")),
1277
+ exists(path8.join(root, ".idea"))
1278
+ ]);
1279
+ if (hasTrae && !detected.has("Trae")) {
1280
+ detected.set("Trae", { ide: "trae", supported: true });
845
1281
  }
846
- if (!strategy) {
847
- printManualInstructions(
848
- strategy,
849
- detection,
850
- `No injection strategy found for ${detection.tool}`
851
- );
852
- return { success: false, mutations, failureReason: "No strategy found" };
1282
+ if (hasCursor && !detected.has("Cursor")) {
1283
+ detected.set("Cursor", { ide: "cursor", supported: true });
853
1284
  }
854
- if (dryRun) {
855
- log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
856
- return { success: true, mutations: [] };
1285
+ if (hasVscode && !detected.has("vscode")) {
1286
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
857
1287
  }
858
- try {
859
- const mod = await loadFile(configPath);
860
- strategy.inject({
861
- mod,
862
- detection
863
- });
864
- await writeAstFile(mod, configPath);
865
- mutations.push({
866
- type: "file_modified",
867
- path: detection.configPath,
868
- description: "Automatically configured inspecto() plugin"
869
- });
870
- log.success(`Configured plugin in ${detection.configPath}`);
871
- return { success: true, mutations };
872
- } catch (err) {
873
- printManualInstructions(
874
- strategy,
875
- detection,
876
- `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`
877
- );
878
- return {
879
- success: false,
880
- mutations,
881
- failureReason: "Automatic configuration unavailable"
882
- };
1288
+ if (hasIdea && !detected.has("JetBrains IDE")) {
1289
+ detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
1290
+ }
1291
+ if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
1292
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
883
1293
  }
1294
+ return {
1295
+ detected: Array.from(detected.values())
1296
+ };
884
1297
  }
885
1298
 
886
- // src/inject/gitignore.ts
887
- import path8 from "path";
888
- var DEFAULT_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
889
- var SHARED_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
890
- async function updateGitignore(root, shared, dryRun) {
891
- const gitignorePath = path8.join(root, ".gitignore");
892
- let content = await readFile(gitignorePath) ?? "";
893
- const desiredRules = shared ? SHARED_RULES : DEFAULT_RULES;
894
- const hasGlobalRule = content.match(/^\.inspecto\/\s*$/m) !== null;
895
- if (hasGlobalRule) {
896
- content = content.replace(/^\.inspecto\/\s*$/gm, SHARED_RULES.join("\n"));
897
- if (!dryRun) {
898
- await writeFile(gitignorePath, content);
1299
+ // src/detect/provider.ts
1300
+ import path9 from "path";
1301
+ var KNOWN_CLI_TOOLS = [
1302
+ { id: "claude-code", bin: "claude", label: "Claude Code", supported: true },
1303
+ { id: "coco", bin: "coco", label: "Trae CLI (Coco)", supported: true },
1304
+ { id: "codex", bin: "codex", label: "Codex CLI", supported: true },
1305
+ { id: "gemini", bin: "gemini", label: "Gemini CLI", supported: true }
1306
+ ];
1307
+ var KNOWN_IDE_PLUGINS = [
1308
+ { id: "claude-code", extId: "anthropic.claude-code", label: "Claude Code", supported: true },
1309
+ { id: "copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
1310
+ { id: "codex", extId: "openai.chatgpt", label: "Codex (ChatGPT)", supported: true },
1311
+ { id: "gemini", extId: "google.geminicodeassist", label: "Gemini Code Assist", supported: true }
1312
+ ];
1313
+ async function detectProviders(root) {
1314
+ const detectedMap = /* @__PURE__ */ new Map();
1315
+ const cliChecks = KNOWN_CLI_TOOLS.map(async (tool) => {
1316
+ if (await which(tool.bin)) {
1317
+ detectedMap.set(tool.id, {
1318
+ id: tool.id,
1319
+ label: tool.label,
1320
+ supported: tool.supported,
1321
+ providerModes: ["cli"],
1322
+ preferredMode: "cli"
1323
+ });
1324
+ }
1325
+ });
1326
+ await Promise.all(cliChecks);
1327
+ const extensionsJsonPath = path9.join(root, ".vscode", "extensions.json");
1328
+ let recommendedExts = [];
1329
+ if (await exists(extensionsJsonPath)) {
1330
+ try {
1331
+ const extData = await readJSON(extensionsJsonPath);
1332
+ if (extData && Array.isArray(extData.recommendations)) {
1333
+ recommendedExts = extData.recommendations.map((e) => e.toLowerCase());
1334
+ }
1335
+ } catch {
899
1336
  }
900
- log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
901
- return;
902
1337
  }
903
- const missingRules = desiredRules.filter((rule) => !content.includes(rule));
904
- if (missingRules.length === 0) {
905
- return;
1338
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "";
1339
+ const globalExtDir = path9.join(homeDir, ".vscode", "extensions");
1340
+ const globalExtExists = await exists(globalExtDir);
1341
+ let installedExtensionFolders = [];
1342
+ if (globalExtExists) {
1343
+ try {
1344
+ const { readdir } = await import("fs/promises");
1345
+ installedExtensionFolders = await readdir(globalExtDir);
1346
+ const obsoletePath = path9.join(globalExtDir, ".obsolete");
1347
+ if (await exists(obsoletePath)) {
1348
+ try {
1349
+ const obsoleteData = await readJSON(obsoletePath);
1350
+ if (obsoleteData) {
1351
+ const obsoleteKeys = Object.keys(obsoleteData);
1352
+ installedExtensionFolders = installedExtensionFolders.filter((folder) => {
1353
+ return !obsoleteKeys.includes(folder);
1354
+ });
1355
+ }
1356
+ } catch {
1357
+ }
1358
+ }
1359
+ } catch {
1360
+ }
906
1361
  }
907
- const section = "\n# Inspecto\n" + missingRules.join("\n") + "\n";
908
- content = content.trimEnd() + "\n" + section;
909
- if (dryRun) {
910
- log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
911
- } else {
912
- await writeFile(gitignorePath, content);
913
- log.success("Updated .gitignore");
1362
+ for (const plugin of KNOWN_IDE_PLUGINS) {
1363
+ let isInstalled = false;
1364
+ if (recommendedExts.includes(plugin.extId.toLowerCase())) {
1365
+ isInstalled = true;
1366
+ } else if (installedExtensionFolders.some((f) => {
1367
+ const lower = f.toLowerCase();
1368
+ return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
1369
+ })) {
1370
+ isInstalled = true;
1371
+ }
1372
+ if (isInstalled) {
1373
+ const existing = detectedMap.get(plugin.id);
1374
+ if (existing) {
1375
+ existing.providerModes.push("extension");
1376
+ existing.preferredMode = "extension";
1377
+ } else {
1378
+ detectedMap.set(plugin.id, {
1379
+ id: plugin.id,
1380
+ label: plugin.label,
1381
+ supported: plugin.supported,
1382
+ providerModes: ["extension"],
1383
+ preferredMode: "extension"
1384
+ });
1385
+ }
1386
+ }
914
1387
  }
1388
+ return { detected: Array.from(detectedMap.values()) };
915
1389
  }
916
- async function cleanGitignore(root) {
917
- const gitignorePath = path8.join(root, ".gitignore");
918
- const content = await readFile(gitignorePath);
919
- if (!content) return;
920
- 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");
921
- await writeFile(gitignorePath, cleaned);
1390
+
1391
+ // src/onboarding/context.ts
1392
+ async function buildOnboardingContext(root) {
1393
+ const [packageManager, buildTools, frameworks, ides, providers] = await Promise.all([
1394
+ detectPackageManager(root),
1395
+ detectBuildTools(root),
1396
+ detectFrameworks(root),
1397
+ detectIDE(root),
1398
+ detectProviders(root)
1399
+ ]);
1400
+ return {
1401
+ root,
1402
+ packageManager,
1403
+ buildTools: {
1404
+ supported: buildTools.supported,
1405
+ unsupported: buildTools.unsupported
1406
+ },
1407
+ frameworks: {
1408
+ supported: frameworks.supported,
1409
+ unsupported: frameworks.unsupported.map((item) => item.name)
1410
+ },
1411
+ ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
1412
+ providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
1413
+ id,
1414
+ label,
1415
+ supported,
1416
+ preferredMode
1417
+ }))
1418
+ };
922
1419
  }
923
1420
 
924
- // src/inject/extension.ts
925
- var EXTENSION_ID = "inspecto.inspecto";
926
- var VSCODE_PATHS = {
927
- darwin: [
928
- "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
929
- "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
930
- `${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`
931
- ],
932
- linux: ["/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code", "/usr/bin/code-insiders"],
933
- win32: [
934
- `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
935
- `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
936
- `${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`
937
- ]
938
- };
939
- async function findVSCodeBinary() {
940
- const platform = process.platform;
941
- const candidates = VSCODE_PATHS[platform] || [];
942
- for (const candidate of candidates) {
943
- if (await exists(candidate)) {
944
- return candidate;
1421
+ // src/onboarding/planner.ts
1422
+ function message(code, message2) {
1423
+ return { code, message: message2 };
1424
+ }
1425
+ function uniqueMessages(messages) {
1426
+ const seen = /* @__PURE__ */ new Set();
1427
+ return messages.filter((item) => {
1428
+ const key = `${item.code}:${item.message}`;
1429
+ if (seen.has(key)) return false;
1430
+ seen.add(key);
1431
+ return true;
1432
+ });
1433
+ }
1434
+ function detectionStatus(warnings, blockers) {
1435
+ if (blockers.length > 0) return "blocked";
1436
+ if (warnings.length > 0) return "warning";
1437
+ return "ok";
1438
+ }
1439
+ function planStatus(warnings, blockers) {
1440
+ if (blockers.length > 0) return "blocked";
1441
+ if (warnings.length > 0) return "warning";
1442
+ return "ok";
1443
+ }
1444
+ function supportedIde(context) {
1445
+ return context.ides.find((ide) => ide.supported)?.ide;
1446
+ }
1447
+ function supportedProvider(context) {
1448
+ return context.providers.find((provider) => provider.supported)?.id;
1449
+ }
1450
+ function buildToolBlockers(context) {
1451
+ if (context.buildTools.unsupported.length > 0) {
1452
+ return [
1453
+ message(
1454
+ "unsupported-build-tool",
1455
+ `Detected unsupported build tool(s): ${context.buildTools.unsupported.join(", ")}`
1456
+ )
1457
+ ];
1458
+ }
1459
+ if (context.buildTools.supported.length > 0) {
1460
+ if (context.buildTools.supported.length === 1) {
1461
+ return [];
945
1462
  }
1463
+ const targets = context.buildTools.supported.map((target) => target.packagePath ?? target.configPath).join(", ");
1464
+ return [
1465
+ message(
1466
+ "multiple-supported-build-targets",
1467
+ `Multiple supported build targets detected: ${targets}. Run inspecto apply from a single app/package root until explicit target selection is available.`
1468
+ )
1469
+ ];
946
1470
  }
947
- if (await which("code-insiders")) {
948
- return "code-insiders";
1471
+ return [message("missing-build-tool", "No supported build tool detected")];
1472
+ }
1473
+ function frameworkBlockers(context) {
1474
+ if (context.frameworks.supported.length > 0) {
1475
+ return [];
949
1476
  }
950
- return null;
1477
+ if (context.frameworks.unsupported.length > 0) {
1478
+ return [
1479
+ message(
1480
+ "unsupported-framework",
1481
+ `Detected unsupported framework(s): ${context.frameworks.unsupported.join(", ")}`
1482
+ )
1483
+ ];
1484
+ }
1485
+ return [message("missing-framework", "No supported frontend framework detected")];
951
1486
  }
952
- async function tryOpenURI(uri) {
953
- try {
954
- const platform = process.platform;
955
- if (platform === "win32") {
956
- await shell(`cmd /c start "" "${uri}"`);
957
- } else {
958
- const openCmd = platform === "darwin" ? "open" : "xdg-open";
959
- await shell(`${openCmd} "${uri}"`);
960
- }
961
- return true;
962
- } catch {
963
- return false;
1487
+ function unsupportedEnvironmentWarnings(context) {
1488
+ const warnings = [];
1489
+ if (context.frameworks.unsupported.length > 0 && context.frameworks.supported.length > 0) {
1490
+ warnings.push(
1491
+ message(
1492
+ "unsupported-framework-present",
1493
+ `Unsupported framework(s) also detected: ${context.frameworks.unsupported.join(", ")}`
1494
+ )
1495
+ );
1496
+ }
1497
+ const unsupportedIdes = context.ides.filter((ide) => !ide.supported).map((ide) => ide.ide);
1498
+ if (unsupportedIdes.length > 0) {
1499
+ warnings.push(message("unsupported-ide", `Unsupported IDE(s) detected: ${unsupportedIdes.join(", ")}`));
1500
+ }
1501
+ const unsupportedProviders = context.providers.filter((provider) => !provider.supported).map((provider) => provider.label);
1502
+ if (unsupportedProviders.length > 0) {
1503
+ warnings.push(
1504
+ message(
1505
+ "unsupported-provider",
1506
+ `Unsupported provider(s) detected: ${unsupportedProviders.join(", ")}`
1507
+ )
1508
+ );
964
1509
  }
1510
+ return warnings;
965
1511
  }
966
- async function installExtension(dryRun, ide) {
967
- if (dryRun) {
968
- log.dryRun("Would attempt to install VS Code extension");
969
- return null;
1512
+ function manualBuildToolActions(context) {
1513
+ if (context.buildTools.unsupported.length > 0) {
1514
+ return [
1515
+ {
1516
+ type: "manual_step",
1517
+ target: context.buildTools.unsupported.join(", "),
1518
+ description: "Inspecto cannot auto-configure this build stack yet. Follow the manual setup guide for the detected framework or build tool."
1519
+ }
1520
+ ];
970
1521
  }
971
- const isVSCode = !ide || ide === "vscode";
972
- if (isVSCode) {
973
- if (await which("code")) {
974
- try {
975
- await run("code", ["--install-extension", EXTENSION_ID]);
976
- log.success("VS Code extension installed via CLI");
977
- return { type: "extension_installed", id: EXTENSION_ID };
978
- } catch {
1522
+ if (context.buildTools.supported.length > 1) {
1523
+ const targets = context.buildTools.supported.map((target) => target.packagePath ?? target.configPath).join(", ");
1524
+ return [
1525
+ {
1526
+ type: "manual_step",
1527
+ target: targets,
1528
+ description: "Run inspecto apply from the target app/package root. Root-level apply is blocked when multiple supported targets are detected."
979
1529
  }
980
- }
981
- const codePath = await findVSCodeBinary();
982
- if (codePath) {
983
- try {
984
- await run(codePath, ["--install-extension", EXTENSION_ID]);
985
- log.success("VS Code extension installed via binary path");
986
- log.info(
987
- 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
988
- );
989
- return { type: "extension_installed", id: EXTENSION_ID };
990
- } catch {
1530
+ ];
1531
+ }
1532
+ return [
1533
+ {
1534
+ type: "manual_step",
1535
+ target: context.root,
1536
+ description: "No supported build tool was detected. Add a supported build config before trying Inspecto again."
1537
+ }
1538
+ ];
1539
+ }
1540
+ function manualFrameworkActions(context) {
1541
+ if (context.frameworks.unsupported.length > 0) {
1542
+ return [
1543
+ {
1544
+ type: "manual_step",
1545
+ target: context.frameworks.unsupported.join(", "),
1546
+ description: "Inspecto cannot auto-configure this framework yet. Follow the manual setup guide for the detected framework."
991
1547
  }
1548
+ ];
1549
+ }
1550
+ return [
1551
+ {
1552
+ type: "manual_step",
1553
+ target: context.root,
1554
+ description: "No supported frontend framework was detected. Add a supported React or Vue app before trying Inspecto again."
992
1555
  }
993
- const uri = `vscode:extension/${EXTENSION_ID}`;
994
- if (await tryOpenURI(uri)) {
995
- log.warn("Opened extension page in VS Code");
996
- log.hint('Please click "Install" in the opened VS Code window to complete setup.');
997
- return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
1556
+ ];
1557
+ }
1558
+ async function createDetectionResult(root) {
1559
+ const context = await buildOnboardingContext(root);
1560
+ const warnings = uniqueMessages([...unsupportedEnvironmentWarnings(context)]);
1561
+ const buildToolResult = buildToolBlockers(context);
1562
+ const frameworkResult = frameworkBlockers(context);
1563
+ const blockers = uniqueMessages([...buildToolResult, ...frameworkResult]);
1564
+ return {
1565
+ status: detectionStatus(warnings, blockers),
1566
+ warnings,
1567
+ blockers,
1568
+ project: {
1569
+ root: context.root,
1570
+ packageManager: context.packageManager
1571
+ },
1572
+ environment: {
1573
+ frameworks: context.frameworks.supported,
1574
+ unsupportedFrameworks: context.frameworks.unsupported,
1575
+ buildTools: context.buildTools.supported,
1576
+ unsupportedBuildTools: context.buildTools.unsupported,
1577
+ ides: context.ides,
1578
+ providers: context.providers
998
1579
  }
999
- log.warn("Could not auto-install VS Code extension");
1000
- log.hint("Please install it manually to enable Inspector features:");
1001
- log.hint(" 1. Open VS Code");
1002
- log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
1003
- log.hint(' 3. Search for "Inspecto"');
1004
- log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
1005
- return null;
1006
- }
1007
- log.warn(`Could not auto-install extension for ${ide}`);
1008
- log.hint("Please install it manually to enable Inspector features:");
1009
- log.hint(" 1. Download the latest .vsix file from Inspecto releases");
1010
- log.hint(` 2. Open ${ide}`);
1011
- log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
1012
- log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
1013
- log.hint(" 5. Select the downloaded .vsix file");
1014
- return null;
1580
+ };
1015
1581
  }
1016
- async function isExtensionInstalled() {
1017
- try {
1018
- if (await which("code")) {
1019
- const { stdout } = await run("code", ["--list-extensions"]);
1020
- return stdout.toLowerCase().includes(EXTENSION_ID);
1582
+ function createPlanResult(context) {
1583
+ const warnings = uniqueMessages(unsupportedEnvironmentWarnings(context));
1584
+ const blockers = uniqueMessages([...buildToolBlockers(context), ...frameworkBlockers(context)]);
1585
+ const actions = [];
1586
+ let strategy = "supported";
1587
+ if (blockers.length > 0) {
1588
+ strategy = "manual";
1589
+ if (context.buildTools.unsupported.length > 0 || context.buildTools.supported.length === 0 || context.buildTools.supported.length > 1) {
1590
+ actions.push(...manualBuildToolActions(context));
1021
1591
  }
1022
- const codePath = await findVSCodeBinary();
1023
- if (codePath) {
1024
- const { stdout } = await run(codePath, ["--list-extensions"]);
1025
- return stdout.toLowerCase().includes(EXTENSION_ID);
1592
+ if (frameworkBlockers(context).length > 0) {
1593
+ actions.push(...manualFrameworkActions(context));
1026
1594
  }
1027
- return false;
1028
- } catch {
1029
- return false;
1595
+ } else {
1596
+ actions.push({
1597
+ type: "install_dependency",
1598
+ target: "@inspecto-dev/plugin @inspecto-dev/core",
1599
+ description: `Install the Inspecto runtime packages with ${context.packageManager}.`
1600
+ });
1601
+ for (const buildTool of context.buildTools.supported) {
1602
+ actions.push({
1603
+ type: "modify_file",
1604
+ target: buildTool.configPath,
1605
+ description: `Inject the Inspecto plugin into ${buildTool.label}.`
1606
+ });
1607
+ }
1608
+ const ide2 = supportedIde(context);
1609
+ if (ide2 === "vscode") {
1610
+ actions.push({
1611
+ type: "install_extension",
1612
+ target: "vscode",
1613
+ description: "Install the Inspecto VS Code extension."
1614
+ });
1615
+ }
1616
+ }
1617
+ const defaults = {
1618
+ shared: false,
1619
+ extension: supportedIde(context) === "vscode"
1620
+ };
1621
+ const provider = supportedProvider(context);
1622
+ if (provider) {
1623
+ defaults.provider = provider;
1624
+ }
1625
+ const ide = supportedIde(context);
1626
+ if (ide) {
1627
+ defaults.ide = ide;
1628
+ }
1629
+ return {
1630
+ status: planStatus(warnings, blockers),
1631
+ warnings,
1632
+ blockers,
1633
+ strategy,
1634
+ actions,
1635
+ defaults
1636
+ };
1637
+ }
1638
+
1639
+ // src/commands/apply.ts
1640
+ function getProviderDefault(providerId, preferredMode) {
1641
+ if (!providerId) return void 0;
1642
+ const mode = preferredMode ?? (providerId === "coco" ? "cli" : "extension");
1643
+ return `${providerId}.${mode}`;
1644
+ }
1645
+ function statusRank(status) {
1646
+ switch (status) {
1647
+ case "error":
1648
+ return 3;
1649
+ case "blocked":
1650
+ return 2;
1651
+ case "warning":
1652
+ return 1;
1653
+ case "ok":
1654
+ default:
1655
+ return 0;
1656
+ }
1657
+ }
1658
+ function mergeStatus(planStatus2, applyStatus) {
1659
+ return statusRank(planStatus2) >= statusRank(applyStatus) ? planStatus2 : applyStatus;
1660
+ }
1661
+ function printApplyResult(result) {
1662
+ const manualSteps = result.postInstall.nextSteps.filter(
1663
+ (step) => !result.plan.blockers.some((blocker) => blocker.message === step)
1664
+ );
1665
+ log.header("Inspecto Apply");
1666
+ log.info(`Status: ${result.status}`);
1667
+ log.info(`Strategy: ${result.plan.strategy}`);
1668
+ for (const blocker of result.plan.blockers) {
1669
+ log.error(blocker.message);
1670
+ }
1671
+ for (const warning of result.plan.warnings) {
1672
+ log.warn(warning.message);
1673
+ }
1674
+ if (manualSteps.length > 0 || result.plan.blockers.length > 0) {
1675
+ log.blank();
1676
+ log.warn("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Manual Steps Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1677
+ manualSteps.forEach((step) => log.error(step));
1678
+ return;
1679
+ }
1680
+ if (result.plan.warnings.length > 0) {
1681
+ return;
1682
+ }
1683
+ log.ready("Ready! Hold Alt + Click any element to inspect.");
1684
+ }
1685
+ async function apply(options = {}) {
1686
+ const root = process.cwd();
1687
+ const context = await buildOnboardingContext(root);
1688
+ const plan2 = createPlanResult(context);
1689
+ const selectedProvider = context.providers.find((provider) => provider.id === plan2.defaults.provider) ?? null;
1690
+ const selectedIDE = context.ides.find((ide) => ide.ide === plan2.defaults.ide) ?? context.ides.find((ide) => ide.supported) ?? null;
1691
+ const applyResult = await applyOnboardingPlan({
1692
+ repoRoot: root,
1693
+ projectRoot: root,
1694
+ packageManager: context.packageManager,
1695
+ supportedBuildTargets: context.buildTools.supported,
1696
+ options: {
1697
+ shared: options.shared ?? plan2.defaults.shared,
1698
+ skipInstall: options.skipInstall ?? false,
1699
+ dryRun: options.dryRun ?? false,
1700
+ noExtension: options.noExtension ?? !plan2.defaults.extension,
1701
+ quiet: options.json ?? false
1702
+ },
1703
+ selectedIDE,
1704
+ providerDefault: getProviderDefault(plan2.defaults.provider, selectedProvider?.preferredMode),
1705
+ plan: plan2
1706
+ });
1707
+ const result = {
1708
+ ...applyResult,
1709
+ status: mergeStatus(plan2.status, applyResult.status),
1710
+ plan: plan2
1711
+ };
1712
+ return writeCommandOutput(result, options.json ?? false, printApplyResult);
1713
+ }
1714
+
1715
+ // src/commands/detect.ts
1716
+ function printDetectionResult(result) {
1717
+ const suppressedCodes = /* @__PURE__ */ new Set([
1718
+ "unsupported-build-tool",
1719
+ "unsupported-build-tool-present",
1720
+ "unsupported-framework",
1721
+ "unsupported-framework-present"
1722
+ ]);
1723
+ log.header("Inspecto Detect");
1724
+ log.info(`Status: ${result.status}`);
1725
+ log.info(`Root: ${result.project.root}`);
1726
+ log.info(`Package manager: ${result.project.packageManager}`);
1727
+ if (result.environment.frameworks.length > 0) {
1728
+ log.success(`Supported frameworks: ${result.environment.frameworks.join(", ")}`);
1729
+ }
1730
+ if (result.environment.unsupportedFrameworks.length > 0) {
1731
+ log.warn(`Unsupported frameworks: ${result.environment.unsupportedFrameworks.join(", ")}`);
1732
+ }
1733
+ if (result.environment.buildTools.length > 0) {
1734
+ log.success(
1735
+ `Supported build tools: ${result.environment.buildTools.map((tool) => tool.label).join(", ")}`
1736
+ );
1737
+ }
1738
+ if (result.environment.unsupportedBuildTools.length > 0) {
1739
+ log.warn(`Unsupported build tools: ${result.environment.unsupportedBuildTools.join(", ")}`);
1740
+ }
1741
+ const supportedIdes = result.environment.ides.filter((ide) => ide.supported).map((ide) => ide.ide);
1742
+ if (supportedIdes.length > 0) {
1743
+ log.success(`Supported IDEs: ${supportedIdes.join(", ")}`);
1744
+ }
1745
+ const supportedProviders = result.environment.providers.filter((provider) => provider.supported).map((provider) => provider.label);
1746
+ if (supportedProviders.length > 0) {
1747
+ log.success(`Supported providers: ${supportedProviders.join(", ")}`);
1748
+ }
1749
+ for (const blocker of result.blockers) {
1750
+ if (suppressedCodes.has(blocker.code)) continue;
1751
+ log.error(blocker.message);
1030
1752
  }
1753
+ for (const warning of result.warnings) {
1754
+ if (suppressedCodes.has(warning.code)) continue;
1755
+ log.warn(warning.message);
1756
+ }
1757
+ }
1758
+ async function detect(json = false) {
1759
+ const result = await createDetectionResult(process.cwd());
1760
+ return writeCommandOutput(result, json, printDetectionResult);
1031
1761
  }
1032
1762
 
1763
+ // src/commands/init.ts
1764
+ import path10 from "path";
1765
+
1033
1766
  // src/prompts.ts
1034
1767
  import prompts from "prompts";
1035
1768
  async function promptIDEChoice(detections) {
@@ -1069,9 +1802,9 @@ async function promptProviderChoice(detections) {
1069
1802
  title: `${d.label} ${d.supported ? `(supported ${modeStr})` : "(unsupported/limited)"}`,
1070
1803
  value: i
1071
1804
  };
1072
- })
1805
+ }).concat({ title: "Skip (configure later)", value: -1 })
1073
1806
  });
1074
- if (choice === void 0) return null;
1807
+ if (choice === void 0 || choice === -1) return null;
1075
1808
  return detections[choice];
1076
1809
  }
1077
1810
  async function promptConfigChoice(detections) {
@@ -1097,6 +1830,33 @@ async function promptConfigChoice(detections) {
1097
1830
  if (choice === void 0 || choice === -1) return null;
1098
1831
  return detections[choice];
1099
1832
  }
1833
+ async function promptMonorepoPackageChoice(detections) {
1834
+ const uniquePackages = Array.from(
1835
+ new Map(
1836
+ detections.filter((d) => !!d.packagePath).map((d) => [d.packagePath, d])
1837
+ ).values()
1838
+ );
1839
+ if (uniquePackages.length === 0) {
1840
+ return null;
1841
+ }
1842
+ if (!process.stdin.isTTY) {
1843
+ log.error("Monorepo root detected, but stdin is not interactive.");
1844
+ log.hint("Re-run `inspecto init` inside a specific app directory, or pass --packages <app-path>.");
1845
+ return null;
1846
+ }
1847
+ const { choice } = await prompts({
1848
+ type: "select",
1849
+ name: "choice",
1850
+ message: "Monorepo root detected. Choose the app to initialize:",
1851
+ choices: uniquePackages.map((d, i) => ({
1852
+ title: `${d.packagePath} (${d.tool})`,
1853
+ description: d.configPath,
1854
+ value: i
1855
+ }))
1856
+ });
1857
+ if (choice === void 0) return null;
1858
+ return uniquePackages[choice].packagePath;
1859
+ }
1100
1860
  async function promptUnsupportedFrameworkContinue() {
1101
1861
  if (!process.stdin.isTTY) {
1102
1862
  log.error("Unsupported framework detected in non-interactive environment.");
@@ -1115,60 +1875,75 @@ async function promptUnsupportedFrameworkContinue() {
1115
1875
  // src/instructions.ts
1116
1876
  function printNuxtManualInstructions() {
1117
1877
  log.blank();
1118
- log.hint("To enable Inspecto in Nuxt, update your nuxt.config.ts:");
1119
- console.log(`\x1B[36m
1120
- import { vitePlugin as inspecto } from '@inspecto-dev/plugin'
1121
- export default defineNuxtConfig({
1122
- vite: {
1123
- plugins: [inspecto()]
1124
- }
1125
- })
1126
- \x1B[0m`);
1127
- log.hint("And create a Nuxt plugin at plugins/inspecto.client.ts:");
1128
- console.log(`\x1B[36m
1129
- export default defineNuxtPlugin(() => {
1130
- if (import.meta.dev) {
1131
- import('@inspecto-dev/core').then(({ mountInspector }) => {
1132
- mountInspector()
1133
- })
1134
- }
1135
- })
1136
- \x1B[0m`);
1878
+ log.hint("Nuxt requires manual setup in the current version.");
1879
+ log.hint("1. Update `nuxt.config.ts` to register the Inspecto Vite plugin:");
1880
+ log.copyableCodeBlock([
1881
+ "import { vitePlugin as inspecto } from '@inspecto-dev/plugin'",
1882
+ "",
1883
+ "export default defineNuxtConfig({",
1884
+ " vite: {",
1885
+ " plugins: [inspecto()],",
1886
+ " },",
1887
+ "})"
1888
+ ]);
1889
+ log.hint("2. Create `plugins/inspecto.client.ts` to mount `@inspecto-dev/core` in development:");
1890
+ log.copyableCodeBlock([
1891
+ "export default defineNuxtPlugin(() => {",
1892
+ " if (import.meta.dev) {",
1893
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
1894
+ " mountInspector()",
1895
+ " })",
1896
+ " }",
1897
+ "})"
1898
+ ]);
1899
+ log.hint("3. Restart your Nuxt dev server after updating the config.");
1137
1900
  }
1138
1901
  function printNextJsManualInstructions() {
1139
1902
  log.blank();
1140
- log.hint("To enable Inspecto in Next.js, update your next.config.mjs:");
1141
- console.log(`\x1B[36m
1142
- import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'
1143
- const nextConfig = {
1144
- webpack: (config, { dev, isServer }) => {
1145
- if (dev && !isServer) config.plugins.push(inspecto())
1146
- return config
1147
- }
1148
- }
1149
- export default nextConfig
1150
- \x1B[0m`);
1151
- log.hint("And initialize the client dynamically in your app/layout.tsx (or pages/_app.tsx):");
1152
- console.log(`\x1B[36m
1153
- 'use client'
1154
- import { useEffect } from 'react'
1155
-
1156
- export default function RootLayout({ children }) {
1157
- useEffect(() => {
1158
- if (process.env.NODE_ENV !== 'production') {
1159
- import('@inspecto-dev/core').then(({ mountInspector }) => {
1160
- mountInspector({ serverUrl: 'http://127.0.0.1:5678' })
1161
- })
1162
- }
1163
- }, [])
1164
- return <html><body>{children}</body></html>
1165
- }
1166
- \x1B[0m`);
1903
+ log.hint("Next.js requires manual setup in the current version.");
1904
+ log.hint("1. Update `next.config.mjs` to register the Inspecto webpack plugin:");
1905
+ log.copyableCodeBlock([
1906
+ "import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'",
1907
+ "",
1908
+ "/** @type {import('next').NextConfig} */",
1909
+ "const nextConfig = {",
1910
+ " webpack: (config, { dev, isServer }) => {",
1911
+ " if (dev && !isServer) {",
1912
+ " config.plugins.push(inspecto())",
1913
+ " }",
1914
+ " return config",
1915
+ " },",
1916
+ "}",
1917
+ "",
1918
+ "export default nextConfig"
1919
+ ]);
1920
+ log.hint(
1921
+ "2. Initialize `@inspecto-dev/core` from a client component such as `app/layout.tsx` or `pages/_app.tsx`:"
1922
+ );
1923
+ log.copyableCodeBlock([
1924
+ "'use client'",
1925
+ "",
1926
+ "import { useEffect } from 'react'",
1927
+ "",
1928
+ "export default function RootLayout({ children }) {",
1929
+ " useEffect(() => {",
1930
+ " if (process.env.NODE_ENV !== 'production') {",
1931
+ " import('@inspecto-dev/core').then(({ mountInspector }) => {",
1932
+ " mountInspector({ serverUrl: 'http://127.0.0.1:5678' })",
1933
+ " })",
1934
+ " }",
1935
+ " }, [])",
1936
+ "",
1937
+ " return <html><body>{children}</body></html>",
1938
+ "}"
1939
+ ]);
1940
+ log.hint("3. Restart your Next.js dev server after updating the config.");
1167
1941
  }
1168
1942
 
1169
1943
  // src/commands/init.ts
1170
1944
  async function init(options) {
1171
- const root = process.cwd();
1945
+ const repoRoot = process.cwd();
1946
+ let projectRoot = repoRoot;
1172
1947
  const mutations = [];
1173
1948
  const normalizedPackages = normalizePackageList(options.packages);
1174
1949
  const verifiedPackages = [];
@@ -1177,7 +1952,7 @@ async function init(options) {
1177
1952
  verifiedPackages.push(pkg);
1178
1953
  continue;
1179
1954
  }
1180
- const absolutePath = path9.join(root, pkg);
1955
+ const absolutePath = path10.join(repoRoot, pkg);
1181
1956
  if (await exists(absolutePath)) {
1182
1957
  verifiedPackages.push(pkg);
1183
1958
  } else {
@@ -1190,21 +1965,54 @@ async function init(options) {
1190
1965
  return;
1191
1966
  }
1192
1967
  log.header("Inspecto Setup");
1193
- if (!await exists(path9.join(root, "package.json"))) {
1968
+ if (!await exists(path10.join(repoRoot, "package.json"))) {
1194
1969
  log.error("No package.json found in current directory");
1195
1970
  log.hint("Run this command from your project root");
1196
1971
  return;
1197
1972
  }
1198
- const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
1199
- detectPackageManager(root),
1200
- detectFrameworks(root),
1201
- detectBuildTools(root, verifiedPackages.length > 0 ? verifiedPackages : void 0),
1202
- detectIDE(root),
1203
- detectProviders(root)
1973
+ const pm = await detectPackageManager(repoRoot);
1974
+ let buildResult = await detectBuildTools(
1975
+ repoRoot,
1976
+ verifiedPackages.length > 0 ? verifiedPackages : void 0
1977
+ );
1978
+ if (verifiedPackages.length === 0) {
1979
+ const monorepoTargets = Array.from(
1980
+ new Set(
1981
+ buildResult.supported.map((d) => d.packagePath).filter((value) => !!value)
1982
+ )
1983
+ );
1984
+ if (monorepoTargets.length > 1) {
1985
+ log.warn("Monorepo root detected with multiple candidate apps.");
1986
+ const selectedPackage = await promptMonorepoPackageChoice(buildResult.supported);
1987
+ if (!selectedPackage) {
1988
+ log.hint("Run `inspecto init` inside the target app, or pass --packages <app-path>.");
1989
+ return;
1990
+ }
1991
+ projectRoot = path10.join(repoRoot, selectedPackage);
1992
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
1993
+ log.info(`Continuing initialization in ${selectedPackage}`);
1994
+ } else if (monorepoTargets.length === 1) {
1995
+ const [selectedPackage] = monorepoTargets;
1996
+ projectRoot = path10.join(repoRoot, selectedPackage);
1997
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
1998
+ log.warn(`Monorepo root detected. Using the only candidate app: ${selectedPackage}`);
1999
+ log.hint("Run `inspecto init` inside that app next time to skip this prompt.");
2000
+ }
2001
+ }
2002
+ const [frameworkResult, ideProbe, providerProbe] = await Promise.all([
2003
+ detectFrameworks(projectRoot),
2004
+ detectIDE(projectRoot),
2005
+ detectProviders(projectRoot)
1204
2006
  ]);
1205
2007
  log.success(`Detected package manager: ${pm}`);
1206
2008
  if (frameworkResult.supported.length > 0) {
1207
- log.success(`Detected framework: ${frameworkResult.supported.join(", ")}`);
2009
+ const frameworks = frameworkResult.supported.join(", ");
2010
+ log.success(`Detected framework: ${frameworks}`);
2011
+ if (frameworkResult.unsupported.length > 0) {
2012
+ log.hint(
2013
+ `Other frameworks detected (${frameworkResult.unsupported.map((f) => f.name).join(", ")}) will be skipped in this setup.`
2014
+ );
2015
+ }
1208
2016
  }
1209
2017
  const isSupported = frameworkResult.supported.length > 0;
1210
2018
  const hasUnsupported = frameworkResult.unsupported.length > 0;
@@ -1246,6 +2054,12 @@ async function init(options) {
1246
2054
  manualConfigRequiredFor = buildResult.unsupported[0] || "";
1247
2055
  log.warn(`Detected ${names} \u2014 automatic plugin injection is not supported in current version`);
1248
2056
  log.hint("You can still manually configure it by modifying your configuration file");
2057
+ if (buildResult.unsupported.includes("Next.js")) {
2058
+ printNextJsManualInstructions();
2059
+ }
2060
+ if (buildResult.unsupported.includes("Nuxt")) {
2061
+ printNuxtManualInstructions();
2062
+ }
1249
2063
  }
1250
2064
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
1251
2065
  log.warn("No recognized build tool detected");
@@ -1276,6 +2090,7 @@ async function init(options) {
1276
2090
  }
1277
2091
  }
1278
2092
  let selectedProvider = null;
2093
+ const explicitProvider = options.provider ? providerProbe.detected.find((provider) => provider.id === options.provider) ?? null : null;
1279
2094
  if (!options.provider) {
1280
2095
  if (providerProbe.detected.length === 0) {
1281
2096
  log.warn("No supported AI tools detected");
@@ -1286,36 +2101,17 @@ async function init(options) {
1286
2101
  log.success(`Detected AI tool: ${selectedProvider.label}`);
1287
2102
  }
1288
2103
  } else {
2104
+ log.info("Multiple providers detected, waiting for your selection...");
1289
2105
  selectedProvider = await promptProviderChoice(providerProbe.detected);
1290
2106
  if (selectedProvider) {
1291
2107
  log.success(`Selected provider: ${selectedProvider.label}`);
2108
+ } else {
2109
+ log.warn("No provider selected. You can set provider.default later in .inspecto/settings.");
1292
2110
  }
1293
2111
  }
1294
2112
  }
1295
- let installFailed = false;
1296
- if (options.skipInstall) {
1297
- log.warn("Skipping dependency installation (--skip-install)");
1298
- } else {
1299
- const installCmd = getInstallCommand(pm, "@inspecto-dev/plugin @inspecto-dev/core");
1300
- if (options.dryRun) {
1301
- log.dryRun(`Would run: ${installCmd}`);
1302
- } else {
1303
- try {
1304
- const result = await shell(installCmd, root);
1305
- if (result.stderr && result.stderr.toLowerCase().includes("error")) {
1306
- throw new Error(result.stderr);
1307
- }
1308
- log.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
1309
- mutations.push({ type: "dependency_added", name: "@inspecto-dev/plugin", dev: true });
1310
- mutations.push({ type: "dependency_added", name: "@inspecto-dev/core", dev: true });
1311
- } catch (err) {
1312
- installFailed = true;
1313
- log.error(`Failed to install dependency: ${err?.message || "Unknown error"}`);
1314
- log.hint(`Run manually: ${installCmd}`);
1315
- }
1316
- }
1317
- }
1318
- let injectionFailed = false;
2113
+ let injectionSkippedRequiresManualConfig = false;
2114
+ const supportedBuildTargets = [];
1319
2115
  if (buildResult.supported.length > 0) {
1320
2116
  if (verifiedPackages.length > 0) {
1321
2117
  const targets = buildResult.supported.filter(
@@ -1331,137 +2127,54 @@ async function init(options) {
1331
2127
  log.hint("Check the package paths or run without --packages to inspect the repo root");
1332
2128
  }
1333
2129
  if (targets.length === 0) {
1334
- injectionFailed = true;
1335
- }
1336
- for (const target of targets) {
1337
- const result = await injectPlugin(root, target, options.dryRun);
1338
- if (result.success) {
1339
- mutations.push(...result.mutations);
1340
- } else {
1341
- injectionFailed = true;
1342
- }
2130
+ injectionSkippedRequiresManualConfig = true;
1343
2131
  }
2132
+ supportedBuildTargets.push(...targets);
1344
2133
  } else {
1345
2134
  let target = resolveInjectionTarget(buildResult.supported);
1346
2135
  if (target === "ambiguous") {
1347
2136
  target = await promptConfigChoice(buildResult.supported);
1348
2137
  }
1349
2138
  if (target) {
1350
- const result = await injectPlugin(root, target, options.dryRun);
1351
- if (result.success) {
1352
- mutations.push(...result.mutations);
1353
- } else {
1354
- injectionFailed = true;
1355
- }
2139
+ supportedBuildTargets.push(target);
1356
2140
  } else {
1357
- injectionFailed = true;
2141
+ injectionSkippedRequiresManualConfig = true;
1358
2142
  log.warn("Skipping plugin injection (manual configuration required)");
1359
2143
  }
1360
2144
  }
1361
2145
  }
1362
- const settingsDir = path9.join(root, ".inspecto");
1363
- const settingsFileName = options.shared ? "settings.json" : "settings.local.json";
1364
- const promptsFileName = options.shared ? "prompts.json" : "prompts.local.json";
1365
- const settingsPath = path9.join(settingsDir, settingsFileName);
1366
- const promptsPath = path9.join(settingsDir, promptsFileName);
1367
- if (await exists(settingsPath)) {
1368
- const existingSettings = await readJSON(settingsPath);
1369
- if (existingSettings === null) {
1370
- log.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`);
1371
- log.hint("Please fix the syntax errors manually, or delete it and re-run init");
1372
- } else {
1373
- log.success(`.inspecto/${settingsFileName} already exists (skipped)`);
1374
- }
1375
- } else {
1376
- const defaultSettings = {};
1377
- if (selectedIDE && selectedIDE.supported) {
1378
- defaultSettings.ide = selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : selectedIDE.ide.toLowerCase();
1379
- }
1380
- if (options.provider) {
1381
- const tool = options.provider;
1382
- const mode = tool === "coco" ? "cli" : "extension";
1383
- defaultSettings["provider.default"] = `${tool}.${mode}`;
1384
- } else if (selectedProvider) {
1385
- const toolId = selectedProvider.id;
1386
- const mode = selectedProvider.preferredMode === "cli" ? "cli" : "extension";
1387
- defaultSettings["provider.default"] = `${toolId}.${mode}`;
1388
- }
1389
- if (options.dryRun) {
1390
- log.dryRun(`Would create .inspecto/${settingsFileName}`);
1391
- } else {
1392
- await writeJSON(settingsPath, defaultSettings);
1393
- log.success(`Created .inspecto/${settingsFileName}`);
1394
- mutations.push({ type: "file_created", path: `.inspecto/${settingsFileName}` });
1395
- }
1396
- }
1397
- if (await exists(promptsPath)) {
1398
- log.success(`.inspecto/${promptsFileName} already exists (skipped)`);
1399
- } else {
1400
- const defaultPrompts = [];
1401
- if (options.dryRun) {
1402
- log.dryRun(`Would create .inspecto/${promptsFileName}`);
1403
- } else {
1404
- await writeJSON(promptsPath, defaultPrompts);
1405
- log.success(`Created .inspecto/${promptsFileName}`);
1406
- mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
1407
- }
1408
- }
1409
- if (!options.dryRun) {
1410
- await updateGitignore(root, options.shared, options.dryRun);
1411
- mutations.push({
1412
- type: "file_modified",
1413
- path: ".gitignore",
1414
- description: "Appended .inspecto/ ignore rules"
1415
- });
1416
- } else {
1417
- log.dryRun("Would update .gitignore");
1418
- }
1419
- if (!options.dryRun && mutations.length > 0) {
1420
- const lock = {
1421
- version: "1.0.0",
1422
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1423
- mutations
1424
- };
1425
- await writeJSON(path9.join(settingsDir, "install.lock"), lock);
1426
- }
1427
- const shouldInstallExt = !options.noExtension && (!selectedIDE || selectedIDE && selectedIDE.supported);
1428
- let manualExtensionInstallNeeded = false;
1429
- if (options.noExtension) {
1430
- log.warn("Skipping IDE extension (--no-extension)");
1431
- } else if (!shouldInstallExt) {
1432
- } else {
1433
- const extMutation = await installExtension(options.dryRun, selectedIDE?.ide);
1434
- if (extMutation && !options.dryRun) {
1435
- mutations.push(extMutation);
1436
- if (extMutation.manual_action_required) {
1437
- manualExtensionInstallNeeded = true;
1438
- }
1439
- const lockPath = path9.join(settingsDir, "install.lock");
1440
- const lock = await readJSON(lockPath);
1441
- if (lock) {
1442
- lock.mutations = mutations;
1443
- await writeJSON(lockPath, lock);
1444
- }
1445
- } else if (extMutation === null && !options.dryRun) {
1446
- manualExtensionInstallNeeded = true;
1447
- }
1448
- }
2146
+ const providerDefault = options.provider ? `${options.provider}.${explicitProvider?.preferredMode ?? (options.provider === "coco" ? "cli" : "extension")}` : selectedProvider ? `${selectedProvider.id}.${selectedProvider.preferredMode === "cli" ? "cli" : "extension"}` : void 0;
2147
+ const applyResult = await applyOnboardingPlan({
2148
+ repoRoot,
2149
+ projectRoot,
2150
+ packageManager: pm,
2151
+ supportedBuildTargets,
2152
+ options: {
2153
+ shared: options.shared,
2154
+ skipInstall: options.skipInstall,
2155
+ dryRun: options.dryRun,
2156
+ noExtension: options.noExtension
2157
+ },
2158
+ selectedIDE,
2159
+ providerDefault,
2160
+ manualConfigRequiredFor,
2161
+ injectionSkippedRequiresManualConfig,
2162
+ allowManualPlanApply: true
2163
+ });
2164
+ mutations.push(...applyResult.mutations);
1449
2165
  if (options.dryRun) {
1450
2166
  log.blank();
1451
2167
  log.warn("Dry run complete. No files were modified.");
1452
- } else if (installFailed || injectionFailed || manualExtensionInstallNeeded || manualConfigRequiredFor) {
2168
+ } else {
1453
2169
  log.blank();
1454
- log.warn("Setup completed with some manual steps required.");
1455
- if (manualConfigRequiredFor === "Nuxt") {
1456
- printNuxtManualInstructions();
1457
- } else if (manualConfigRequiredFor === "Next.js") {
1458
- printNextJsManualInstructions();
2170
+ if (applyResult.postInstall.nextSteps.length > 0) {
2171
+ log.warn("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Manual Steps Required \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2172
+ applyResult.postInstall.nextSteps.forEach((step) => log.error(step));
2173
+ log.hint("Complete the items above.");
2174
+ log.blank();
1459
2175
  } else {
1460
- log.hint("Please check the logs above and complete the manual steps.");
2176
+ log.ready("Ready! Hold Alt + Click any element to inspect.");
1461
2177
  }
1462
- log.blank();
1463
- } else {
1464
- log.ready("Ready! Hold Alt + Click any element to inspect.");
1465
2178
  }
1466
2179
  }
1467
2180
  function normalizePackageList(packages) {
@@ -1487,15 +2200,66 @@ function matchesAnyPackage(detection, packages) {
1487
2200
  }
1488
2201
 
1489
2202
  // src/commands/doctor.ts
1490
- import path10 from "path";
1491
- async function doctor() {
1492
- const root = process.cwd();
1493
- const result = { errors: 0, warnings: 0 };
2203
+ import path11 from "path";
2204
+ function createDiagnostic(code, status, message2, hints = [], details) {
2205
+ return {
2206
+ code,
2207
+ status,
2208
+ message: message2,
2209
+ hints,
2210
+ ...details ? { details } : {}
2211
+ };
2212
+ }
2213
+ function doctorStatus(errors, warnings) {
2214
+ if (errors > 0) return "blocked";
2215
+ if (warnings > 0) return "warning";
2216
+ return "ok";
2217
+ }
2218
+ function printDoctorResult(result) {
1494
2219
  log.header("Inspecto Doctor");
1495
- if (!await exists(path10.join(root, "package.json"))) {
1496
- log.error("No package.json found");
1497
- log.hint("Run this command from your project root");
1498
- return;
2220
+ for (const check of result.checks) {
2221
+ if (check.status === "ok") {
2222
+ log.success(check.message);
2223
+ } else if (check.status === "warning") {
2224
+ log.warn(check.message);
2225
+ } else {
2226
+ log.error(check.message);
2227
+ }
2228
+ for (const hint of check.hints) {
2229
+ log.hint(hint);
2230
+ }
2231
+ }
2232
+ log.blank();
2233
+ if (result.summary.errors === 0 && result.summary.warnings === 0) {
2234
+ log.success("All checks passed. Hold Alt + Click to start!");
2235
+ } else {
2236
+ const parts = [];
2237
+ if (result.summary.errors > 0) parts.push(`${result.summary.errors} error(s)`);
2238
+ if (result.summary.warnings > 0) parts.push(`${result.summary.warnings} warning(s)`);
2239
+ console.log(
2240
+ ` ${parts.join(", ")}. ${result.summary.errors > 0 ? "Fix the errors above to get started." : ""}`
2241
+ );
2242
+ }
2243
+ log.blank();
2244
+ }
2245
+ async function collectDoctorResult(root = process.cwd()) {
2246
+ const checks = [];
2247
+ if (!await exists(path11.join(root, "package.json"))) {
2248
+ const diagnostic = createDiagnostic(
2249
+ "missing-package-json",
2250
+ "error",
2251
+ "No package.json found",
2252
+ ["Run this command from your project root"]
2253
+ );
2254
+ checks.push(diagnostic);
2255
+ return {
2256
+ status: "blocked",
2257
+ summary: { errors: 1, warnings: 0 },
2258
+ project: { root },
2259
+ errors: [diagnostic],
2260
+ warnings: [],
2261
+ checks
2262
+ };
1499
2263
  }
1500
2264
  const [ideProbe, frameworkResult, providerProbe, pm, buildResult, extInstalled] = await Promise.all([
1501
2265
  detectIDE(root),
@@ -1506,34 +2270,68 @@ async function doctor() {
1506
2270
  isExtensionInstalled()
1507
2271
  ]);
1508
2272
  if (ideProbe.detected.length === 0) {
1509
- log.warn("IDE: not detected");
1510
- result.warnings++;
2273
+ checks.push(createDiagnostic("ide-not-detected", "warning", "IDE: not detected"));
1511
2274
  } else {
1512
2275
  const hasSupported = ideProbe.detected.some((d) => d.supported);
1513
2276
  if (hasSupported) {
1514
- log.success(
1515
- `IDE: ${ideProbe.detected.filter((d) => d.supported).map((d) => d.ide).join(", ")}`
2277
+ checks.push(
2278
+ createDiagnostic(
2279
+ "ide-supported",
2280
+ "ok",
2281
+ `IDE: ${ideProbe.detected.filter((d) => d.supported).map((d) => d.ide).join(", ")}`,
2282
+ [],
2283
+ {
2284
+ detected: ideProbe.detected
2285
+ }
2286
+ )
1516
2287
  );
1517
2288
  } else {
1518
2289
  const names = ideProbe.detected.map((d) => d.ide).join(", ");
1519
- log.warn(`IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`);
1520
- result.warnings++;
2290
+ checks.push(
2291
+ createDiagnostic(
2292
+ "ide-unsupported",
2293
+ "warning",
2294
+ `IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`,
2295
+ [],
2296
+ {
2297
+ detected: ideProbe.detected
2298
+ }
2299
+ )
2300
+ );
1521
2301
  }
1522
2302
  }
1523
2303
  if (frameworkResult.supported.length > 0) {
1524
- log.success(`Framework: ${frameworkResult.supported.join(", ")}`);
2304
+ checks.push(
2305
+ createDiagnostic(
2306
+ "framework-supported",
2307
+ "ok",
2308
+ `Framework: ${frameworkResult.supported.join(", ")}`
2309
+ )
2310
+ );
1525
2311
  } else if (frameworkResult.unsupported.length > 0) {
1526
2312
  const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1527
- log.warn(`Framework: ${names} (not supported in v1, React/Vue only)`);
1528
- result.warnings++;
2313
+ checks.push(
2314
+ createDiagnostic(
2315
+ "framework-unsupported",
2316
+ "warning",
2317
+ `Framework: ${names} (not supported in v1, React/Vue only)`
2318
+ )
2319
+ );
1529
2320
  } else {
1530
- log.warn("Framework: not detected (React / Vue expected)");
1531
- result.warnings++;
2321
+ checks.push(
2322
+ createDiagnostic(
2323
+ "framework-not-detected",
2324
+ "warning",
2325
+ "Framework: not detected (React / Vue expected)"
2326
+ )
2327
+ );
1532
2328
  }
1533
2329
  if (providerProbe.detected.length === 0) {
1534
- log.warn("Provider: none detected");
1535
- log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
1536
- result.warnings++;
2330
+ checks.push(
2331
+ createDiagnostic("provider-missing", "warning", "Provider: none detected", [
2332
+ "Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot"
2333
+ ])
2334
+ );
1537
2335
  } else {
1538
2336
  const aiNames = providerProbe.detected.map((d) => {
1539
2337
  const modeLabels = d.providerModes.map(
@@ -1541,58 +2339,80 @@ async function doctor() {
1541
2339
  );
1542
2340
  return `${d.label} (${modeLabels.join(" & ")})`;
1543
2341
  }).join(", ");
1544
- log.success(`Provider: ${aiNames}`);
2342
+ checks.push(createDiagnostic("provider-detected", "ok", `Provider: ${aiNames}`));
1545
2343
  }
1546
- const pluginPath = path10.join(root, "node_modules", "@inspecto-dev", "plugin");
2344
+ const pluginPath = path11.join(root, "node_modules", "@inspecto-dev", "plugin");
1547
2345
  if (await exists(pluginPath)) {
1548
- const pkgJson = await readJSON(path10.join(pluginPath, "package.json"));
2346
+ const pkgJson = await readJSON(path11.join(pluginPath, "package.json"));
1549
2347
  const version = pkgJson?.version ?? "unknown";
1550
- log.success(`@inspecto-dev/plugin@${version} installed`);
2348
+ checks.push(
2349
+ createDiagnostic("plugin-installed", "ok", `@inspecto-dev/plugin@${version} installed`, [], {
2350
+ version
2351
+ })
2352
+ );
1551
2353
  } else {
1552
- log.error("@inspecto-dev/plugin not installed");
1553
- const pm2 = await detectPackageManager(root);
1554
- log.hint(`Fix: ${getInstallCommand(pm2, "@inspecto-dev/plugin")}`);
1555
- result.errors++;
2354
+ checks.push(
2355
+ createDiagnostic("plugin-missing", "error", "@inspecto-dev/plugin not installed", [
2356
+ `Fix: ${getInstallCommand(pm, "@inspecto-dev/plugin")}`
2357
+ ])
2358
+ );
1556
2359
  }
1557
2360
  if (buildResult.supported.length > 0) {
1558
2361
  let injected = false;
1559
2362
  for (const bt of buildResult.supported) {
1560
- const content = await readFile(path10.join(root, bt.configPath));
2363
+ const content = await readFile(path11.join(root, bt.configPath));
1561
2364
  if (content && content.includes("@inspecto-dev/plugin")) {
1562
- log.success(`Plugin configured in ${bt.configPath}`);
2365
+ checks.push(
2366
+ createDiagnostic("plugin-configured", "ok", `Plugin configured in ${bt.configPath}`, [], {
2367
+ configPath: bt.configPath,
2368
+ buildTool: bt.tool
2369
+ })
2370
+ );
1563
2371
  injected = true;
1564
2372
  break;
1565
2373
  }
1566
2374
  }
1567
2375
  if (!injected) {
1568
- log.error("Plugin not configured in any build config");
1569
- log.hint("Fix: npx @inspecto-dev/cli init");
1570
- result.errors++;
2376
+ checks.push(
2377
+ createDiagnostic("plugin-not-configured", "error", "Plugin not configured in any build config", [
2378
+ "Fix: npx @inspecto-dev/cli init"
2379
+ ])
2380
+ );
1571
2381
  }
1572
2382
  } else if (buildResult.unsupported.length > 0) {
1573
2383
  const names = buildResult.unsupported.join(", ");
1574
- log.warn(`Build tool: ${names} (not supported in v1)`);
1575
- log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
1576
- result.warnings++;
2384
+ checks.push(
2385
+ createDiagnostic(`build-tool-unsupported`, "warning", `Build tool: ${names} (not supported in v1)`, [
2386
+ "current version supports: Vite, Webpack, Rspack, esbuild, Rollup"
2387
+ ])
2388
+ );
1577
2389
  } else {
1578
- log.warn("No recognized build config found");
1579
- result.warnings++;
2390
+ checks.push(createDiagnostic("build-tool-missing", "warning", "No recognized build config found"));
1580
2391
  }
1581
2392
  if (extInstalled) {
1582
- log.success("VS Code extension detected");
2393
+ checks.push(createDiagnostic("extension-installed", "ok", "VS Code extension detected"));
1583
2394
  } else {
1584
- const hasSupported = ideProbe.detected.some((d) => d.supported);
1585
- if (ideProbe.detected.length > 0 && !hasSupported) {
1586
- log.warn("VS Code extension not applicable (non-VS Code IDE)");
2395
+ const hasVSCode = ideProbe.detected.some((d) => d.supported && d.ide === "vscode");
2396
+ const hasSupportedNonVSCode = ideProbe.detected.some((d) => d.supported && d.ide !== "vscode");
2397
+ if (hasSupportedNonVSCode && !hasVSCode) {
2398
+ checks.push(
2399
+ createDiagnostic(
2400
+ "extension-not-applicable",
2401
+ "warning",
2402
+ "VS Code extension not applicable (non-VS Code IDE)"
2403
+ )
2404
+ );
1587
2405
  } else {
1588
- log.error("VS Code extension not found");
1589
- log.hint("Fix: code --install-extension inspecto.inspecto");
1590
- log.hint("Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto");
1591
- result.errors++;
2406
+ checks.push(
2407
+ createDiagnostic("extension-missing", "error", "VS Code extension not found", [
2408
+ "Fix: code --install-extension inspecto.inspecto",
2409
+ "Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto"
2410
+ ])
2411
+ );
1592
2412
  }
1593
2413
  }
1594
- const settingsJsonPath = path10.join(root, ".inspecto", "settings.json");
1595
- const settingsLocalPath = path10.join(root, ".inspecto", "settings.local.json");
2414
+ const settingsJsonPath = path11.join(root, ".inspecto", "settings.json");
2415
+ const settingsLocalPath = path11.join(root, ".inspecto", "settings.local.json");
1596
2416
  const hasSettingsJson = await exists(settingsJsonPath);
1597
2417
  const hasSettingsLocal = await exists(settingsLocalPath);
1598
2418
  if (hasSettingsJson || hasSettingsLocal) {
@@ -1600,48 +2420,108 @@ async function doctor() {
1600
2420
  const fileName = hasSettingsLocal ? "settings.local.json" : "settings.json";
1601
2421
  const settings = await readJSON(targetPath);
1602
2422
  if (settings) {
1603
- log.success(`.inspecto/${fileName} valid`);
2423
+ checks.push(createDiagnostic("settings-valid", "ok", `.inspecto/${fileName} valid`));
1604
2424
  } else {
1605
- log.error(`.inspecto/${fileName} has invalid JSON`);
1606
- log.hint(
1607
- "Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init"
2425
+ checks.push(
2426
+ createDiagnostic(
2427
+ "settings-invalid-json",
2428
+ "error",
2429
+ `.inspecto/${fileName} has invalid JSON`,
2430
+ [
2431
+ "Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init"
2432
+ ],
2433
+ {
2434
+ fileName
2435
+ }
2436
+ )
1608
2437
  );
1609
- result.errors++;
1610
2438
  }
1611
2439
  } else {
1612
- log.warn("No .inspecto/settings.json or settings.local.json found (using defaults)");
1613
- log.hint("Optional: npx @inspecto-dev/cli init");
1614
- result.warnings++;
2440
+ checks.push(
2441
+ createDiagnostic(
2442
+ "settings-missing",
2443
+ "warning",
2444
+ "No .inspecto/settings.json or settings.local.json found (using defaults)",
2445
+ ["Optional: npx @inspecto-dev/cli init"]
2446
+ )
2447
+ );
1615
2448
  }
1616
- const gitignoreContent = await readFile(path10.join(root, ".gitignore"));
2449
+ const gitignoreContent = await readFile(path11.join(root, ".gitignore"));
1617
2450
  if (gitignoreContent) {
1618
2451
  const hasLockIgnore = gitignoreContent.includes(".inspecto/install.lock") || gitignoreContent.includes(".inspecto/");
1619
2452
  if (!hasLockIgnore) {
1620
- log.warn(".inspecto/install.lock not in .gitignore");
1621
- log.hint("install.lock contains local machine state and should not be committed");
1622
- result.warnings++;
2453
+ checks.push(
2454
+ createDiagnostic(
2455
+ "gitignore-missing-install-lock",
2456
+ "warning",
2457
+ ".inspecto/install.lock not in .gitignore",
2458
+ ["install.lock contains local machine state and should not be committed"]
2459
+ )
2460
+ );
1623
2461
  }
1624
2462
  }
1625
- log.blank();
1626
- if (result.errors === 0 && result.warnings === 0) {
1627
- log.success("All checks passed. Hold Alt + Click to start!");
1628
- } else {
1629
- const parts = [];
1630
- if (result.errors > 0) parts.push(`${result.errors} error(s)`);
1631
- if (result.warnings > 0) parts.push(`${result.warnings} warning(s)`);
1632
- console.log(
1633
- ` ${parts.join(", ")}. ${result.errors > 0 ? "Fix the errors above to get started." : ""}`
1634
- );
2463
+ const errors = checks.filter((check) => check.status === "error");
2464
+ const warnings = checks.filter((check) => check.status === "warning");
2465
+ return {
2466
+ status: doctorStatus(errors.length, warnings.length),
2467
+ summary: {
2468
+ errors: errors.length,
2469
+ warnings: warnings.length
2470
+ },
2471
+ project: {
2472
+ root,
2473
+ packageManager: pm
2474
+ },
2475
+ errors,
2476
+ warnings,
2477
+ checks
2478
+ };
2479
+ }
2480
+ async function doctor(options = {}) {
2481
+ const json = typeof options === "boolean" ? options : options.json ?? false;
2482
+ const result = await collectDoctorResult(process.cwd());
2483
+ return writeCommandOutput(result, json, printDoctorResult);
2484
+ }
2485
+
2486
+ // src/commands/plan.ts
2487
+ function printPlanResult(result) {
2488
+ log.header("Inspecto Plan");
2489
+ log.info(`Status: ${result.status}`);
2490
+ log.info(`Strategy: ${result.strategy}`);
2491
+ if (result.defaults.provider) {
2492
+ log.info(`Default provider: ${result.defaults.provider}`);
2493
+ }
2494
+ if (result.defaults.ide) {
2495
+ log.info(`Default IDE: ${result.defaults.ide}`);
2496
+ }
2497
+ log.info(`Shared mode: ${result.defaults.shared ? "enabled" : "disabled"}`);
2498
+ log.info(`Extension mode: ${result.defaults.extension ? "enabled" : "disabled"}`);
2499
+ if (result.actions.length > 0) {
2500
+ log.blank();
2501
+ log.info("Actions:");
2502
+ for (const action of result.actions) {
2503
+ log.hint(`${action.type}: ${action.target} \u2014 ${action.description}`);
2504
+ }
2505
+ }
2506
+ for (const blocker of result.blockers) {
2507
+ log.error(blocker.message);
2508
+ }
2509
+ for (const warning of result.warnings) {
2510
+ log.warn(warning.message);
1635
2511
  }
1636
- log.blank();
2512
+ }
2513
+ async function plan(json = false) {
2514
+ const context = await buildOnboardingContext(process.cwd());
2515
+ const result = createPlanResult(context);
2516
+ return writeCommandOutput(result, json, printPlanResult);
1637
2517
  }
1638
2518
 
1639
2519
  // src/commands/teardown.ts
1640
- import path11 from "path";
2520
+ import path12 from "path";
1641
2521
  async function teardown() {
1642
2522
  const root = process.cwd();
1643
2523
  log.header("Inspecto Teardown");
1644
- const lockPath = path11.join(root, ".inspecto", "install.lock");
2524
+ const lockPath = path12.join(root, ".inspecto", "install.lock");
1645
2525
  const lock = await readJSON(lockPath);
1646
2526
  if (!lock) {
1647
2527
  log.warn("No .inspecto/install.lock found. Running in best-effort mode.");
@@ -1654,8 +2534,8 @@ async function teardown() {
1654
2534
  } catch {
1655
2535
  log.warn("Could not remove @inspecto-dev/plugin (may not be installed)");
1656
2536
  }
1657
- if (await exists(path11.join(root, ".inspecto"))) {
1658
- await removeDir(path11.join(root, ".inspecto"));
2537
+ if (await exists(path12.join(root, ".inspecto"))) {
2538
+ await removeDir(path12.join(root, ".inspecto"));
1659
2539
  log.success("Deleted .inspecto/ directory");
1660
2540
  }
1661
2541
  await cleanGitignore(root);
@@ -1704,7 +2584,7 @@ async function teardown() {
1704
2584
  }
1705
2585
  }
1706
2586
  }
1707
- await removeDir(path11.join(root, ".inspecto"));
2587
+ await removeDir(path12.join(root, ".inspecto"));
1708
2588
  log.success("Deleted .inspecto/ directory");
1709
2589
  await cleanGitignore(root);
1710
2590
  log.blank();
@@ -1713,8 +2593,13 @@ async function teardown() {
1713
2593
  }
1714
2594
 
1715
2595
  export {
1716
- log,
2596
+ writeCommandOutput,
2597
+ reportCommandError,
2598
+ apply,
2599
+ detect,
1717
2600
  init,
2601
+ collectDoctorResult,
1718
2602
  doctor,
2603
+ plan,
1719
2604
  teardown
1720
2605
  };