@inspecto-dev/cli 0.2.0-alpha.4 → 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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/.turbo/turbo-test.log +16 -21
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +58 -11
  5. package/bin/inspecto.js +5 -1
  6. package/dist/bin.d.ts +5 -1
  7. package/dist/bin.js +89 -50
  8. package/dist/{chunk-EUCQCD3Y.js → chunk-PDDFPQJS.js} +1954 -1053
  9. package/dist/index.d.ts +128 -2
  10. package/dist/index.js +15 -3
  11. package/package.json +2 -1
  12. package/src/bin.ts +139 -67
  13. package/src/commands/apply.ts +114 -0
  14. package/src/commands/detect.ts +59 -0
  15. package/src/commands/doctor.ts +225 -72
  16. package/src/commands/init.ts +106 -183
  17. package/src/commands/plan.ts +41 -0
  18. package/src/detect/build-tool.ts +107 -3
  19. package/src/index.ts +13 -2
  20. package/src/inject/ast-injector.ts +20 -9
  21. package/src/inject/extension.ts +3 -1
  22. package/src/inject/strategies/vite.ts +2 -1
  23. package/src/instructions.ts +60 -46
  24. package/src/onboarding/apply.ts +325 -0
  25. package/src/onboarding/context.ts +36 -0
  26. package/src/onboarding/planner.ts +278 -0
  27. package/src/prompts.ts +54 -11
  28. package/src/types.ts +95 -0
  29. package/src/utils/fs.ts +2 -1
  30. package/src/utils/logger.ts +9 -0
  31. package/src/utils/output.ts +40 -0
  32. package/tests/apply.test.ts +537 -0
  33. package/tests/ast-injector.test.ts +50 -0
  34. package/tests/build-tool.test.ts +3 -5
  35. package/tests/detect.test.ts +94 -0
  36. package/tests/doctor.test.ts +224 -0
  37. package/tests/init.test.ts +333 -0
  38. package/tests/instructions.test.ts +61 -0
  39. package/tests/logger.test.ts +100 -0
  40. package/tests/plan.test.ts +713 -0
  41. package/tests/workspace-build-tool.test.ts +75 -0
@@ -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,262 +185,700 @@ 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;
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";
193
198
  }
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;
199
+ inject({ mod, detection }) {
200
+ addVitePlugin(mod, {
201
+ from: "@inspecto-dev/plugin",
202
+ constructor: "inspecto",
203
+ imported: "vitePlugin"
204
+ });
203
205
  }
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"
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
+ ];
242
216
  }
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 }];
217
+ };
218
+
219
+ // src/inject/strategies/webpack.ts
220
+ var WebpackStrategy = class {
221
+ name = "Webpack";
222
+ supports(tool) {
223
+ return tool === "webpack";
259
224
  }
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
- }
225
+ inject(options) {
226
+ throw new Error("Webpack requires manual plugin configuration");
304
227
  }
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);
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
+ ];
333
244
  }
334
- let detectedFile = "";
335
- if (pattern.tool === "esbuild" && !hasDep) {
336
- return null;
245
+ };
246
+
247
+ // src/inject/strategies/rspack.ts
248
+ var RspackStrategy = class {
249
+ name = "Rspack";
250
+ supports(tool) {
251
+ return tool === "rspack";
337
252
  }
338
- for (const file of pattern.files) {
339
- if (await exists(path3.join(targetRoot, file))) {
340
- detectedFile = file;
341
- break;
342
- }
253
+ inject(options) {
254
+ throw new Error("Rspack requires manual plugin configuration");
343
255
  }
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
- }
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
+ ];
271
+ }
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;
397
+ }
398
+ }
399
+ return false;
400
+ }
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
+ }
457
+ }
458
+
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
+ ]
504
+ };
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() {
583
+ try {
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);
592
+ }
593
+ return false;
594
+ } catch {
595
+ return false;
596
+ }
597
+ }
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);
612
+ }
613
+ log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
614
+ return;
615
+ }
616
+ const missingRules = desiredRules.filter((rule) => !content.includes(rule));
617
+ if (missingRules.length === 0) {
618
+ return;
619
+ }
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");
627
+ }
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);
635
+ }
636
+
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() {
370
662
  }
663
+ };
664
+ }
665
+ return {
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);
371
680
  }
681
+ };
682
+ }
683
+ function createSpinner(text, quiet = false) {
684
+ if (quiet) {
685
+ return {
686
+ start() {
687
+ },
688
+ succeed() {
689
+ },
690
+ fail() {
691
+ }
692
+ };
372
693
  }
373
- if (!detectedFile) {
374
- return null;
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);
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
+ };
375
720
  }
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;
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
+ );
742
+ try {
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
+ );
382
757
  }
383
- } else if (pattern.tool === "webpack") {
384
- const version = resolvedVersion;
385
- if (version && version.includes("^4") || version?.startsWith("4.")) {
386
- isLegacyWebpack = true;
758
+ }
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;
766
+ }
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);
836
+ }
837
+ } else if (extMutation === null && !input.options.dryRun) {
838
+ manualExtensionInstallNeeded = true;
839
+ }
840
+ }
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
+ );
387
861
  }
388
862
  }
389
- const absoluteConfig = path3.join(targetRoot, detectedFile);
390
- const relativeConfig = normalizeRelativePath(workspaceRoot, absoluteConfig);
391
863
  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
864
+ status: resultStatus(nextSteps),
865
+ mutations,
866
+ postInstall: {
867
+ installFailed,
868
+ injectionFailed,
869
+ manualExtensionInstallNeeded,
870
+ nextSteps
871
+ }
398
872
  };
399
873
  }
400
- function resolveInjectionTarget(detections) {
401
- if (detections.length === 0) return null;
402
- if (detections.length === 1) return detections[0];
403
- return "ambiguous";
404
- }
405
874
 
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"
419
- };
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) {
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) {
432
880
  try {
433
- const require2 = createRequire2(path4.join(root, "package.json"));
881
+ const require2 = createRequire(path6.join(root, "package.json"));
434
882
  try {
435
883
  require2.resolve(`${pkgName}/package.json`, { paths: [root] });
436
884
  return true;
@@ -442,579 +890,878 @@ function isPackageResolvable2(pkgName, root) {
442
890
  return false;
443
891
  }
444
892
  }
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);
458
- }
459
- }
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
- }
468
- }
469
- for (const fw of UNSUPPORTED_FRAMEWORKS) {
470
- if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
471
- unsupported.push(fw);
472
- }
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;
473
901
  }
474
- return {
475
- supported: Array.from(supportedSet),
476
- unsupported
477
- };
478
902
  }
479
-
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 });
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"
490
940
  }
491
- if (process.env.ZED_TERM) {
492
- detected.set("Zed", { ide: "Zed", supported: false });
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 }];
493
957
  }
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 });
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
+ }
496
984
  }
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 });
985
+ return patterns;
986
+ }
987
+ async function expandWorkspacePattern(root, pattern) {
988
+ const normalized = pattern.replace(/\\/g, "/").replace(/\/$/, "");
989
+ if (!normalized || normalized.startsWith("!")) {
990
+ return [];
505
991
  }
506
- if (hasCursor && !detected.has("Cursor")) {
507
- detected.set("Cursor", { ide: "cursor", supported: true });
992
+ if (!normalized.includes("*")) {
993
+ return await exists(path6.join(root, normalized)) ? [normalized] : [];
508
994
  }
509
- if (hasVscode && !detected.has("vscode")) {
510
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
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 [];
511
1000
  }
512
- if (hasIdea && !detected.has("JetBrains IDE")) {
513
- detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
1001
+ const absoluteBaseDir = path6.join(root, baseDir);
1002
+ if (!await exists(absoluteBaseDir)) {
1003
+ return [];
514
1004
  }
515
- if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
516
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
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 [];
517
1010
  }
518
- return {
519
- detected: Array.from(detected.values())
520
- };
521
1011
  }
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
- });
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);
548
1022
  }
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());
1023
+ }
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);
558
1053
  }
559
- } catch {
560
1054
  }
561
- }
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) {
567
- 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 {
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;
581
1060
  }
582
1061
  }
583
- } catch {
584
- }
585
- }
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;
595
- }
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
- });
1062
+ return null;
1063
+ });
1064
+ const unsupportedResults = await Promise.all(unsupportedChecks);
1065
+ for (const result of unsupportedResults) {
1066
+ if (result) {
1067
+ unsupported.add(result);
609
1068
  }
610
1069
  }
611
1070
  }
612
- return { detected: Array.from(detectedMap.values()) };
1071
+ return { supported, unsupported: Array.from(unsupported) };
613
1072
  }
614
-
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";
625
- }
626
- inject({ mod, detection }) {
627
- addVitePlugin(mod, {
628
- from: "@inspecto-dev/plugin",
629
- constructor: "vitePlugin"
630
- });
631
- }
632
- getManualInstructions(detection, reason) {
633
- return [
634
- `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
635
- "",
636
- "// Add to your plugins array:",
637
- `plugins: [`,
638
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
639
- ` ...otherPlugins`,
640
- `].filter(Boolean)`
641
- ];
642
- }
643
- };
644
-
645
- // src/inject/strategies/webpack.ts
646
- var WebpackStrategy = class {
647
- name = "Webpack";
648
- supports(tool) {
649
- return tool === "webpack";
650
- }
651
- inject(options) {
652
- throw new Error("Webpack requires manual plugin configuration");
653
- }
654
- getManualInstructions(detection, reason) {
655
- const importPkg = detection.isLegacyWebpack ? "@inspecto-dev/plugin/legacy/webpack4" : "@inspecto-dev/plugin";
656
- const pluginName = detection.isLegacyWebpack ? "webpack4Plugin" : "webpackPlugin";
657
- const pluginCall = detection.isLegacyWebpack ? `process.env.NODE_ENV !== 'production' && inspecto({
658
- pathType: 'absolute',
659
- escapeTags: ['Transition', 'AnimatePresence'],
660
- })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
661
- return [
662
- `import { ${pluginName} as inspecto } from '${importPkg}'`,
663
- "",
664
- "// Add to your plugins array:",
665
- `plugins: [`,
666
- ` ${pluginCall},`,
667
- ` ...otherPlugins`,
668
- `].filter(Boolean)`
669
- ];
670
- }
671
- };
672
-
673
- // src/inject/strategies/rspack.ts
674
- var RspackStrategy = class {
675
- name = "Rspack";
676
- supports(tool) {
677
- return tool === "rspack";
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);
678
1099
  }
679
- inject(options) {
680
- throw new Error("Rspack requires manual plugin configuration");
1100
+ let detectedFile = "";
1101
+ let inferredFromScripts = false;
1102
+ if (pattern.tool === "esbuild" && !hasDep) {
1103
+ return null;
681
1104
  }
682
- getManualInstructions(detection, reason) {
683
- const importPkg = detection.isLegacyRspack ? "@inspecto-dev/plugin/legacy/rspack" : "@inspecto-dev/plugin";
684
- const pluginCall = detection.isLegacyRspack ? `process.env.NODE_ENV !== 'production' && inspecto({
685
- pathType: 'absolute',
686
- escapeTags: ['Transition', 'AnimatePresence'],
687
- })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
688
- return [
689
- `import { rspackPlugin as inspecto } from '${importPkg}'`,
690
- "",
691
- "// Add to your plugins array:",
692
- `plugins: [`,
693
- ` ${pluginCall},`,
694
- ` ...otherPlugins`,
695
- `].filter(Boolean)`
696
- ];
1105
+ for (const file of pattern.files) {
1106
+ if (await exists(path6.join(targetRoot, file))) {
1107
+ detectedFile = file;
1108
+ break;
1109
+ }
697
1110
  }
698
- };
699
-
700
- // src/inject/strategies/rsbuild.ts
701
- var RsbuildStrategy = class {
702
- name = "Rsbuild";
703
- supports(tool) {
704
- return tool === "rsbuild";
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
+ }
705
1140
  }
706
- inject(options) {
707
- throw new Error("Rsbuild requires manual plugin configuration due to nested structure");
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;
708
1151
  }
709
- getManualInstructions(detection, reason) {
710
- return [
711
- `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
712
- "",
713
- "// Add to tools.rspack:",
714
- `tools: {`,
715
- ` rspack: {`,
716
- ` plugins: [`,
717
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
718
- ` ]`,
719
- ` }`,
720
- `}`
721
- ];
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
+ }
722
1164
  }
723
- };
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
+ }
724
1181
 
725
- // src/inject/strategies/esbuild.ts
726
- var EsbuildStrategy = class {
727
- name = "esbuild";
728
- supports(tool) {
729
- return tool === "esbuild";
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"] }
1199
+ ];
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;
730
1219
  }
731
- inject(options) {
732
- throw new Error("Esbuild requires manual plugin configuration");
1220
+ }
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);
1234
+ }
733
1235
  }
734
- getManualInstructions(detection, reason) {
735
- return [
736
- `1. Update your esbuild config (${detection.configPath}):`,
737
- `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
738
- "",
739
- "// Add to your plugins array:",
740
- `plugins: [`,
741
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
742
- ` ...otherPlugins`,
743
- `].filter(Boolean)`,
744
- "",
745
- "2. Initialize the client in your app entry (e.g., main.js / index.js):",
746
- `import { mountInspector } from '@inspecto-dev/core'`,
747
- "",
748
- "// Call this before your app renders",
749
- `if (process.env.NODE_ENV !== 'production') {`,
750
- ` mountInspector()`,
751
- `}`
752
- ];
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
+ }
753
1244
  }
754
- };
755
-
756
- // src/inject/strategies/rollup.ts
757
- var RollupStrategy = class {
758
- name = "Rollup";
759
- supports(tool) {
760
- return tool === "rollup";
1245
+ for (const fw of UNSUPPORTED_FRAMEWORKS) {
1246
+ if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
1247
+ unsupported.push(fw);
1248
+ }
761
1249
  }
762
- inject(options) {
763
- throw new Error("Rollup requires manual plugin configuration");
1250
+ return {
1251
+ supported: Array.from(supportedSet),
1252
+ unsupported
1253
+ };
1254
+ }
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 });
764
1263
  }
765
- getManualInstructions(detection, reason) {
766
- return [
767
- `1. Update your rollup config (${detection.configPath}):`,
768
- `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
769
- "",
770
- "// Add to your plugins array:",
771
- `plugins: [`,
772
- ` process.env.NODE_ENV !== 'production' && inspecto(),`,
773
- ` ...otherPlugins`,
774
- `].filter(Boolean)`,
775
- "",
776
- "2. Initialize the client in your app entry (e.g., main.js / index.js):",
777
- `import { mountInspector } from '@inspecto-dev/core'`,
778
- "",
779
- "// Call this before your app renders",
780
- `if (process.env.NODE_ENV !== 'production') {`,
781
- ` mountInspector()`,
782
- `}`
783
- ];
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 });
784
1266
  }
785
- };
786
-
787
- // src/inject/strategies/index.ts
788
- var STRATEGIES = [
789
- new ViteStrategy(),
790
- new WebpackStrategy(),
791
- new RspackStrategy(),
792
- new RsbuildStrategy(),
793
- new EsbuildStrategy(),
794
- new RollupStrategy()
795
- ];
796
-
797
- // src/inject/ast-injector.ts
798
- function printManualInstructions(strategy, detection, reason) {
799
- log.warn(`Could not automatically configure ${detection.configPath}`);
800
- log.hint(`(reason: ${reason})`);
801
- log.blank();
802
- log.hint("Please add the following manually:");
803
- if (strategy) {
804
- const instructions = strategy.getManualInstructions(detection, reason);
805
- log.codeBlock(instructions);
806
- } else {
807
- log.error(`Unsupported build tool: ${detection.tool}`);
1267
+ if (process.env.ZED_TERM) {
1268
+ detected.set("Zed", { ide: "Zed", supported: false });
808
1269
  }
809
- }
810
- function isAlreadyInjected(content) {
811
- return /import\s+.*@inspecto-dev\/plugin/.test(content) || /require\(['"]@inspecto-dev\/plugin['"]\)/.test(content) || /import\s+.*ai-dev-inspector/.test(content) || // Legacy support
812
- /require\(['"]ai-dev-inspector['"]\)/.test(content);
813
- }
814
- async function injectPlugin(root, detection, dryRun) {
815
- const configPath = path7.join(root, detection.configPath);
816
- const mutations = [];
817
- const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
818
- const content = await readFile(configPath);
819
- if (!content) {
820
- printManualInstructions(strategy, detection, "config file not readable");
821
- return { success: false, mutations, failureReason: "config file not readable" };
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 });
822
1272
  }
823
- if (isAlreadyInjected(content)) {
824
- log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
825
- mutations.push({
826
- type: "file_modified",
827
- path: detection.configPath,
828
- description: "Previously configured inspecto() plugin"
829
- });
830
- return { success: true, mutations };
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 });
831
1281
  }
832
- if (!strategy) {
833
- printManualInstructions(
834
- strategy,
835
- detection,
836
- `No injection strategy found for ${detection.tool}`
837
- );
838
- return { success: false, mutations, failureReason: "No strategy found" };
1282
+ if (hasCursor && !detected.has("Cursor")) {
1283
+ detected.set("Cursor", { ide: "cursor", supported: true });
839
1284
  }
840
- if (dryRun) {
841
- log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
842
- return { success: true, mutations: [] };
1285
+ if (hasVscode && !detected.has("vscode")) {
1286
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
843
1287
  }
844
- try {
845
- const mod = await loadFile(configPath);
846
- strategy.inject({
847
- mod,
848
- detection
849
- });
850
- await writeAstFile(mod, configPath);
851
- mutations.push({
852
- type: "file_modified",
853
- path: detection.configPath,
854
- description: "Automatically configured inspecto() plugin"
855
- });
856
- log.success(`Configured plugin in ${detection.configPath}`);
857
- return { success: true, mutations };
858
- } catch (err) {
859
- printManualInstructions(
860
- strategy,
861
- detection,
862
- `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`
863
- );
864
- return {
865
- success: false,
866
- mutations,
867
- failureReason: "Automatic configuration unavailable"
868
- };
1288
+ if (hasIdea && !detected.has("JetBrains IDE")) {
1289
+ detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
869
1290
  }
1291
+ if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
1292
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
1293
+ }
1294
+ return {
1295
+ detected: Array.from(detected.values())
1296
+ };
870
1297
  }
871
1298
 
872
- // src/inject/gitignore.ts
873
- import path8 from "path";
874
- var DEFAULT_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
875
- var SHARED_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
876
- async function updateGitignore(root, shared, dryRun) {
877
- const gitignorePath = path8.join(root, ".gitignore");
878
- let content = await readFile(gitignorePath) ?? "";
879
- const desiredRules = shared ? SHARED_RULES : DEFAULT_RULES;
880
- const hasGlobalRule = content.match(/^\.inspecto\/\s*$/m) !== null;
881
- if (hasGlobalRule) {
882
- content = content.replace(/^\.inspecto\/\s*$/gm, SHARED_RULES.join("\n"));
883
- if (!dryRun) {
884
- 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 {
885
1336
  }
886
- log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
887
- return;
888
1337
  }
889
- const missingRules = desiredRules.filter((rule) => !content.includes(rule));
890
- if (missingRules.length === 0) {
891
- 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
+ }
892
1361
  }
893
- const section = "\n# Inspecto\n" + missingRules.join("\n") + "\n";
894
- content = content.trimEnd() + "\n" + section;
895
- if (dryRun) {
896
- log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
897
- } else {
898
- await writeFile(gitignorePath, content);
899
- 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
+ }
900
1387
  }
1388
+ return { detected: Array.from(detectedMap.values()) };
901
1389
  }
902
- async function cleanGitignore(root) {
903
- const gitignorePath = path8.join(root, ".gitignore");
904
- const content = await readFile(gitignorePath);
905
- if (!content) return;
906
- 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");
907
- 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
+ };
908
1419
  }
909
1420
 
910
- // src/inject/extension.ts
911
- var EXTENSION_ID = "inspecto.inspecto";
912
- var VSCODE_PATHS = {
913
- darwin: [
914
- "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
915
- "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code-insiders",
916
- `${process.env.HOME}/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code`
917
- ],
918
- linux: ["/usr/bin/code", "/usr/share/code/bin/code", "/snap/bin/code", "/usr/bin/code-insiders"],
919
- win32: [
920
- `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code\\bin\\code.cmd`,
921
- `${process.env.LOCALAPPDATA}\\Programs\\Microsoft VS Code Insiders\\bin\\code-insiders.cmd`,
922
- `${process.env.PROGRAMFILES}\\Microsoft VS Code\\bin\\code.cmd`
923
- ]
924
- };
925
- async function findVSCodeBinary() {
926
- const platform = process.platform;
927
- const candidates = VSCODE_PATHS[platform] || [];
928
- for (const candidate of candidates) {
929
- if (await exists(candidate)) {
930
- 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 [];
931
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
+ ];
932
1470
  }
933
- if (await which("code-insiders")) {
934
- 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 [];
935
1476
  }
936
- 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")];
937
1486
  }
938
- async function tryOpenURI(uri) {
939
- try {
940
- const platform = process.platform;
941
- if (platform === "win32") {
942
- await shell(`cmd /c start "" "${uri}"`);
943
- } else {
944
- const openCmd = platform === "darwin" ? "open" : "xdg-open";
945
- await shell(`${openCmd} "${uri}"`);
946
- }
947
- return true;
948
- } catch {
949
- 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
+ );
950
1509
  }
1510
+ return warnings;
951
1511
  }
952
- async function installExtension(dryRun, ide) {
953
- if (dryRun) {
954
- log.dryRun("Would attempt to install VS Code extension");
955
- 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
+ ];
956
1521
  }
957
- const isVSCode = !ide || ide === "vscode";
958
- if (isVSCode) {
959
- if (await which("code")) {
960
- try {
961
- await run("code", ["--install-extension", EXTENSION_ID]);
962
- log.success("VS Code extension installed via CLI");
963
- return { type: "extension_installed", id: EXTENSION_ID };
964
- } 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."
965
1529
  }
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."
966
1537
  }
967
- const codePath = await findVSCodeBinary();
968
- if (codePath) {
969
- try {
970
- await run(codePath, ["--install-extension", EXTENSION_ID]);
971
- log.success("VS Code extension installed via binary path");
972
- log.info(
973
- 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
974
- );
975
- return { type: "extension_installed", id: EXTENSION_ID };
976
- } catch {
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."
977
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."
978
1555
  }
979
- const uri = `vscode:extension/${EXTENSION_ID}`;
980
- if (await tryOpenURI(uri)) {
981
- log.warn("Opened extension page in VS Code");
982
- log.hint('Please click "Install" in the opened VS Code window to complete setup.');
983
- 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
984
1579
  }
985
- log.warn("Could not auto-install VS Code extension");
986
- log.hint("Please install it manually to enable Inspector features:");
987
- log.hint(" 1. Open VS Code");
988
- log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
989
- log.hint(' 3. Search for "Inspecto"');
990
- log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
991
- return null;
992
- }
993
- log.warn(`Could not auto-install extension for ${ide}`);
994
- log.hint("Please install it manually to enable Inspector features:");
995
- log.hint(" 1. Download the latest .vsix file from Inspecto releases");
996
- log.hint(` 2. Open ${ide}`);
997
- log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
998
- log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
999
- log.hint(" 5. Select the downloaded .vsix file");
1000
- return null;
1580
+ };
1001
1581
  }
1002
- async function isExtensionInstalled() {
1003
- try {
1004
- if (await which("code")) {
1005
- const { stdout } = await run("code", ["--list-extensions"]);
1006
- 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));
1007
1591
  }
1008
- const codePath = await findVSCodeBinary();
1009
- if (codePath) {
1010
- const { stdout } = await run(codePath, ["--list-extensions"]);
1011
- return stdout.toLowerCase().includes(EXTENSION_ID);
1592
+ if (frameworkBlockers(context).length > 0) {
1593
+ actions.push(...manualFrameworkActions(context));
1012
1594
  }
1013
- return false;
1014
- } catch {
1015
- 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);
1752
+ }
1753
+ for (const warning of result.warnings) {
1754
+ if (suppressedCodes.has(warning.code)) continue;
1755
+ log.warn(warning.message);
1016
1756
  }
1017
1757
  }
1758
+ async function detect(json = false) {
1759
+ const result = await createDetectionResult(process.cwd());
1760
+ return writeCommandOutput(result, json, printDetectionResult);
1761
+ }
1762
+
1763
+ // src/commands/init.ts
1764
+ import path10 from "path";
1018
1765
 
1019
1766
  // src/prompts.ts
1020
1767
  import prompts from "prompts";
@@ -1055,9 +1802,9 @@ async function promptProviderChoice(detections) {
1055
1802
  title: `${d.label} ${d.supported ? `(supported ${modeStr})` : "(unsupported/limited)"}`,
1056
1803
  value: i
1057
1804
  };
1058
- })
1805
+ }).concat({ title: "Skip (configure later)", value: -1 })
1059
1806
  });
1060
- if (choice === void 0) return null;
1807
+ if (choice === void 0 || choice === -1) return null;
1061
1808
  return detections[choice];
1062
1809
  }
1063
1810
  async function promptConfigChoice(detections) {
@@ -1083,6 +1830,33 @@ async function promptConfigChoice(detections) {
1083
1830
  if (choice === void 0 || choice === -1) return null;
1084
1831
  return detections[choice];
1085
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
+ }
1086
1860
  async function promptUnsupportedFrameworkContinue() {
1087
1861
  if (!process.stdin.isTTY) {
1088
1862
  log.error("Unsupported framework detected in non-interactive environment.");
@@ -1101,60 +1875,75 @@ async function promptUnsupportedFrameworkContinue() {
1101
1875
  // src/instructions.ts
1102
1876
  function printNuxtManualInstructions() {
1103
1877
  log.blank();
1104
- log.hint("To enable Inspecto in Nuxt, update your nuxt.config.ts:");
1105
- console.log(`\x1B[36m
1106
- import { vitePlugin as inspecto } from '@inspecto-dev/plugin'
1107
- export default defineNuxtConfig({
1108
- vite: {
1109
- plugins: [inspecto()]
1110
- }
1111
- })
1112
- \x1B[0m`);
1113
- log.hint("And create a Nuxt plugin at plugins/inspecto.client.ts:");
1114
- console.log(`\x1B[36m
1115
- export default defineNuxtPlugin(() => {
1116
- if (import.meta.dev) {
1117
- import('@inspecto-dev/core').then(({ mountInspector }) => {
1118
- mountInspector()
1119
- })
1120
- }
1121
- })
1122
- \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.");
1123
1900
  }
1124
1901
  function printNextJsManualInstructions() {
1125
1902
  log.blank();
1126
- log.hint("To enable Inspecto in Next.js, update your next.config.mjs:");
1127
- console.log(`\x1B[36m
1128
- import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'
1129
- const nextConfig = {
1130
- webpack: (config, { dev, isServer }) => {
1131
- if (dev && !isServer) config.plugins.push(inspecto())
1132
- return config
1133
- }
1134
- }
1135
- export default nextConfig
1136
- \x1B[0m`);
1137
- log.hint("And initialize the client dynamically in your app/layout.tsx (or pages/_app.tsx):");
1138
- console.log(`\x1B[36m
1139
- 'use client'
1140
- import { useEffect } from 'react'
1141
-
1142
- export default function RootLayout({ children }) {
1143
- useEffect(() => {
1144
- if (process.env.NODE_ENV !== 'production') {
1145
- import('@inspecto-dev/core').then(({ mountInspector }) => {
1146
- mountInspector({ serverUrl: 'http://127.0.0.1:5678' })
1147
- })
1148
- }
1149
- }, [])
1150
- return <html><body>{children}</body></html>
1151
- }
1152
- \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.");
1153
1941
  }
1154
1942
 
1155
1943
  // src/commands/init.ts
1156
1944
  async function init(options) {
1157
- const root = process.cwd();
1945
+ const repoRoot = process.cwd();
1946
+ let projectRoot = repoRoot;
1158
1947
  const mutations = [];
1159
1948
  const normalizedPackages = normalizePackageList(options.packages);
1160
1949
  const verifiedPackages = [];
@@ -1163,7 +1952,7 @@ async function init(options) {
1163
1952
  verifiedPackages.push(pkg);
1164
1953
  continue;
1165
1954
  }
1166
- const absolutePath = path9.join(root, pkg);
1955
+ const absolutePath = path10.join(repoRoot, pkg);
1167
1956
  if (await exists(absolutePath)) {
1168
1957
  verifiedPackages.push(pkg);
1169
1958
  } else {
@@ -1176,21 +1965,54 @@ async function init(options) {
1176
1965
  return;
1177
1966
  }
1178
1967
  log.header("Inspecto Setup");
1179
- if (!await exists(path9.join(root, "package.json"))) {
1968
+ if (!await exists(path10.join(repoRoot, "package.json"))) {
1180
1969
  log.error("No package.json found in current directory");
1181
1970
  log.hint("Run this command from your project root");
1182
1971
  return;
1183
1972
  }
1184
- const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
1185
- detectPackageManager(root),
1186
- detectFrameworks(root),
1187
- detectBuildTools(root, verifiedPackages.length > 0 ? verifiedPackages : void 0),
1188
- detectIDE(root),
1189
- 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)
1190
2006
  ]);
1191
2007
  log.success(`Detected package manager: ${pm}`);
1192
2008
  if (frameworkResult.supported.length > 0) {
1193
- 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
+ }
1194
2016
  }
1195
2017
  const isSupported = frameworkResult.supported.length > 0;
1196
2018
  const hasUnsupported = frameworkResult.unsupported.length > 0;
@@ -1219,7 +2041,9 @@ async function init(options) {
1219
2041
  }
1220
2042
  let manualConfigRequiredFor = "";
1221
2043
  if (verifiedPackages.length > 0 && buildResult.supported.length === 0) {
1222
- log.warn(`No supported build configs detected for: ${verifiedPackages.map((pkg) => pkg ? pkg : ".").join(", ")}`);
2044
+ log.warn(
2045
+ `No supported build configs detected for: ${verifiedPackages.map((pkg) => pkg ? pkg : ".").join(", ")}`
2046
+ );
1223
2047
  log.hint("Double-check the --packages values or run without the flag to scan the repo root");
1224
2048
  }
1225
2049
  if (buildResult.supported.length > 0) {
@@ -1230,6 +2054,12 @@ async function init(options) {
1230
2054
  manualConfigRequiredFor = buildResult.unsupported[0] || "";
1231
2055
  log.warn(`Detected ${names} \u2014 automatic plugin injection is not supported in current version`);
1232
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
+ }
1233
2063
  }
1234
2064
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
1235
2065
  log.warn("No recognized build tool detected");
@@ -1260,6 +2090,7 @@ async function init(options) {
1260
2090
  }
1261
2091
  }
1262
2092
  let selectedProvider = null;
2093
+ const explicitProvider = options.provider ? providerProbe.detected.find((provider) => provider.id === options.provider) ?? null : null;
1263
2094
  if (!options.provider) {
1264
2095
  if (providerProbe.detected.length === 0) {
1265
2096
  log.warn("No supported AI tools detected");
@@ -1270,36 +2101,17 @@ async function init(options) {
1270
2101
  log.success(`Detected AI tool: ${selectedProvider.label}`);
1271
2102
  }
1272
2103
  } else {
2104
+ log.info("Multiple providers detected, waiting for your selection...");
1273
2105
  selectedProvider = await promptProviderChoice(providerProbe.detected);
1274
2106
  if (selectedProvider) {
1275
2107
  log.success(`Selected provider: ${selectedProvider.label}`);
2108
+ } else {
2109
+ log.warn("No provider selected. You can set provider.default later in .inspecto/settings.");
1276
2110
  }
1277
2111
  }
1278
2112
  }
1279
- let installFailed = false;
1280
- if (options.skipInstall) {
1281
- log.warn("Skipping dependency installation (--skip-install)");
1282
- } else {
1283
- const installCmd = getInstallCommand(pm, "@inspecto-dev/plugin @inspecto-dev/core");
1284
- if (options.dryRun) {
1285
- log.dryRun(`Would run: ${installCmd}`);
1286
- } else {
1287
- try {
1288
- const result = await shell(installCmd, root);
1289
- if (result.stderr && result.stderr.toLowerCase().includes("error")) {
1290
- throw new Error(result.stderr);
1291
- }
1292
- log.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
1293
- mutations.push({ type: "dependency_added", name: "@inspecto-dev/plugin", dev: true });
1294
- mutations.push({ type: "dependency_added", name: "@inspecto-dev/core", dev: true });
1295
- } catch (err) {
1296
- installFailed = true;
1297
- log.error(`Failed to install dependency: ${err?.message || "Unknown error"}`);
1298
- log.hint(`Run manually: ${installCmd}`);
1299
- }
1300
- }
1301
- }
1302
- let injectionFailed = false;
2113
+ let injectionSkippedRequiresManualConfig = false;
2114
+ const supportedBuildTargets = [];
1303
2115
  if (buildResult.supported.length > 0) {
1304
2116
  if (verifiedPackages.length > 0) {
1305
2117
  const targets = buildResult.supported.filter(
@@ -1315,137 +2127,54 @@ async function init(options) {
1315
2127
  log.hint("Check the package paths or run without --packages to inspect the repo root");
1316
2128
  }
1317
2129
  if (targets.length === 0) {
1318
- injectionFailed = true;
1319
- }
1320
- for (const target of targets) {
1321
- const result = await injectPlugin(root, target, options.dryRun);
1322
- if (result.success) {
1323
- mutations.push(...result.mutations);
1324
- } else {
1325
- injectionFailed = true;
1326
- }
2130
+ injectionSkippedRequiresManualConfig = true;
1327
2131
  }
2132
+ supportedBuildTargets.push(...targets);
1328
2133
  } else {
1329
2134
  let target = resolveInjectionTarget(buildResult.supported);
1330
2135
  if (target === "ambiguous") {
1331
2136
  target = await promptConfigChoice(buildResult.supported);
1332
2137
  }
1333
2138
  if (target) {
1334
- const result = await injectPlugin(root, target, options.dryRun);
1335
- if (result.success) {
1336
- mutations.push(...result.mutations);
1337
- } else {
1338
- injectionFailed = true;
1339
- }
2139
+ supportedBuildTargets.push(target);
1340
2140
  } else {
1341
- injectionFailed = true;
2141
+ injectionSkippedRequiresManualConfig = true;
1342
2142
  log.warn("Skipping plugin injection (manual configuration required)");
1343
2143
  }
1344
2144
  }
1345
2145
  }
1346
- const settingsDir = path9.join(root, ".inspecto");
1347
- const settingsFileName = options.shared ? "settings.json" : "settings.local.json";
1348
- const promptsFileName = options.shared ? "prompts.json" : "prompts.local.json";
1349
- const settingsPath = path9.join(settingsDir, settingsFileName);
1350
- const promptsPath = path9.join(settingsDir, promptsFileName);
1351
- if (await exists(settingsPath)) {
1352
- const existingSettings = await readJSON(settingsPath);
1353
- if (existingSettings === null) {
1354
- log.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`);
1355
- log.hint("Please fix the syntax errors manually, or delete it and re-run init");
1356
- } else {
1357
- log.success(`.inspecto/${settingsFileName} already exists (skipped)`);
1358
- }
1359
- } else {
1360
- const defaultSettings = {};
1361
- if (selectedIDE && selectedIDE.supported) {
1362
- defaultSettings.ide = selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : selectedIDE.ide.toLowerCase();
1363
- }
1364
- if (options.provider) {
1365
- const tool = options.provider;
1366
- const mode = tool === "coco" ? "cli" : "extension";
1367
- defaultSettings["provider.default"] = `${tool}.${mode}`;
1368
- } else if (selectedProvider) {
1369
- const toolId = selectedProvider.id;
1370
- const mode = selectedProvider.preferredMode === "cli" ? "cli" : "extension";
1371
- defaultSettings["provider.default"] = `${toolId}.${mode}`;
1372
- }
1373
- if (options.dryRun) {
1374
- log.dryRun(`Would create .inspecto/${settingsFileName}`);
1375
- } else {
1376
- await writeJSON(settingsPath, defaultSettings);
1377
- log.success(`Created .inspecto/${settingsFileName}`);
1378
- mutations.push({ type: "file_created", path: `.inspecto/${settingsFileName}` });
1379
- }
1380
- }
1381
- if (await exists(promptsPath)) {
1382
- log.success(`.inspecto/${promptsFileName} already exists (skipped)`);
1383
- } else {
1384
- const defaultPrompts = [];
1385
- if (options.dryRun) {
1386
- log.dryRun(`Would create .inspecto/${promptsFileName}`);
1387
- } else {
1388
- await writeJSON(promptsPath, defaultPrompts);
1389
- log.success(`Created .inspecto/${promptsFileName}`);
1390
- mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
1391
- }
1392
- }
1393
- if (!options.dryRun) {
1394
- await updateGitignore(root, options.shared, options.dryRun);
1395
- mutations.push({
1396
- type: "file_modified",
1397
- path: ".gitignore",
1398
- description: "Appended .inspecto/ ignore rules"
1399
- });
1400
- } else {
1401
- log.dryRun("Would update .gitignore");
1402
- }
1403
- if (!options.dryRun && mutations.length > 0) {
1404
- const lock = {
1405
- version: "1.0.0",
1406
- created_at: (/* @__PURE__ */ new Date()).toISOString(),
1407
- mutations
1408
- };
1409
- await writeJSON(path9.join(settingsDir, "install.lock"), lock);
1410
- }
1411
- const shouldInstallExt = !options.noExtension && (!selectedIDE || selectedIDE && selectedIDE.supported);
1412
- let manualExtensionInstallNeeded = false;
1413
- if (options.noExtension) {
1414
- log.warn("Skipping IDE extension (--no-extension)");
1415
- } else if (!shouldInstallExt) {
1416
- } else {
1417
- const extMutation = await installExtension(options.dryRun, selectedIDE?.ide);
1418
- if (extMutation && !options.dryRun) {
1419
- mutations.push(extMutation);
1420
- if (extMutation.manual_action_required) {
1421
- manualExtensionInstallNeeded = true;
1422
- }
1423
- const lockPath = path9.join(settingsDir, "install.lock");
1424
- const lock = await readJSON(lockPath);
1425
- if (lock) {
1426
- lock.mutations = mutations;
1427
- await writeJSON(lockPath, lock);
1428
- }
1429
- } else if (extMutation === null && !options.dryRun) {
1430
- manualExtensionInstallNeeded = true;
1431
- }
1432
- }
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);
1433
2165
  if (options.dryRun) {
1434
2166
  log.blank();
1435
2167
  log.warn("Dry run complete. No files were modified.");
1436
- } else if (installFailed || injectionFailed || manualExtensionInstallNeeded || manualConfigRequiredFor) {
2168
+ } else {
1437
2169
  log.blank();
1438
- log.warn("Setup completed with some manual steps required.");
1439
- if (manualConfigRequiredFor === "Nuxt") {
1440
- printNuxtManualInstructions();
1441
- } else if (manualConfigRequiredFor === "Next.js") {
1442
- 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();
1443
2175
  } else {
1444
- log.hint("Please check the logs above and complete the manual steps.");
2176
+ log.ready("Ready! Hold Alt + Click any element to inspect.");
1445
2177
  }
1446
- log.blank();
1447
- } else {
1448
- log.ready("Ready! Hold Alt + Click any element to inspect.");
1449
2178
  }
1450
2179
  }
1451
2180
  function normalizePackageList(packages) {
@@ -1471,15 +2200,66 @@ function matchesAnyPackage(detection, packages) {
1471
2200
  }
1472
2201
 
1473
2202
  // src/commands/doctor.ts
1474
- import path10 from "path";
1475
- async function doctor() {
1476
- const root = process.cwd();
1477
- 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) {
1478
2219
  log.header("Inspecto Doctor");
1479
- if (!await exists(path10.join(root, "package.json"))) {
1480
- log.error("No package.json found");
1481
- log.hint("Run this command from your project root");
1482
- 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
+ };
1483
2263
  }
1484
2264
  const [ideProbe, frameworkResult, providerProbe, pm, buildResult, extInstalled] = await Promise.all([
1485
2265
  detectIDE(root),
@@ -1490,34 +2270,68 @@ async function doctor() {
1490
2270
  isExtensionInstalled()
1491
2271
  ]);
1492
2272
  if (ideProbe.detected.length === 0) {
1493
- log.warn("IDE: not detected");
1494
- result.warnings++;
2273
+ checks.push(createDiagnostic("ide-not-detected", "warning", "IDE: not detected"));
1495
2274
  } else {
1496
2275
  const hasSupported = ideProbe.detected.some((d) => d.supported);
1497
2276
  if (hasSupported) {
1498
- log.success(
1499
- `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
+ )
1500
2287
  );
1501
2288
  } else {
1502
2289
  const names = ideProbe.detected.map((d) => d.ide).join(", ");
1503
- log.warn(`IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`);
1504
- 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
+ );
1505
2301
  }
1506
2302
  }
1507
2303
  if (frameworkResult.supported.length > 0) {
1508
- log.success(`Framework: ${frameworkResult.supported.join(", ")}`);
2304
+ checks.push(
2305
+ createDiagnostic(
2306
+ "framework-supported",
2307
+ "ok",
2308
+ `Framework: ${frameworkResult.supported.join(", ")}`
2309
+ )
2310
+ );
1509
2311
  } else if (frameworkResult.unsupported.length > 0) {
1510
2312
  const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1511
- log.warn(`Framework: ${names} (not supported in v1, React/Vue only)`);
1512
- result.warnings++;
2313
+ checks.push(
2314
+ createDiagnostic(
2315
+ "framework-unsupported",
2316
+ "warning",
2317
+ `Framework: ${names} (not supported in v1, React/Vue only)`
2318
+ )
2319
+ );
1513
2320
  } else {
1514
- log.warn("Framework: not detected (React / Vue expected)");
1515
- result.warnings++;
2321
+ checks.push(
2322
+ createDiagnostic(
2323
+ "framework-not-detected",
2324
+ "warning",
2325
+ "Framework: not detected (React / Vue expected)"
2326
+ )
2327
+ );
1516
2328
  }
1517
2329
  if (providerProbe.detected.length === 0) {
1518
- log.warn("Provider: none detected");
1519
- log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
1520
- 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
+ );
1521
2335
  } else {
1522
2336
  const aiNames = providerProbe.detected.map((d) => {
1523
2337
  const modeLabels = d.providerModes.map(
@@ -1525,58 +2339,80 @@ async function doctor() {
1525
2339
  );
1526
2340
  return `${d.label} (${modeLabels.join(" & ")})`;
1527
2341
  }).join(", ");
1528
- log.success(`Provider: ${aiNames}`);
2342
+ checks.push(createDiagnostic("provider-detected", "ok", `Provider: ${aiNames}`));
1529
2343
  }
1530
- const pluginPath = path10.join(root, "node_modules", "@inspecto-dev", "plugin");
2344
+ const pluginPath = path11.join(root, "node_modules", "@inspecto-dev", "plugin");
1531
2345
  if (await exists(pluginPath)) {
1532
- const pkgJson = await readJSON(path10.join(pluginPath, "package.json"));
2346
+ const pkgJson = await readJSON(path11.join(pluginPath, "package.json"));
1533
2347
  const version = pkgJson?.version ?? "unknown";
1534
- log.success(`@inspecto-dev/plugin@${version} installed`);
2348
+ checks.push(
2349
+ createDiagnostic("plugin-installed", "ok", `@inspecto-dev/plugin@${version} installed`, [], {
2350
+ version
2351
+ })
2352
+ );
1535
2353
  } else {
1536
- log.error("@inspecto-dev/plugin not installed");
1537
- const pm2 = await detectPackageManager(root);
1538
- log.hint(`Fix: ${getInstallCommand(pm2, "@inspecto-dev/plugin")}`);
1539
- result.errors++;
2354
+ checks.push(
2355
+ createDiagnostic("plugin-missing", "error", "@inspecto-dev/plugin not installed", [
2356
+ `Fix: ${getInstallCommand(pm, "@inspecto-dev/plugin")}`
2357
+ ])
2358
+ );
1540
2359
  }
1541
2360
  if (buildResult.supported.length > 0) {
1542
2361
  let injected = false;
1543
2362
  for (const bt of buildResult.supported) {
1544
- const content = await readFile(path10.join(root, bt.configPath));
2363
+ const content = await readFile(path11.join(root, bt.configPath));
1545
2364
  if (content && content.includes("@inspecto-dev/plugin")) {
1546
- 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
+ );
1547
2371
  injected = true;
1548
2372
  break;
1549
2373
  }
1550
2374
  }
1551
2375
  if (!injected) {
1552
- log.error("Plugin not configured in any build config");
1553
- log.hint("Fix: npx @inspecto-dev/cli init");
1554
- 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
+ );
1555
2381
  }
1556
2382
  } else if (buildResult.unsupported.length > 0) {
1557
2383
  const names = buildResult.unsupported.join(", ");
1558
- log.warn(`Build tool: ${names} (not supported in v1)`);
1559
- log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
1560
- 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
+ );
1561
2389
  } else {
1562
- log.warn("No recognized build config found");
1563
- result.warnings++;
2390
+ checks.push(createDiagnostic("build-tool-missing", "warning", "No recognized build config found"));
1564
2391
  }
1565
2392
  if (extInstalled) {
1566
- log.success("VS Code extension detected");
2393
+ checks.push(createDiagnostic("extension-installed", "ok", "VS Code extension detected"));
1567
2394
  } else {
1568
- const hasSupported = ideProbe.detected.some((d) => d.supported);
1569
- if (ideProbe.detected.length > 0 && !hasSupported) {
1570
- 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
+ );
1571
2405
  } else {
1572
- log.error("VS Code extension not found");
1573
- log.hint("Fix: code --install-extension inspecto.inspecto");
1574
- log.hint("Or: https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto");
1575
- 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
+ );
1576
2412
  }
1577
2413
  }
1578
- const settingsJsonPath = path10.join(root, ".inspecto", "settings.json");
1579
- 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");
1580
2416
  const hasSettingsJson = await exists(settingsJsonPath);
1581
2417
  const hasSettingsLocal = await exists(settingsLocalPath);
1582
2418
  if (hasSettingsJson || hasSettingsLocal) {
@@ -1584,48 +2420,108 @@ async function doctor() {
1584
2420
  const fileName = hasSettingsLocal ? "settings.local.json" : "settings.json";
1585
2421
  const settings = await readJSON(targetPath);
1586
2422
  if (settings) {
1587
- log.success(`.inspecto/${fileName} valid`);
2423
+ checks.push(createDiagnostic("settings-valid", "ok", `.inspecto/${fileName} valid`));
1588
2424
  } else {
1589
- log.error(`.inspecto/${fileName} has invalid JSON`);
1590
- log.hint(
1591
- "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
+ )
1592
2437
  );
1593
- result.errors++;
1594
2438
  }
1595
2439
  } else {
1596
- log.warn("No .inspecto/settings.json or settings.local.json found (using defaults)");
1597
- log.hint("Optional: npx @inspecto-dev/cli init");
1598
- 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
+ );
1599
2448
  }
1600
- const gitignoreContent = await readFile(path10.join(root, ".gitignore"));
2449
+ const gitignoreContent = await readFile(path11.join(root, ".gitignore"));
1601
2450
  if (gitignoreContent) {
1602
2451
  const hasLockIgnore = gitignoreContent.includes(".inspecto/install.lock") || gitignoreContent.includes(".inspecto/");
1603
2452
  if (!hasLockIgnore) {
1604
- log.warn(".inspecto/install.lock not in .gitignore");
1605
- log.hint("install.lock contains local machine state and should not be committed");
1606
- 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
+ );
1607
2461
  }
1608
2462
  }
1609
- log.blank();
1610
- if (result.errors === 0 && result.warnings === 0) {
1611
- log.success("All checks passed. Hold Alt + Click to start!");
1612
- } else {
1613
- const parts = [];
1614
- if (result.errors > 0) parts.push(`${result.errors} error(s)`);
1615
- if (result.warnings > 0) parts.push(`${result.warnings} warning(s)`);
1616
- console.log(
1617
- ` ${parts.join(", ")}. ${result.errors > 0 ? "Fix the errors above to get started." : ""}`
1618
- );
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);
1619
2511
  }
1620
- 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);
1621
2517
  }
1622
2518
 
1623
2519
  // src/commands/teardown.ts
1624
- import path11 from "path";
2520
+ import path12 from "path";
1625
2521
  async function teardown() {
1626
2522
  const root = process.cwd();
1627
2523
  log.header("Inspecto Teardown");
1628
- const lockPath = path11.join(root, ".inspecto", "install.lock");
2524
+ const lockPath = path12.join(root, ".inspecto", "install.lock");
1629
2525
  const lock = await readJSON(lockPath);
1630
2526
  if (!lock) {
1631
2527
  log.warn("No .inspecto/install.lock found. Running in best-effort mode.");
@@ -1638,8 +2534,8 @@ async function teardown() {
1638
2534
  } catch {
1639
2535
  log.warn("Could not remove @inspecto-dev/plugin (may not be installed)");
1640
2536
  }
1641
- if (await exists(path11.join(root, ".inspecto"))) {
1642
- await removeDir(path11.join(root, ".inspecto"));
2537
+ if (await exists(path12.join(root, ".inspecto"))) {
2538
+ await removeDir(path12.join(root, ".inspecto"));
1643
2539
  log.success("Deleted .inspecto/ directory");
1644
2540
  }
1645
2541
  await cleanGitignore(root);
@@ -1688,7 +2584,7 @@ async function teardown() {
1688
2584
  }
1689
2585
  }
1690
2586
  }
1691
- await removeDir(path11.join(root, ".inspecto"));
2587
+ await removeDir(path12.join(root, ".inspecto"));
1692
2588
  log.success("Deleted .inspecto/ directory");
1693
2589
  await cleanGitignore(root);
1694
2590
  log.blank();
@@ -1697,8 +2593,13 @@ async function teardown() {
1697
2593
  }
1698
2594
 
1699
2595
  export {
1700
- log,
2596
+ writeCommandOutput,
2597
+ reportCommandError,
2598
+ apply,
2599
+ detect,
1701
2600
  init,
2601
+ collectDoctorResult,
1702
2602
  doctor,
2603
+ plan,
1703
2604
  teardown
1704
2605
  };