@inspecto-dev/cli 0.2.0-alpha.0 → 0.2.0-alpha.2

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.
@@ -77,15 +77,6 @@ async function writeFile(filePath, content) {
77
77
  await fs.mkdir(path.dirname(filePath), { recursive: true });
78
78
  await fs.writeFile(filePath, content, "utf-8");
79
79
  }
80
- async function copyFile(src, dest) {
81
- await fs.copyFile(src, dest);
82
- }
83
- async function removeFile(filePath) {
84
- try {
85
- await fs.unlink(filePath);
86
- } catch {
87
- }
88
- }
89
80
  async function removeDir(dirPath) {
90
81
  try {
91
82
  await fs.rm(dirPath, { recursive: true, force: true });
@@ -146,9 +137,15 @@ async function detectPackageManager(root) {
146
137
  ["yarn.lock", "yarn"],
147
138
  ["package-lock.json", "npm"]
148
139
  ];
149
- for (const [file, pm] of checks) {
150
- if (await exists(path2.join(root, file))) {
151
- return pm;
140
+ const results = await Promise.all(
141
+ checks.map(async ([file, pm]) => {
142
+ const isExist = await exists(path2.join(root, file));
143
+ return { isExist, pm };
144
+ })
145
+ );
146
+ for (const result of results) {
147
+ if (result.isExist) {
148
+ return result.pm;
152
149
  }
153
150
  }
154
151
  return "npm";
@@ -203,7 +200,7 @@ var SUPPORTED_PATTERNS = [
203
200
  },
204
201
  {
205
202
  tool: "esbuild",
206
- files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs"],
203
+ files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs", "build.js", "build.ts"],
207
204
  label: "esbuild"
208
205
  },
209
206
  {
@@ -224,34 +221,79 @@ async function detectBuildTools(root) {
224
221
  const unsupported = [];
225
222
  const pkg = await readJSON(path3.join(root, "package.json"));
226
223
  const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
227
- for (const pattern of SUPPORTED_PATTERNS) {
224
+ const supportedChecks = SUPPORTED_PATTERNS.map(async (pattern) => {
225
+ const hasDep = pattern.tool === "rspack" ? !!(allDeps["@rspack/cli"] || allDeps["@rspack/core"]) : pattern.tool === "webpack" ? !!(allDeps["webpack"] || allDeps["webpack-cli"]) : pattern.tool === "rsbuild" ? !!allDeps["@rsbuild/core"] : !!allDeps[pattern.tool];
226
+ let detectedFile = "";
227
+ if (pattern.tool === "esbuild" && !hasDep) {
228
+ return null;
229
+ }
228
230
  for (const file of pattern.files) {
229
231
  if (await exists(path3.join(root, file))) {
230
- let isLegacyRspack = false;
231
- if (pattern.tool === "rspack") {
232
- const version = allDeps["@rspack/cli"] || allDeps["@rspack/core"];
233
- if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
234
- isLegacyRspack = true;
232
+ detectedFile = file;
233
+ break;
234
+ }
235
+ }
236
+ if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup")) {
237
+ const scripts = pkg?.scripts || {};
238
+ for (const [_, cmd] of Object.entries(scripts)) {
239
+ if (cmd.includes("node ")) {
240
+ const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
241
+ if (match && match[1]) {
242
+ if (await exists(path3.join(root, match[1]))) {
243
+ detectedFile = match[1];
244
+ break;
245
+ }
235
246
  }
247
+ } else if (cmd.includes(`${pattern.tool} `)) {
248
+ detectedFile = "package.json (scripts)";
249
+ break;
236
250
  }
237
- supported.push({
238
- tool: pattern.tool,
239
- configPath: file,
240
- label: `${pattern.label} (${file})${isLegacyRspack ? " [Legacy]" : ""}`,
241
- isLegacyRspack
242
- });
243
- break;
244
251
  }
245
252
  }
253
+ if (detectedFile) {
254
+ let isLegacyRspack = false;
255
+ let isLegacyWebpack = false;
256
+ if (pattern.tool === "rspack") {
257
+ const version = allDeps["@rspack/cli"] || allDeps["@rspack/core"];
258
+ if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
259
+ isLegacyRspack = true;
260
+ }
261
+ } else if (pattern.tool === "webpack") {
262
+ const version = allDeps["webpack"] || allDeps["webpack-cli"];
263
+ if (version && version.includes("^4") || version?.startsWith("4.")) {
264
+ isLegacyWebpack = true;
265
+ }
266
+ }
267
+ return {
268
+ tool: pattern.tool,
269
+ configPath: detectedFile,
270
+ label: `${pattern.label} (${detectedFile})${isLegacyRspack ? " [Legacy]" : ""}${isLegacyWebpack ? " [Webpack 4]" : ""}`,
271
+ isLegacyRspack,
272
+ isLegacyWebpack
273
+ };
274
+ }
275
+ return null;
276
+ });
277
+ const supportedResults = await Promise.all(supportedChecks);
278
+ for (const result of supportedResults) {
279
+ if (result) {
280
+ supported.push(result);
281
+ }
246
282
  }
247
- for (const meta of UNSUPPORTED_META) {
248
- if (!(meta.dep in allDeps)) continue;
283
+ const unsupportedChecks = UNSUPPORTED_META.map(async (meta) => {
284
+ if (!(meta.dep in allDeps)) return null;
249
285
  for (const file of meta.files) {
250
286
  if (await exists(path3.join(root, file))) {
251
- unsupported.push(meta.name);
252
- break;
287
+ return meta.name;
253
288
  }
254
289
  }
290
+ return null;
291
+ });
292
+ const unsupportedResults = await Promise.all(unsupportedChecks);
293
+ for (const result of unsupportedResults) {
294
+ if (result) {
295
+ unsupported.push(result);
296
+ }
255
297
  }
256
298
  return { supported, unsupported };
257
299
  }
@@ -302,39 +344,44 @@ var SUPPORTED_IDE = "vscode";
302
344
  async function detectIDE(root) {
303
345
  const detected = /* @__PURE__ */ new Map();
304
346
  if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
305
- detected.set("Cursor", { ide: "Cursor", supported: false });
347
+ detected.set("Cursor", { ide: "cursor", supported: true });
306
348
  }
307
349
  if (process.env.TRAE_APP_DIR || process.env.__CFBundleIdentifier === "com.byteocean.trae" || process.env.COCO_IDE_PLUGIN_TYPE === "Trae" || process.env.npm_config_user_agent && process.env.npm_config_user_agent.includes("trae")) {
308
- detected.set("Trae", { ide: "Trae", supported: false });
350
+ detected.set("Trae", { ide: "trae", supported: true });
309
351
  }
310
352
  if (process.env.ZED_TERM) {
311
353
  detected.set("Zed", { ide: "Zed", supported: false });
312
354
  }
313
- if (process.env.TERM_PROGRAM === "vscode") {
314
- if (!detected.has("Trae") && !detected.has("Cursor")) {
315
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
316
- }
355
+ 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")) {
356
+ detected.set("Windsurf", { ide: "Windsurf", supported: false });
317
357
  }
318
- if (await exists(path5.join(root, ".trae"))) {
319
- detected.set("Trae", { ide: "Trae", supported: false });
358
+ const [hasTrae, hasCursor, hasVscode, hasIdea] = await Promise.all([
359
+ exists(path5.join(root, ".trae")),
360
+ exists(path5.join(root, ".cursor")),
361
+ exists(path5.join(root, ".vscode")),
362
+ exists(path5.join(root, ".idea"))
363
+ ]);
364
+ if (hasTrae && !detected.has("Trae")) {
365
+ detected.set("Trae", { ide: "trae", supported: true });
320
366
  }
321
- if (await exists(path5.join(root, ".cursor"))) {
322
- detected.set("Cursor", { ide: "Cursor", supported: false });
367
+ if (hasCursor && !detected.has("Cursor")) {
368
+ detected.set("Cursor", { ide: "cursor", supported: true });
323
369
  }
324
- if (await exists(path5.join(root, ".vscode"))) {
325
- if (!detected.has("vscode")) {
326
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
327
- }
370
+ if (hasVscode && !detected.has("vscode")) {
371
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
328
372
  }
329
- if (await exists(path5.join(root, ".idea"))) {
373
+ if (hasIdea && !detected.has("JetBrains IDE")) {
330
374
  detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
331
375
  }
376
+ if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
377
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
378
+ }
332
379
  return {
333
380
  detected: Array.from(detected.values())
334
381
  };
335
382
  }
336
383
 
337
- // src/detect/ai-tool.ts
384
+ // src/detect/provider.ts
338
385
  import path6 from "path";
339
386
  var KNOWN_CLI_TOOLS = [
340
387
  { id: "claude-code", bin: "claude", label: "Claude Code", supported: true },
@@ -344,23 +391,24 @@ var KNOWN_CLI_TOOLS = [
344
391
  ];
345
392
  var KNOWN_IDE_PLUGINS = [
346
393
  { id: "claude-code", extId: "anthropic.claude-code", label: "Claude Code", supported: true },
347
- { id: "github-copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
394
+ { id: "copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
348
395
  { id: "codex", extId: "openai.chatgpt", label: "Codex (ChatGPT)", supported: true },
349
396
  { id: "gemini", extId: "google.geminicodeassist", label: "Gemini Code Assist", supported: true }
350
397
  ];
351
- async function detectAITools(root) {
398
+ async function detectProviders(root) {
352
399
  const detectedMap = /* @__PURE__ */ new Map();
353
- for (const tool of KNOWN_CLI_TOOLS) {
400
+ const cliChecks = KNOWN_CLI_TOOLS.map(async (tool) => {
354
401
  if (await which(tool.bin)) {
355
402
  detectedMap.set(tool.id, {
356
403
  id: tool.id,
357
404
  label: tool.label,
358
405
  supported: tool.supported,
359
- toolModes: ["cli"],
406
+ providerModes: ["cli"],
360
407
  preferredMode: "cli"
361
408
  });
362
409
  }
363
- }
410
+ });
411
+ await Promise.all(cliChecks);
364
412
  const extensionsJsonPath = path6.join(root, ".vscode", "extensions.json");
365
413
  let recommendedExts = [];
366
414
  if (await exists(extensionsJsonPath)) {
@@ -375,35 +423,49 @@ async function detectAITools(root) {
375
423
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
376
424
  const globalExtDir = path6.join(homeDir, ".vscode", "extensions");
377
425
  const globalExtExists = await exists(globalExtDir);
426
+ let installedExtensionFolders = [];
427
+ if (globalExtExists) {
428
+ try {
429
+ const { readdir } = await import("fs/promises");
430
+ installedExtensionFolders = await readdir(globalExtDir);
431
+ const obsoletePath = path6.join(globalExtDir, ".obsolete");
432
+ if (await exists(obsoletePath)) {
433
+ try {
434
+ const obsoleteData = await readJSON(obsoletePath);
435
+ if (obsoleteData) {
436
+ const obsoleteKeys = Object.keys(obsoleteData);
437
+ installedExtensionFolders = installedExtensionFolders.filter((folder) => {
438
+ return !obsoleteKeys.includes(folder);
439
+ });
440
+ }
441
+ } catch {
442
+ }
443
+ }
444
+ } catch {
445
+ }
446
+ }
378
447
  for (const plugin of KNOWN_IDE_PLUGINS) {
379
448
  let isInstalled = false;
380
449
  if (recommendedExts.includes(plugin.extId.toLowerCase())) {
381
450
  isInstalled = true;
382
- } else if (globalExtExists) {
383
- try {
384
- const { readdir } = await import("fs/promises");
385
- const folders = await readdir(globalExtDir);
386
- if (folders.some((f) => {
387
- const lower = f.toLowerCase();
388
- return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
389
- })) {
390
- isInstalled = true;
391
- }
392
- } catch {
393
- }
451
+ } else if (installedExtensionFolders.some((f) => {
452
+ const lower = f.toLowerCase();
453
+ return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
454
+ })) {
455
+ isInstalled = true;
394
456
  }
395
457
  if (isInstalled) {
396
458
  const existing = detectedMap.get(plugin.id);
397
459
  if (existing) {
398
- existing.toolModes.push("plugin");
399
- existing.preferredMode = "plugin";
460
+ existing.providerModes.push("extension");
461
+ existing.preferredMode = "extension";
400
462
  } else {
401
463
  detectedMap.set(plugin.id, {
402
464
  id: plugin.id,
403
465
  label: plugin.label,
404
466
  supported: plugin.supported,
405
- toolModes: ["plugin"],
406
- preferredMode: "plugin"
467
+ providerModes: ["extension"],
468
+ preferredMode: "extension"
407
469
  });
408
470
  }
409
471
  }
@@ -413,207 +475,259 @@ async function detectAITools(root) {
413
475
 
414
476
  // src/inject/ast-injector.ts
415
477
  import path7 from "path";
416
- var IMPORT_MAP = {
417
- vite: `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
418
- webpack: `import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'`,
419
- rspack: `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
420
- rsbuild: `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
421
- esbuild: `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
422
- rollup: `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`
478
+ import { loadFile, writeFile as writeAstFile } from "magicast";
479
+
480
+ // src/inject/strategies/vite.ts
481
+ import { addVitePlugin } from "magicast/helpers";
482
+ var ViteStrategy = class {
483
+ name = "Vite";
484
+ supports(tool) {
485
+ return tool === "vite";
486
+ }
487
+ inject({ mod, detection }) {
488
+ addVitePlugin(mod, {
489
+ from: "@inspecto-dev/plugin",
490
+ constructor: "vitePlugin"
491
+ });
492
+ }
493
+ getManualInstructions(detection, reason) {
494
+ return [
495
+ `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
496
+ "",
497
+ "// Add to your plugins array:",
498
+ `plugins: [`,
499
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
500
+ ` ...otherPlugins`,
501
+ `].filter(Boolean)`
502
+ ];
503
+ }
423
504
  };
424
- function getImportStatement(tool, isLegacyRspack) {
425
- if (tool === "rspack" && isLegacyRspack) {
426
- return `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin/legacy/rspack'`;
505
+
506
+ // src/inject/strategies/webpack.ts
507
+ var WebpackStrategy = class {
508
+ name = "Webpack";
509
+ supports(tool) {
510
+ return tool === "webpack";
511
+ }
512
+ inject(options) {
513
+ throw new Error("Webpack requires manual plugin configuration");
514
+ }
515
+ getManualInstructions(detection, reason) {
516
+ const importPkg = detection.isLegacyWebpack ? "@inspecto-dev/plugin/legacy/webpack4" : "@inspecto-dev/plugin";
517
+ const pluginName = detection.isLegacyWebpack ? "webpack4Plugin" : "webpackPlugin";
518
+ const pluginCall = detection.isLegacyWebpack ? `process.env.NODE_ENV !== 'production' && inspecto({
519
+ pathType: 'absolute',
520
+ escapeTags: ['Transition', 'AnimatePresence'],
521
+ })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
522
+ return [
523
+ `import { ${pluginName} as inspecto } from '${importPkg}'`,
524
+ "",
525
+ "// Add to your plugins array:",
526
+ `plugins: [`,
527
+ ` ${pluginCall},`,
528
+ ` ...otherPlugins`,
529
+ `].filter(Boolean)`
530
+ ];
427
531
  }
428
- return IMPORT_MAP[tool];
429
- }
430
- function getPluginExpression(isLegacyRspack) {
431
- if (isLegacyRspack) {
432
- return `process.env.NODE_ENV !== 'production' && inspecto({
532
+ };
533
+
534
+ // src/inject/strategies/rspack.ts
535
+ var RspackStrategy = class {
536
+ name = "Rspack";
537
+ supports(tool) {
538
+ return tool === "rspack";
539
+ }
540
+ inject(options) {
541
+ throw new Error("Rspack requires manual plugin configuration");
542
+ }
543
+ getManualInstructions(detection, reason) {
544
+ const importPkg = detection.isLegacyRspack ? "@inspecto-dev/plugin/legacy/rspack" : "@inspecto-dev/plugin";
545
+ const pluginCall = detection.isLegacyRspack ? `process.env.NODE_ENV !== 'production' && inspecto({
433
546
  pathType: 'absolute',
434
547
  escapeTags: ['Transition', 'AnimatePresence'],
435
- }) as any`;
548
+ })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
549
+ return [
550
+ `import { rspackPlugin as inspecto } from '${importPkg}'`,
551
+ "",
552
+ "// Add to your plugins array:",
553
+ `plugins: [`,
554
+ ` ${pluginCall},`,
555
+ ` ...otherPlugins`,
556
+ `].filter(Boolean)`
557
+ ];
436
558
  }
437
- return `process.env.NODE_ENV !== 'production' && inspecto()`;
438
- }
439
- function printManualInstructions(tool, configPath, reason, isLegacyRspack) {
440
- const isRsbuild = tool === "rsbuild";
441
- log.warn(`Could not safely auto-inject into ${configPath}`);
442
- log.hint(`(reason: ${reason})`);
443
- log.blank();
444
- log.hint("Please add the following manually:");
445
- if (isRsbuild) {
446
- log.codeBlock([
447
- getImportStatement(tool, isLegacyRspack),
559
+ };
560
+
561
+ // src/inject/strategies/rsbuild.ts
562
+ var RsbuildStrategy = class {
563
+ name = "Rsbuild";
564
+ supports(tool) {
565
+ return tool === "rsbuild";
566
+ }
567
+ inject(options) {
568
+ throw new Error("Rsbuild requires manual plugin configuration due to nested structure");
569
+ }
570
+ getManualInstructions(detection, reason) {
571
+ return [
572
+ `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
448
573
  "",
449
574
  "// Add to tools.rspack:",
450
575
  `tools: {`,
451
576
  ` rspack: {`,
452
577
  ` plugins: [`,
453
- ` ${getPluginExpression(isLegacyRspack)},`,
578
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
454
579
  ` ]`,
455
580
  ` }`,
456
581
  `}`
457
- ]);
458
- } else {
459
- log.codeBlock([
460
- getImportStatement(tool, isLegacyRspack),
582
+ ];
583
+ }
584
+ };
585
+
586
+ // src/inject/strategies/esbuild.ts
587
+ var EsbuildStrategy = class {
588
+ name = "esbuild";
589
+ supports(tool) {
590
+ return tool === "esbuild";
591
+ }
592
+ inject(options) {
593
+ throw new Error("Esbuild requires manual plugin configuration");
594
+ }
595
+ getManualInstructions(detection, reason) {
596
+ return [
597
+ `1. Update your esbuild config (${detection.configPath}):`,
598
+ `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
461
599
  "",
462
600
  "// Add to your plugins array:",
463
601
  `plugins: [`,
464
- ` ${getPluginExpression(isLegacyRspack)},`,
602
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
465
603
  ` ...otherPlugins`,
466
- `].filter(Boolean)`
467
- ]);
468
- }
469
- }
470
- function isAlreadyInjected(content) {
471
- return content.includes("@inspecto-dev/plugin") || content.includes("inspecto()") || content.includes("aiDevInspector");
472
- }
473
- function injectImport(content, importStmt) {
474
- const importRegex = /^import\s.+$/gm;
475
- let lastImportEnd = 0;
476
- let match;
477
- while ((match = importRegex.exec(content)) !== null) {
478
- const lineEnd = content.indexOf("\n", match.index);
479
- if (lineEnd > lastImportEnd) {
480
- lastImportEnd = lineEnd;
481
- }
482
- }
483
- if (lastImportEnd === 0) {
484
- const requireRegex = /^(?:const|let|var)\s.+=\s*require\(.+\).*$/gm;
485
- while ((match = requireRegex.exec(content)) !== null) {
486
- const lineEnd = content.indexOf("\n", match.index);
487
- if (lineEnd > lastImportEnd) {
488
- lastImportEnd = lineEnd;
489
- }
490
- }
604
+ `].filter(Boolean)`,
605
+ "",
606
+ "2. Initialize the client in your app entry (e.g., main.js / index.js):",
607
+ `import { mountInspector } from '@inspecto-dev/core'`,
608
+ "",
609
+ "// Call this before your app renders",
610
+ `if (process.env.NODE_ENV !== 'production') {`,
611
+ ` mountInspector()`,
612
+ `}`
613
+ ];
491
614
  }
492
- if (lastImportEnd > 0) {
493
- return content.slice(0, lastImportEnd) + "\n" + importStmt + content.slice(lastImportEnd);
615
+ };
616
+
617
+ // src/inject/strategies/rollup.ts
618
+ var RollupStrategy = class {
619
+ name = "Rollup";
620
+ supports(tool) {
621
+ return tool === "rollup";
622
+ }
623
+ inject(options) {
624
+ throw new Error("Rollup requires manual plugin configuration");
625
+ }
626
+ getManualInstructions(detection, reason) {
627
+ return [
628
+ `1. Update your rollup config (${detection.configPath}):`,
629
+ `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
630
+ "",
631
+ "// Add to your plugins array:",
632
+ `plugins: [`,
633
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
634
+ ` ...otherPlugins`,
635
+ `].filter(Boolean)`,
636
+ "",
637
+ "2. Initialize the client in your app entry (e.g., main.js / index.js):",
638
+ `import { mountInspector } from '@inspecto-dev/core'`,
639
+ "",
640
+ "// Call this before your app renders",
641
+ `if (process.env.NODE_ENV !== 'production') {`,
642
+ ` mountInspector()`,
643
+ `}`
644
+ ];
494
645
  }
495
- return importStmt + "\n\n" + content;
496
- }
497
- function injectIntoPluginsArray(content, detection) {
498
- const tool = detection.tool;
499
- if (tool === "rsbuild") {
500
- if (!content.includes("tools:") && !content.includes("rspack:")) {
501
- const exportRegex = /(export default defineConfig\(\{)/;
502
- const match2 = exportRegex.exec(content);
503
- if (match2) {
504
- const insertPos2 = match2.index + match2[0].length;
505
- const pluginExpr2 = `
506
- tools: {
507
- rspack: {
508
- plugins: [
509
- ${getPluginExpression(detection.isLegacyRspack)}
510
- ]
511
- }
512
- },`;
513
- return content.slice(0, insertPos2) + pluginExpr2 + content.slice(insertPos2);
514
- }
515
- }
516
- return null;
646
+ };
647
+
648
+ // src/inject/strategies/index.ts
649
+ var STRATEGIES = [
650
+ new ViteStrategy(),
651
+ new WebpackStrategy(),
652
+ new RspackStrategy(),
653
+ new RsbuildStrategy(),
654
+ new EsbuildStrategy(),
655
+ new RollupStrategy()
656
+ ];
657
+
658
+ // src/inject/ast-injector.ts
659
+ function printManualInstructions(strategy, detection, reason) {
660
+ log.warn(`Could not automatically configure ${detection.configPath}`);
661
+ log.hint(`(reason: ${reason})`);
662
+ log.blank();
663
+ log.hint("Please add the following manually:");
664
+ if (strategy) {
665
+ const instructions = strategy.getManualInstructions(detection, reason);
666
+ log.codeBlock(instructions);
667
+ } else {
668
+ log.error(`Unsupported build tool: ${detection.tool}`);
517
669
  }
518
- const pluginsRegex = /(plugins\s*:\s*\[)/;
519
- const match = pluginsRegex.exec(content);
520
- if (!match) return null;
521
- const insertPos = match.index + match[0].length;
522
- const pluginExpr = `
523
- ${getPluginExpression(detection.isLegacyRspack)},`;
524
- return content.slice(0, insertPos) + pluginExpr + content.slice(insertPos);
525
670
  }
526
- function validateBrackets(content) {
527
- const openBraces = (content.match(/\{/g) || []).length;
528
- const closeBraces = (content.match(/\}/g) || []).length;
529
- const openBrackets = (content.match(/\[/g) || []).length;
530
- const closeBrackets = (content.match(/\]/g) || []).length;
531
- return Math.abs(openBraces - closeBraces) <= 1 && Math.abs(openBrackets - closeBrackets) <= 1;
671
+ function isAlreadyInjected(content) {
672
+ return /import\s+.*@inspecto-dev\/plugin/.test(content) || /require\(['"]@inspecto-dev\/plugin['"]\)/.test(content) || /import\s+.*ai-dev-inspector/.test(content) || // Legacy support
673
+ /require\(['"]ai-dev-inspector['"]\)/.test(content);
532
674
  }
533
675
  async function injectPlugin(root, detection, dryRun) {
534
676
  const configPath = path7.join(root, detection.configPath);
535
- const backupPath = configPath + ".bak";
536
677
  const mutations = [];
678
+ const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
537
679
  const content = await readFile(configPath);
538
680
  if (!content) {
539
- printManualInstructions(
540
- detection.tool,
541
- detection.configPath,
542
- "config file not readable",
543
- detection.isLegacyRspack
544
- );
681
+ printManualInstructions(strategy, detection, "config file not readable");
545
682
  return { success: false, mutations, failureReason: "config file not readable" };
546
683
  }
547
684
  if (isAlreadyInjected(content)) {
548
- log.success(`Plugin already injected in ${detection.configPath} (skipped)`);
549
- if (await exists(backupPath)) {
550
- mutations.push({
551
- type: "file_modified",
552
- path: detection.configPath,
553
- backup: detection.configPath + ".bak",
554
- description: "Previously injected inspecto() plugin"
555
- });
556
- } else {
557
- mutations.push({
558
- type: "file_modified",
559
- path: detection.configPath,
560
- description: "Previously injected inspecto() plugin (no backup)"
561
- });
562
- }
563
- return { success: true, mutations };
564
- }
565
- if (!dryRun) {
566
- await copyFile(configPath, backupPath);
685
+ log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
567
686
  mutations.push({
568
687
  type: "file_modified",
569
688
  path: detection.configPath,
570
- backup: detection.configPath + ".bak",
571
- description: "Injected inspecto() plugin"
689
+ description: "Previously configured inspecto() plugin"
572
690
  });
691
+ return { success: true, mutations };
573
692
  }
574
- log.success(`Backed up ${detection.configPath} \u2192 ${detection.configPath}.bak`);
575
- const injected = injectIntoPluginsArray(content, detection);
576
- if (!injected) {
693
+ if (!strategy) {
577
694
  printManualInstructions(
578
- detection.tool,
579
- detection.configPath,
580
- "could not locate plugins array \u2014 file may use dynamic config, function wrapper, or non-standard export",
581
- detection.isLegacyRspack
695
+ strategy,
696
+ detection,
697
+ `No injection strategy found for ${detection.tool}`
582
698
  );
583
- return {
584
- success: false,
585
- mutations,
586
- failureReason: "could not locate plugins array"
587
- };
699
+ return { success: false, mutations, failureReason: "No strategy found" };
588
700
  }
589
- const importStmt = getImportStatement(detection.tool, detection.isLegacyRspack);
590
- const modifiedContent = injectImport(injected, importStmt);
591
- if (!validateBrackets(modifiedContent)) {
592
- log.error("Syntax validation failed after injection");
593
- if (!dryRun) {
594
- await copyFile(backupPath, configPath);
595
- log.success(`Restored ${detection.configPath} from backup`);
596
- }
701
+ if (dryRun) {
702
+ log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
703
+ return { success: true, mutations: [] };
704
+ }
705
+ try {
706
+ const mod = await loadFile(configPath);
707
+ strategy.inject({
708
+ mod,
709
+ detection
710
+ });
711
+ await writeAstFile(mod, configPath);
712
+ mutations.push({
713
+ type: "file_modified",
714
+ path: detection.configPath,
715
+ description: "Automatically configured inspecto() plugin"
716
+ });
717
+ log.success(`Configured plugin in ${detection.configPath}`);
718
+ return { success: true, mutations };
719
+ } catch (err) {
597
720
  printManualInstructions(
598
- detection.tool,
599
- detection.configPath,
600
- "injection produced unbalanced brackets",
601
- detection.isLegacyRspack
721
+ strategy,
722
+ detection,
723
+ `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`
602
724
  );
603
725
  return {
604
726
  success: false,
605
- mutations: [],
606
- // Rolled back
607
- failureReason: "injection produced invalid syntax"
727
+ mutations,
728
+ failureReason: "Automatic configuration unavailable"
608
729
  };
609
730
  }
610
- if (dryRun) {
611
- log.dryRun(`Would inject plugin into ${detection.configPath}`);
612
- } else {
613
- await writeFile(configPath, modifiedContent);
614
- log.success(`Injected plugin into ${detection.configPath}`);
615
- }
616
- return { success: true, mutations };
617
731
  }
618
732
 
619
733
  // src/inject/gitignore.ts
@@ -630,7 +744,7 @@ async function updateGitignore(root, shared, dryRun) {
630
744
  if (!dryRun) {
631
745
  await writeFile(gitignorePath, content);
632
746
  }
633
- log.success("Updated .gitignore: .inspecto/settings.json is now trackable");
747
+ log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
634
748
  return;
635
749
  }
636
750
  const missingRules = desiredRules.filter((rule) => !content.includes(rule));
@@ -692,41 +806,54 @@ async function tryOpenURI(uri) {
692
806
  return false;
693
807
  }
694
808
  }
695
- async function installExtension(dryRun) {
809
+ async function installExtension(dryRun, ide) {
696
810
  if (dryRun) {
697
811
  log.dryRun("Would attempt to install VS Code extension");
698
812
  return null;
699
813
  }
700
- if (await which("code")) {
701
- try {
702
- await run("code", ["--install-extension", EXTENSION_ID]);
703
- log.success("VS Code extension installed via CLI");
704
- return { type: "extension_installed", id: EXTENSION_ID };
705
- } catch {
814
+ const isVSCode = !ide || ide === "vscode";
815
+ if (isVSCode) {
816
+ if (await which("code")) {
817
+ try {
818
+ await run("code", ["--install-extension", EXTENSION_ID]);
819
+ log.success("VS Code extension installed via CLI");
820
+ return { type: "extension_installed", id: EXTENSION_ID };
821
+ } catch {
822
+ }
706
823
  }
707
- }
708
- const codePath = await findVSCodeBinary();
709
- if (codePath) {
710
- try {
711
- await run(codePath, ["--install-extension", EXTENSION_ID]);
712
- log.success("VS Code extension installed via binary path");
713
- log.info('Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future');
714
- return { type: "extension_installed", id: EXTENSION_ID };
715
- } catch {
824
+ const codePath = await findVSCodeBinary();
825
+ if (codePath) {
826
+ try {
827
+ await run(codePath, ["--install-extension", EXTENSION_ID]);
828
+ log.success("VS Code extension installed via binary path");
829
+ log.info(
830
+ 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
831
+ );
832
+ return { type: "extension_installed", id: EXTENSION_ID };
833
+ } catch {
834
+ }
716
835
  }
836
+ const uri = `vscode:extension/${EXTENSION_ID}`;
837
+ if (await tryOpenURI(uri)) {
838
+ log.warn("Opened extension page in VS Code");
839
+ log.hint('Please click "Install" in the opened VS Code window to complete setup.');
840
+ return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
841
+ }
842
+ log.warn("Could not auto-install VS Code extension");
843
+ log.hint("Please install it manually to enable Inspector features:");
844
+ log.hint(" 1. Open VS Code");
845
+ log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
846
+ log.hint(' 3. Search for "Inspecto"');
847
+ log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
848
+ return null;
717
849
  }
718
- const uri = `vscode:extension/${EXTENSION_ID}`;
719
- if (await tryOpenURI(uri)) {
720
- log.warn("Opened extension page in VS Code");
721
- log.hint('Please click "Install" in the opened VS Code window to complete setup.');
722
- return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
723
- }
724
- log.warn("Could not auto-install VS Code extension");
850
+ log.warn(`Could not auto-install extension for ${ide}`);
725
851
  log.hint("Please install it manually to enable Inspector features:");
726
- log.hint(" 1. Open VS Code");
727
- log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
728
- log.hint(' 3. Search for "Inspecto"');
729
- log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
852
+ log.hint(" 1. Download the latest .vsix file from Inspecto releases");
853
+ log.hint(` 2. Open ${ide}`);
854
+ log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
855
+ log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
856
+ log.hint(" 5. Select the downloaded .vsix file");
730
857
  return null;
731
858
  }
732
859
  async function isExtensionInstalled() {
@@ -746,70 +873,49 @@ async function isExtensionInstalled() {
746
873
  }
747
874
  }
748
875
 
749
- // src/commands/init.ts
876
+ // src/prompts.ts
877
+ import prompts from "prompts";
750
878
  async function promptIDEChoice(detections) {
751
879
  if (!process.stdin.isTTY) {
752
880
  log.warn("Multiple IDEs detected but stdin is not interactive");
753
881
  log.hint(`Using: ${detections[0].ide} (first match)`);
754
882
  return detections[0];
755
883
  }
756
- console.log();
757
- console.log(" ? Detected multiple IDEs:");
758
- detections.forEach((d, i) => {
759
- const status = d.supported ? " (supported)" : " (unsupported/limited)";
760
- console.log(` ${i + 1}. ${d.ide}${status}`);
761
- });
762
- console.log();
763
- return new Promise((resolve) => {
764
- process.stdout.write(" > Your choice: ");
765
- process.stdin.resume();
766
- process.stdin.setEncoding("utf-8");
767
- const onData = (data) => {
768
- const choice = parseInt(String(data).trim(), 10);
769
- process.stdin.off("data", onData);
770
- process.stdin.pause();
771
- if (choice >= 1 && choice <= detections.length) {
772
- resolve(detections[choice - 1]);
773
- } else {
774
- resolve(null);
775
- }
776
- };
777
- process.stdin.on("data", onData);
884
+ const { choice } = await prompts({
885
+ type: "select",
886
+ name: "choice",
887
+ message: "Detected multiple IDEs, please choose one:",
888
+ choices: detections.map((d, i) => ({
889
+ title: `${d.ide} ${d.supported ? "(supported)" : "(unsupported/limited)"}`,
890
+ value: i
891
+ }))
778
892
  });
893
+ if (choice === void 0) return null;
894
+ return detections[choice];
779
895
  }
780
- async function promptAIToolChoice(detections) {
896
+ async function promptProviderChoice(detections) {
781
897
  if (!process.stdin.isTTY) {
782
898
  log.warn("Multiple AI tools detected but stdin is not interactive");
783
899
  log.hint(`Using: ${detections[0].label} (first match)`);
784
900
  return detections[0];
785
901
  }
786
- console.log();
787
- console.log(" ? Detected multiple AI tools:");
788
- detections.forEach((d, i) => {
789
- const modeLabels = d.toolModes.map(
790
- (mode) => mode === "plugin" ? "VS Code Extension" : "Terminal CLI"
791
- );
792
- const modeStr = modeLabels.join(" & ");
793
- const status = d.supported ? ` (supported ${modeStr})` : ` (unsupported/limited)`;
794
- console.log(` ${i + 1}. ${d.label}${status}`);
795
- });
796
- console.log();
797
- return new Promise((resolve) => {
798
- process.stdout.write(" > Your choice: ");
799
- process.stdin.resume();
800
- process.stdin.setEncoding("utf-8");
801
- const onData = (data) => {
802
- const choice = parseInt(String(data).trim(), 10);
803
- process.stdin.off("data", onData);
804
- process.stdin.pause();
805
- if (choice >= 1 && choice <= detections.length) {
806
- resolve(detections[choice - 1]);
807
- } else {
808
- resolve(null);
809
- }
810
- };
811
- process.stdin.on("data", onData);
902
+ const { choice } = await prompts({
903
+ type: "select",
904
+ name: "choice",
905
+ message: "Detected multiple providers, please choose one:",
906
+ choices: detections.map((d, i) => {
907
+ const modeLabels = d.providerModes.map(
908
+ (mode) => mode === "extension" ? "VS Code Extension" : "Terminal CLI"
909
+ );
910
+ const modeStr = modeLabels.join(" & ");
911
+ return {
912
+ title: `${d.label} ${d.supported ? `(supported ${modeStr})` : "(unsupported/limited)"}`,
913
+ value: i
914
+ };
915
+ })
812
916
  });
917
+ if (choice === void 0) return null;
918
+ return detections[choice];
813
919
  }
814
920
  async function promptConfigChoice(detections) {
815
921
  if (!process.stdin.isTTY) {
@@ -817,30 +923,93 @@ async function promptConfigChoice(detections) {
817
923
  log.hint(`Using: ${detections[0].label} (first match)`);
818
924
  return detections[0];
819
925
  }
820
- console.log();
821
- console.log(" ? Detected multiple build tool configs:");
822
- detections.forEach((d, i) => {
823
- console.log(` ${i + 1}. ${d.label}`);
926
+ const choices = detections.map((d, i) => ({
927
+ title: d.label,
928
+ value: i
929
+ }));
930
+ choices.push({
931
+ title: "Skip (I'll configure manually)",
932
+ value: -1
824
933
  });
825
- console.log(` ${detections.length + 1}. Skip (I'll configure manually)`);
826
- console.log();
827
- return new Promise((resolve) => {
828
- process.stdout.write(" > Your choice: ");
829
- process.stdin.resume();
830
- process.stdin.setEncoding("utf-8");
831
- const onData = (data) => {
832
- const choice = parseInt(String(data).trim(), 10);
833
- process.stdin.off("data", onData);
834
- process.stdin.pause();
835
- if (choice >= 1 && choice <= detections.length) {
836
- resolve(detections[choice - 1]);
837
- } else {
838
- resolve(null);
839
- }
840
- };
841
- process.stdin.on("data", onData);
934
+ const { choice } = await prompts({
935
+ type: "select",
936
+ name: "choice",
937
+ message: "Detected multiple build tool configs, please choose one to inject:",
938
+ choices
842
939
  });
940
+ if (choice === void 0 || choice === -1) return null;
941
+ return detections[choice];
942
+ }
943
+ async function promptUnsupportedFrameworkContinue() {
944
+ if (!process.stdin.isTTY) {
945
+ log.error("Unsupported framework detected in non-interactive environment.");
946
+ log.hint("Use --force to skip this check and continue anyway.");
947
+ return false;
948
+ }
949
+ const { confirm } = await prompts({
950
+ type: "confirm",
951
+ name: "confirm",
952
+ message: "Inspecto may not work properly. Do you want to continue anyway?",
953
+ initial: false
954
+ });
955
+ return !!confirm;
956
+ }
957
+
958
+ // src/instructions.ts
959
+ function printNuxtManualInstructions() {
960
+ log.blank();
961
+ log.hint("To enable Inspecto in Nuxt, update your nuxt.config.ts:");
962
+ console.log(`\x1B[36m
963
+ import { vitePlugin as inspecto } from '@inspecto-dev/plugin'
964
+ export default defineNuxtConfig({
965
+ vite: {
966
+ plugins: [inspecto()]
967
+ }
968
+ })
969
+ \x1B[0m`);
970
+ log.hint("And create a Nuxt plugin at plugins/inspecto.client.ts:");
971
+ console.log(`\x1B[36m
972
+ export default defineNuxtPlugin(() => {
973
+ if (import.meta.dev) {
974
+ import('@inspecto-dev/core').then(({ mountInspector }) => {
975
+ mountInspector()
976
+ })
977
+ }
978
+ })
979
+ \x1B[0m`);
843
980
  }
981
+ function printNextJsManualInstructions() {
982
+ log.blank();
983
+ log.hint("To enable Inspecto in Next.js, update your next.config.mjs:");
984
+ console.log(`\x1B[36m
985
+ import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'
986
+ const nextConfig = {
987
+ webpack: (config, { dev, isServer }) => {
988
+ if (dev && !isServer) config.plugins.push(inspecto())
989
+ return config
990
+ }
991
+ }
992
+ export default nextConfig
993
+ \x1B[0m`);
994
+ log.hint("And initialize the client dynamically in your app/layout.tsx (or pages/_app.tsx):");
995
+ console.log(`\x1B[36m
996
+ 'use client'
997
+ import { useEffect } from 'react'
998
+
999
+ export default function RootLayout({ children }) {
1000
+ useEffect(() => {
1001
+ if (process.env.NODE_ENV !== 'production') {
1002
+ import('@inspecto-dev/core').then(({ mountInspector }) => {
1003
+ mountInspector({ serverUrl: 'http://127.0.0.1:5678' })
1004
+ })
1005
+ }
1006
+ }, [])
1007
+ return <html><body>{children}</body></html>
1008
+ }
1009
+ \x1B[0m`);
1010
+ }
1011
+
1012
+ // src/commands/init.ts
844
1013
  async function init(options) {
845
1014
  const root = process.cwd();
846
1015
  const mutations = [];
@@ -850,37 +1019,55 @@ async function init(options) {
850
1019
  log.hint("Run this command from your project root");
851
1020
  return;
852
1021
  }
853
- const pm = await detectPackageManager(root);
1022
+ const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
1023
+ detectPackageManager(root),
1024
+ detectFrameworks(root),
1025
+ detectBuildTools(root),
1026
+ detectIDE(root),
1027
+ detectProviders(root)
1028
+ ]);
854
1029
  log.success(`Detected package manager: ${pm}`);
855
- const frameworkResult = await detectFrameworks(root);
856
1030
  if (frameworkResult.supported.length > 0) {
857
1031
  log.success(`Detected framework: ${frameworkResult.supported.join(", ")}`);
858
1032
  }
859
- if (frameworkResult.unsupported.length > 0) {
860
- const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
861
- log.warn(`Detected ${names} \u2014 not supported in v1 (React / Vue only)`);
862
- log.hint("Inspecto may still work but is not tested for this framework");
863
- }
864
- if (frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0) {
865
- log.warn("No frontend framework detected");
866
- log.hint("Inspecto v1 supports React and Vue projects");
1033
+ const hasUnsupportedFramework = frameworkResult.unsupported.length > 0;
1034
+ const hasNoFramework = frameworkResult.supported.length === 0 && frameworkResult.unsupported.length === 0;
1035
+ if (hasUnsupportedFramework || hasNoFramework) {
1036
+ if (hasUnsupportedFramework) {
1037
+ const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1038
+ log.warn(`Detected ${names} \u2014 not supported in v1 (React / Vue only)`);
1039
+ } else {
1040
+ log.warn("No frontend framework detected");
1041
+ log.hint("Inspecto current version supports React and Vue projects");
1042
+ }
1043
+ if (!options.force) {
1044
+ const shouldContinue = await promptUnsupportedFrameworkContinue();
1045
+ if (!shouldContinue) {
1046
+ log.warn("Initialization aborted.");
1047
+ return;
1048
+ }
1049
+ } else {
1050
+ log.warn("Continuing anyway (--force)");
1051
+ }
867
1052
  }
868
- const buildResult = await detectBuildTools(root);
1053
+ let manualConfigRequiredFor = "";
869
1054
  if (buildResult.supported.length > 0) {
870
1055
  buildResult.supported.forEach((bt) => log.success(`Detected: ${bt.label}`));
871
1056
  }
872
1057
  if (buildResult.unsupported.length > 0) {
873
1058
  const names = buildResult.unsupported.join(", ");
874
- log.warn(`Detected ${names} \u2014 not supported in v1`);
875
- log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
876
- log.hint("Meta-framework support (Next.js, Nuxt, etc.) is planned for v2");
1059
+ manualConfigRequiredFor = buildResult.unsupported[0] || "";
1060
+ log.warn(`Detected ${names} \u2014 automatic plugin injection is not supported in current version`);
1061
+ log.hint("You can still manually configure it by modifying your configuration file");
877
1062
  }
878
1063
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
879
1064
  log.warn("No recognized build tool detected");
880
- log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
1065
+ log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
881
1066
  log.hint("Dependency will be installed but plugin injection will be skipped");
1067
+ log.hint(
1068
+ "Please refer to the manual setup guide: https://inspecto.dev/docs/getting-started/manual-setup"
1069
+ );
882
1070
  }
883
- const ideProbe = await detectIDE(root);
884
1071
  let selectedIDE = null;
885
1072
  if (ideProbe.detected.length === 0) {
886
1073
  log.error("No IDE detected in current project");
@@ -901,21 +1088,20 @@ async function init(options) {
901
1088
  log.hint(`Features may be severely limited or unavailable in ${selectedIDE.ide}.`);
902
1089
  }
903
1090
  }
904
- const aiToolProbe = await detectAITools(root);
905
- let selectedAITool = null;
906
- if (!options.prefer) {
907
- if (aiToolProbe.detected.length === 0) {
1091
+ let selectedProvider = null;
1092
+ if (!options.provider) {
1093
+ if (providerProbe.detected.length === 0) {
908
1094
  log.warn("No supported AI tools detected");
909
1095
  log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
910
- } else if (aiToolProbe.detected.length === 1) {
911
- selectedAITool = aiToolProbe.detected[0];
912
- if (selectedAITool.supported) {
913
- log.success(`Detected AI tool: ${selectedAITool.label}`);
1096
+ } else if (providerProbe.detected.length === 1) {
1097
+ selectedProvider = providerProbe.detected[0];
1098
+ if (selectedProvider.supported) {
1099
+ log.success(`Detected AI tool: ${selectedProvider.label}`);
914
1100
  }
915
1101
  } else {
916
- selectedAITool = await promptAIToolChoice(aiToolProbe.detected);
917
- if (selectedAITool) {
918
- log.success(`Selected AI tool: ${selectedAITool.label}`);
1102
+ selectedProvider = await promptProviderChoice(providerProbe.detected);
1103
+ if (selectedProvider) {
1104
+ log.success(`Selected provider: ${selectedProvider.label}`);
919
1105
  }
920
1106
  }
921
1107
  }
@@ -923,7 +1109,7 @@ async function init(options) {
923
1109
  if (options.skipInstall) {
924
1110
  log.warn("Skipping dependency installation (--skip-install)");
925
1111
  } else {
926
- const installCmd = getInstallCommand(pm, "@inspecto-dev/plugin");
1112
+ const installCmd = getInstallCommand(pm, "@inspecto-dev/plugin @inspecto-dev/core");
927
1113
  if (options.dryRun) {
928
1114
  log.dryRun(`Would run: ${installCmd}`);
929
1115
  } else {
@@ -932,12 +1118,9 @@ async function init(options) {
932
1118
  if (result.stderr && result.stderr.toLowerCase().includes("error")) {
933
1119
  throw new Error(result.stderr);
934
1120
  }
935
- log.success("Installed @inspecto-dev/plugin as devDependency");
936
- mutations.push({
937
- type: "dependency_added",
938
- name: "@inspecto-dev/plugin",
939
- dev: true
940
- });
1121
+ log.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
1122
+ mutations.push({ type: "dependency_added", name: "@inspecto-dev/plugin", dev: true });
1123
+ mutations.push({ type: "dependency_added", name: "@inspecto-dev/core", dev: true });
941
1124
  } catch (err) {
942
1125
  installFailed = true;
943
1126
  log.error(`Failed to install dependency: ${err?.message || "Unknown error"}`);
@@ -964,55 +1147,50 @@ async function init(options) {
964
1147
  }
965
1148
  }
966
1149
  const settingsDir = path9.join(root, ".inspecto");
967
- const settingsPath = path9.join(settingsDir, "settings.json");
968
- const promptsPath = path9.join(settingsDir, "prompts.json");
1150
+ const settingsFileName = options.shared ? "settings.json" : "settings.local.json";
1151
+ const promptsFileName = options.shared ? "prompts.json" : "prompts.local.json";
1152
+ const settingsPath = path9.join(settingsDir, settingsFileName);
1153
+ const promptsPath = path9.join(settingsDir, promptsFileName);
969
1154
  if (await exists(settingsPath)) {
970
1155
  const existingSettings = await readJSON(settingsPath);
971
1156
  if (existingSettings === null) {
972
- log.warn(".inspecto/settings.json exists but contains invalid JSON");
1157
+ log.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`);
973
1158
  log.hint("Please fix the syntax errors manually, or delete it and re-run init");
974
1159
  } else {
975
- log.success(".inspecto/settings.json already exists (skipped)");
1160
+ log.success(`.inspecto/${settingsFileName} already exists (skipped)`);
976
1161
  }
977
1162
  } else {
978
1163
  const defaultSettings = {};
979
1164
  if (selectedIDE && selectedIDE.supported) {
980
- defaultSettings.ide = selectedIDE.ide === "vscode" ? "vscode" : selectedIDE.ide;
1165
+ defaultSettings.ide = selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : selectedIDE.ide.toLowerCase();
981
1166
  }
982
- if (options.prefer) {
983
- defaultSettings.prefer = options.prefer;
984
- } else if (selectedAITool) {
985
- defaultSettings.prefer = selectedAITool.id;
986
- if (selectedAITool.preferredMode) {
987
- defaultSettings.providers = {
988
- [selectedAITool.id]: {
989
- type: selectedAITool.preferredMode
990
- }
991
- };
992
- }
1167
+ if (options.provider) {
1168
+ const tool = options.provider;
1169
+ const mode = tool === "coco" ? "cli" : "extension";
1170
+ defaultSettings["provider.default"] = `${tool}.${mode}`;
1171
+ } else if (selectedProvider) {
1172
+ const toolId = selectedProvider.id;
1173
+ const mode = selectedProvider.preferredMode === "cli" ? "cli" : "extension";
1174
+ defaultSettings["provider.default"] = `${toolId}.${mode}`;
993
1175
  }
994
1176
  if (options.dryRun) {
995
- log.dryRun("Would create .inspecto/settings.json");
1177
+ log.dryRun(`Would create .inspecto/${settingsFileName}`);
996
1178
  } else {
997
1179
  await writeJSON(settingsPath, defaultSettings);
998
- log.success("Created .inspecto/settings.json");
999
- mutations.push({ type: "file_created", path: ".inspecto/settings.json" });
1180
+ log.success(`Created .inspecto/${settingsFileName}`);
1181
+ mutations.push({ type: "file_created", path: `.inspecto/${settingsFileName}` });
1000
1182
  }
1001
1183
  }
1002
1184
  if (await exists(promptsPath)) {
1003
- log.success(".inspecto/prompts.json already exists (skipped)");
1185
+ log.success(`.inspecto/${promptsFileName} already exists (skipped)`);
1004
1186
  } else {
1005
- const defaultPrompts = [
1006
- { id: "code-review", enabled: false },
1007
- { id: "generate-test", enabled: false },
1008
- { id: "performance", enabled: false }
1009
- ];
1187
+ const defaultPrompts = [];
1010
1188
  if (options.dryRun) {
1011
- log.dryRun("Would create .inspecto/prompts.json");
1189
+ log.dryRun(`Would create .inspecto/${promptsFileName}`);
1012
1190
  } else {
1013
1191
  await writeJSON(promptsPath, defaultPrompts);
1014
- log.success("Created .inspecto/prompts.json (disabling low-frequency intents)");
1015
- mutations.push({ type: "file_created", path: ".inspecto/prompts.json" });
1192
+ log.success(`Created .inspecto/${promptsFileName}`);
1193
+ mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
1016
1194
  }
1017
1195
  }
1018
1196
  if (!options.dryRun) {
@@ -1036,10 +1214,10 @@ async function init(options) {
1036
1214
  const shouldInstallExt = !options.noExtension && (!selectedIDE || selectedIDE && selectedIDE.supported);
1037
1215
  let manualExtensionInstallNeeded = false;
1038
1216
  if (options.noExtension) {
1039
- log.warn("Skipping VS Code extension (--no-extension)");
1217
+ log.warn("Skipping IDE extension (--no-extension)");
1040
1218
  } else if (!shouldInstallExt) {
1041
1219
  } else {
1042
- const extMutation = await installExtension(options.dryRun);
1220
+ const extMutation = await installExtension(options.dryRun, selectedIDE?.ide);
1043
1221
  if (extMutation && !options.dryRun) {
1044
1222
  mutations.push(extMutation);
1045
1223
  if (extMutation.manual_action_required) {
@@ -1058,10 +1236,16 @@ async function init(options) {
1058
1236
  if (options.dryRun) {
1059
1237
  log.blank();
1060
1238
  log.warn("Dry run complete. No files were modified.");
1061
- } else if (installFailed || injectionFailed || manualExtensionInstallNeeded) {
1239
+ } else if (installFailed || injectionFailed || manualExtensionInstallNeeded || manualConfigRequiredFor) {
1062
1240
  log.blank();
1063
1241
  log.warn("Setup completed with some manual steps required.");
1064
- log.hint("Please check the logs above and complete the manual steps.");
1242
+ if (manualConfigRequiredFor === "Nuxt") {
1243
+ printNuxtManualInstructions();
1244
+ } else if (manualConfigRequiredFor === "Next.js") {
1245
+ printNextJsManualInstructions();
1246
+ } else {
1247
+ log.hint("Please check the logs above and complete the manual steps.");
1248
+ }
1065
1249
  log.blank();
1066
1250
  } else {
1067
1251
  log.ready("Ready! Hold Alt + Click any element to inspect.");
@@ -1079,7 +1263,14 @@ async function doctor() {
1079
1263
  log.hint("Run this command from your project root");
1080
1264
  return;
1081
1265
  }
1082
- const ideProbe = await detectIDE(root);
1266
+ const [ideProbe, frameworkResult, providerProbe, pm, buildResult, extInstalled] = await Promise.all([
1267
+ detectIDE(root),
1268
+ detectFrameworks(root),
1269
+ detectProviders(root),
1270
+ detectPackageManager(root),
1271
+ detectBuildTools(root),
1272
+ isExtensionInstalled()
1273
+ ]);
1083
1274
  if (ideProbe.detected.length === 0) {
1084
1275
  log.warn("IDE: not detected");
1085
1276
  result.warnings++;
@@ -1091,11 +1282,10 @@ async function doctor() {
1091
1282
  );
1092
1283
  } else {
1093
1284
  const names = ideProbe.detected.map((d) => d.ide).join(", ");
1094
- log.warn(`IDE: ${names} (not supported in v1, VS Code only)`);
1285
+ log.warn(`IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`);
1095
1286
  result.warnings++;
1096
1287
  }
1097
1288
  }
1098
- const frameworkResult = await detectFrameworks(root);
1099
1289
  if (frameworkResult.supported.length > 0) {
1100
1290
  log.success(`Framework: ${frameworkResult.supported.join(", ")}`);
1101
1291
  } else if (frameworkResult.unsupported.length > 0) {
@@ -1106,57 +1296,54 @@ async function doctor() {
1106
1296
  log.warn("Framework: not detected (React / Vue expected)");
1107
1297
  result.warnings++;
1108
1298
  }
1109
- const aiProbe = await detectAITools(root);
1110
- if (aiProbe.detected.length === 0) {
1111
- log.warn("AI Tool: none detected");
1299
+ if (providerProbe.detected.length === 0) {
1300
+ log.warn("Provider: none detected");
1112
1301
  log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
1113
1302
  result.warnings++;
1114
1303
  } else {
1115
- const aiNames = aiProbe.detected.map((d) => {
1116
- const modeLabels = d.toolModes.map(
1117
- (mode) => mode === "plugin" ? "VS Code Extension" : "Terminal CLI"
1304
+ const aiNames = providerProbe.detected.map((d) => {
1305
+ const modeLabels = d.providerModes.map(
1306
+ (mode) => mode === "extension" ? "VS Code Extension" : "Terminal CLI"
1118
1307
  );
1119
1308
  return `${d.label} (${modeLabels.join(" & ")})`;
1120
1309
  }).join(", ");
1121
- log.success(`AI Tool: ${aiNames}`);
1310
+ log.success(`Provider: ${aiNames}`);
1122
1311
  }
1123
- const pluginPath = path10.join(root, "node_modules", "@inspecto", "plugin");
1312
+ const pluginPath = path10.join(root, "node_modules", "@inspecto-dev", "plugin");
1124
1313
  if (await exists(pluginPath)) {
1125
1314
  const pkgJson = await readJSON(path10.join(pluginPath, "package.json"));
1126
1315
  const version = pkgJson?.version ?? "unknown";
1127
1316
  log.success(`@inspecto-dev/plugin@${version} installed`);
1128
1317
  } else {
1129
1318
  log.error("@inspecto-dev/plugin not installed");
1130
- const pm = await detectPackageManager(root);
1131
- log.hint(`Fix: ${getInstallCommand(pm, "@inspecto-dev/plugin")}`);
1319
+ const pm2 = await detectPackageManager(root);
1320
+ log.hint(`Fix: ${getInstallCommand(pm2, "@inspecto-dev/plugin")}`);
1132
1321
  result.errors++;
1133
1322
  }
1134
- const buildResult = await detectBuildTools(root);
1135
1323
  if (buildResult.supported.length > 0) {
1136
1324
  let injected = false;
1137
1325
  for (const bt of buildResult.supported) {
1138
1326
  const content = await readFile(path10.join(root, bt.configPath));
1139
1327
  if (content && content.includes("@inspecto-dev/plugin")) {
1140
- log.success(`Plugin injected in ${bt.configPath}`);
1328
+ log.success(`Plugin configured in ${bt.configPath}`);
1141
1329
  injected = true;
1142
1330
  break;
1143
1331
  }
1144
1332
  }
1145
1333
  if (!injected) {
1146
- log.error("Plugin not injected in any build config");
1147
- log.hint("Fix: npx inspecto init");
1334
+ log.error("Plugin not configured in any build config");
1335
+ log.hint("Fix: npx @inspecto-dev/cli init");
1148
1336
  result.errors++;
1149
1337
  }
1150
1338
  } else if (buildResult.unsupported.length > 0) {
1151
1339
  const names = buildResult.unsupported.join(", ");
1152
1340
  log.warn(`Build tool: ${names} (not supported in v1)`);
1153
- log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
1341
+ log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
1154
1342
  result.warnings++;
1155
1343
  } else {
1156
1344
  log.warn("No recognized build config found");
1157
1345
  result.warnings++;
1158
1346
  }
1159
- const extInstalled = await isExtensionInstalled();
1160
1347
  if (extInstalled) {
1161
1348
  log.success("VS Code extension detected");
1162
1349
  } else {
@@ -1170,21 +1357,26 @@ async function doctor() {
1170
1357
  result.errors++;
1171
1358
  }
1172
1359
  }
1173
- const settingsPath = path10.join(root, ".inspecto", "settings.json");
1174
- if (await exists(settingsPath)) {
1175
- const settings = await readJSON(settingsPath);
1360
+ const settingsJsonPath = path10.join(root, ".inspecto", "settings.json");
1361
+ const settingsLocalPath = path10.join(root, ".inspecto", "settings.local.json");
1362
+ const hasSettingsJson = await exists(settingsJsonPath);
1363
+ const hasSettingsLocal = await exists(settingsLocalPath);
1364
+ if (hasSettingsJson || hasSettingsLocal) {
1365
+ const targetPath = hasSettingsLocal ? settingsLocalPath : settingsJsonPath;
1366
+ const fileName = hasSettingsLocal ? "settings.local.json" : "settings.json";
1367
+ const settings = await readJSON(targetPath);
1176
1368
  if (settings) {
1177
- log.success(".inspecto/settings.json valid");
1369
+ log.success(`.inspecto/${fileName} valid`);
1178
1370
  } else {
1179
- log.error(".inspecto/settings.json has invalid JSON");
1371
+ log.error(`.inspecto/${fileName} has invalid JSON`);
1180
1372
  log.hint(
1181
- "Fix: Manually correct the syntax errors, or delete the file and re-run npx inspecto init"
1373
+ "Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init"
1182
1374
  );
1183
1375
  result.errors++;
1184
1376
  }
1185
1377
  } else {
1186
- log.warn(".inspecto/settings.json not found (using defaults)");
1187
- log.hint("Optional: npx inspecto init");
1378
+ log.warn("No .inspecto/settings.json or settings.local.json found (using defaults)");
1379
+ log.hint("Optional: npx @inspecto-dev/cli init");
1188
1380
  result.warnings++;
1189
1381
  }
1190
1382
  const gitignoreContent = await readFile(path10.join(root, ".gitignore"));
@@ -1234,7 +1426,7 @@ async function teardown() {
1234
1426
  }
1235
1427
  await cleanGitignore(root);
1236
1428
  log.success("Cleaned .gitignore entries");
1237
- log.warn("Cannot restore build config (no backup reference)");
1429
+ log.warn("Cannot restore build config auto-magically");
1238
1430
  log.hint("Please manually remove the inspecto() plugin from your build config");
1239
1431
  log.blank();
1240
1432
  return;
@@ -1244,24 +1436,12 @@ async function teardown() {
1244
1436
  for (const mutation of lock.mutations) {
1245
1437
  switch (mutation.type) {
1246
1438
  case "file_modified": {
1247
- if (mutation.backup && mutation.path) {
1248
- const backupPath = path11.join(root, mutation.backup);
1249
- const targetPath = path11.join(root, mutation.path);
1250
- if (mutation.path === ".gitignore") {
1251
- await cleanGitignore(root);
1252
- log.success("Cleaned .gitignore entries");
1253
- await removeFile(backupPath);
1254
- } else if (await exists(backupPath)) {
1255
- await copyFile(backupPath, targetPath);
1256
- await removeFile(backupPath);
1257
- log.success(`Restored ${mutation.path} from backup`);
1258
- } else {
1259
- log.warn(`Backup not found: ${mutation.backup}`);
1260
- log.hint(`Please manually remove inspecto from ${mutation.path}`);
1261
- }
1262
- } else if (mutation.path && mutation.path !== ".gitignore") {
1263
- log.warn(`Cannot auto-restore ${mutation.path} (no backup recorded)`);
1264
- log.hint(`Please manually remove the inspecto() plugin from ${mutation.path}`);
1439
+ if (!mutation.path) break;
1440
+ if (mutation.path === ".gitignore") {
1441
+ log.success("Cleaned .gitignore entries");
1442
+ } else if (mutation.path) {
1443
+ log.warn(`Cannot auto-restore ${mutation.path}`);
1444
+ log.hint(`Please manually remove the inspecto plugin from ${mutation.path}`);
1265
1445
  }
1266
1446
  break;
1267
1447
  }