@inspecto-dev/cli 0.2.0-alpha.1 → 0.2.0-alpha.3

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";
@@ -180,6 +177,31 @@ function getUninstallCommand(pm, pkg) {
180
177
 
181
178
  // src/detect/build-tool.ts
182
179
  import path3 from "path";
180
+ import { createRequire } from "module";
181
+ function isPackageResolvable(pkgName, root) {
182
+ try {
183
+ const require2 = createRequire(path3.join(root, "package.json"));
184
+ try {
185
+ require2.resolve(`${pkgName}/package.json`, { paths: [root] });
186
+ return true;
187
+ } catch {
188
+ require2.resolve(pkgName, { paths: [root] });
189
+ return true;
190
+ }
191
+ } catch {
192
+ return false;
193
+ }
194
+ }
195
+ async function getResolvedPackageVersion(pkgName, root) {
196
+ try {
197
+ const require2 = createRequire(path3.join(root, "package.json"));
198
+ const pkgJsonPath = require2.resolve(`${pkgName}/package.json`, { paths: [root] });
199
+ const pkg = await readJSON(pkgJsonPath);
200
+ return pkg?.version || null;
201
+ } catch {
202
+ return null;
203
+ }
204
+ }
183
205
  var SUPPORTED_PATTERNS = [
184
206
  {
185
207
  tool: "vite",
@@ -203,7 +225,7 @@ var SUPPORTED_PATTERNS = [
203
225
  },
204
226
  {
205
227
  tool: "esbuild",
206
- files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs"],
228
+ files: ["esbuild.config.js", "esbuild.config.ts", "esbuild.config.mjs", "build.js", "build.ts"],
207
229
  label: "esbuild"
208
230
  },
209
231
  {
@@ -224,34 +246,110 @@ async function detectBuildTools(root) {
224
246
  const unsupported = [];
225
247
  const pkg = await readJSON(path3.join(root, "package.json"));
226
248
  const allDeps = { ...pkg?.dependencies, ...pkg?.devDependencies };
227
- for (const pattern of SUPPORTED_PATTERNS) {
249
+ const supportedChecks = SUPPORTED_PATTERNS.map(async (pattern) => {
250
+ let hasDep;
251
+ let resolvedVersion = null;
252
+ if (pattern.tool === "rspack") {
253
+ const depName = allDeps["@rspack/cli"] ? "@rspack/cli" : "@rspack/core";
254
+ hasDep = !!allDeps["@rspack/cli"] || !!allDeps["@rspack/core"] || isPackageResolvable("@rspack/core", root);
255
+ if (hasDep) {
256
+ resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("@rspack/core", root);
257
+ }
258
+ } else if (pattern.tool === "webpack") {
259
+ const depName = allDeps["webpack"] ? "webpack" : "webpack-cli";
260
+ hasDep = !!allDeps["webpack"] || !!allDeps["webpack-cli"] || isPackageResolvable("webpack", root);
261
+ if (hasDep) {
262
+ resolvedVersion = allDeps[depName] || await getResolvedPackageVersion("webpack", root);
263
+ }
264
+ } else if (pattern.tool === "rsbuild") {
265
+ hasDep = !!allDeps["@rsbuild/core"] || isPackageResolvable("@rsbuild/core", root);
266
+ } else {
267
+ hasDep = !!allDeps[pattern.tool] || isPackageResolvable(pattern.tool, root);
268
+ }
269
+ let detectedFile = "";
270
+ if (pattern.tool === "esbuild" && !hasDep) {
271
+ return null;
272
+ }
228
273
  for (const file of pattern.files) {
229
274
  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;
275
+ detectedFile = file;
276
+ break;
277
+ }
278
+ }
279
+ if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "rsbuild")) {
280
+ const scripts = pkg?.scripts || {};
281
+ for (const cmd of Object.values(scripts)) {
282
+ if (cmd.includes("node ")) {
283
+ const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
284
+ if (match && match[1]) {
285
+ if (await exists(path3.join(root, match[1]))) {
286
+ if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
287
+ detectedFile = match[1];
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ } else if (cmd.includes(`${pattern.tool} `)) {
293
+ if (pattern.tool === "webpack" || pattern.tool === "rspack") {
294
+ const configMatch = cmd.match(/--config\s+([^\s]+)/);
295
+ if (configMatch && configMatch[1]) {
296
+ if (await exists(path3.join(root, configMatch[1]))) {
297
+ detectedFile = configMatch[1];
298
+ break;
299
+ }
300
+ }
301
+ }
302
+ if (!detectedFile) {
303
+ detectedFile = "package.json (scripts)";
304
+ break;
235
305
  }
236
306
  }
237
- supported.push({
238
- tool: pattern.tool,
239
- configPath: file,
240
- label: `${pattern.label} (${file})${isLegacyRspack ? " [Legacy]" : ""}`,
241
- isLegacyRspack
242
- });
243
- break;
244
307
  }
245
308
  }
309
+ if (detectedFile) {
310
+ let isLegacyRspack = false;
311
+ let isLegacyWebpack = false;
312
+ if (pattern.tool === "rspack") {
313
+ const version = resolvedVersion;
314
+ if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
315
+ isLegacyRspack = true;
316
+ }
317
+ } else if (pattern.tool === "webpack") {
318
+ const version = resolvedVersion;
319
+ if (version && version.includes("^4") || version?.startsWith("4.")) {
320
+ isLegacyWebpack = true;
321
+ }
322
+ }
323
+ return {
324
+ tool: pattern.tool,
325
+ configPath: detectedFile,
326
+ label: `${pattern.label} (${detectedFile})${isLegacyRspack ? " [Legacy]" : ""}${isLegacyWebpack ? " [Webpack 4]" : ""}`,
327
+ isLegacyRspack,
328
+ isLegacyWebpack
329
+ };
330
+ }
331
+ return null;
332
+ });
333
+ const supportedResults = await Promise.all(supportedChecks);
334
+ for (const result of supportedResults) {
335
+ if (result) {
336
+ supported.push(result);
337
+ }
246
338
  }
247
- for (const meta of UNSUPPORTED_META) {
248
- if (!(meta.dep in allDeps)) continue;
339
+ const unsupportedChecks = UNSUPPORTED_META.map(async (meta) => {
340
+ if (!(meta.dep in allDeps)) return null;
249
341
  for (const file of meta.files) {
250
342
  if (await exists(path3.join(root, file))) {
251
- unsupported.push(meta.name);
252
- break;
343
+ return meta.name;
253
344
  }
254
345
  }
346
+ return null;
347
+ });
348
+ const unsupportedResults = await Promise.all(unsupportedChecks);
349
+ for (const result of unsupportedResults) {
350
+ if (result) {
351
+ unsupported.push(result);
352
+ }
255
353
  }
256
354
  return { supported, unsupported };
257
355
  }
@@ -263,6 +361,18 @@ function resolveInjectionTarget(detections) {
263
361
 
264
362
  // src/detect/framework.ts
265
363
  import path4 from "path";
364
+ import { createRequire as createRequire2 } from "module";
365
+ var META_FRAMEWORK_MAP = {
366
+ next: "react",
367
+ nuxt: "vue",
368
+ "@remix-run/react": "react",
369
+ "@remix-run/dev": "react",
370
+ "@vue/nuxt": "vue",
371
+ "vite-plugin-vue": "vue",
372
+ "@vitejs/plugin-vue": "vue",
373
+ "@vitejs/plugin-react": "react",
374
+ "@vitejs/plugin-react-swc": "react"
375
+ };
266
376
  var SUPPORTED_FRAMEWORKS = [
267
377
  { framework: "react", deps: ["react", "react-dom"] },
268
378
  { framework: "vue", deps: ["vue"] }
@@ -274,26 +384,53 @@ var UNSUPPORTED_FRAMEWORKS = [
274
384
  { name: "Preact", dep: "preact" },
275
385
  { name: "Lit", dep: "lit" }
276
386
  ];
387
+ function isPackageResolvable2(pkgName, root) {
388
+ try {
389
+ const require2 = createRequire2(path4.join(root, "package.json"));
390
+ try {
391
+ require2.resolve(`${pkgName}/package.json`, { paths: [root] });
392
+ return true;
393
+ } catch {
394
+ require2.resolve(pkgName, { paths: [root] });
395
+ return true;
396
+ }
397
+ } catch {
398
+ return false;
399
+ }
400
+ }
277
401
  async function detectFrameworks(root) {
278
402
  const pkg = await readJSON(path4.join(root, "package.json"));
279
- if (!pkg) return { supported: [], unsupported: [] };
280
403
  const allDeps = {
281
- ...pkg.dependencies,
282
- ...pkg.devDependencies
404
+ ...pkg?.dependencies || {},
405
+ ...pkg?.devDependencies || {},
406
+ ...pkg?.peerDependencies || {}
283
407
  };
284
- const supported = [];
408
+ const supportedSet = /* @__PURE__ */ new Set();
409
+ const unsupported = [];
410
+ const isTest = root.includes("/mock/root");
411
+ for (const [metaPkg, framework] of Object.entries(META_FRAMEWORK_MAP)) {
412
+ if (metaPkg in allDeps || !isTest && isPackageResolvable2(metaPkg, root)) {
413
+ supportedSet.add(framework);
414
+ }
415
+ }
285
416
  for (const { framework, deps } of SUPPORTED_FRAMEWORKS) {
286
- if (deps.some((dep) => dep in allDeps)) {
287
- supported.push(framework);
417
+ if (supportedSet.has(framework)) continue;
418
+ for (const dep of deps) {
419
+ if (dep in allDeps || !isTest && isPackageResolvable2(dep, root)) {
420
+ supportedSet.add(framework);
421
+ break;
422
+ }
288
423
  }
289
424
  }
290
- const unsupported = [];
291
425
  for (const fw of UNSUPPORTED_FRAMEWORKS) {
292
- if (fw.dep in allDeps) {
426
+ if (fw.dep in allDeps || !isTest && isPackageResolvable2(fw.dep, root)) {
293
427
  unsupported.push(fw);
294
428
  }
295
429
  }
296
- return { supported, unsupported };
430
+ return {
431
+ supported: Array.from(supportedSet),
432
+ unsupported
433
+ };
297
434
  }
298
435
 
299
436
  // src/detect/ide.ts
@@ -302,39 +439,44 @@ var SUPPORTED_IDE = "vscode";
302
439
  async function detectIDE(root) {
303
440
  const detected = /* @__PURE__ */ new Map();
304
441
  if (process.env.CURSOR_TRACE_DIR || process.env.CURSOR_CHANNEL) {
305
- detected.set("Cursor", { ide: "Cursor", supported: false });
442
+ detected.set("Cursor", { ide: "cursor", supported: true });
306
443
  }
307
444
  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 });
445
+ detected.set("Trae", { ide: "trae", supported: true });
309
446
  }
310
447
  if (process.env.ZED_TERM) {
311
448
  detected.set("Zed", { ide: "Zed", supported: false });
312
449
  }
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
- }
450
+ 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")) {
451
+ detected.set("Windsurf", { ide: "Windsurf", supported: false });
317
452
  }
318
- if (await exists(path5.join(root, ".trae"))) {
319
- detected.set("Trae", { ide: "Trae", supported: false });
453
+ const [hasTrae, hasCursor, hasVscode, hasIdea] = await Promise.all([
454
+ exists(path5.join(root, ".trae")),
455
+ exists(path5.join(root, ".cursor")),
456
+ exists(path5.join(root, ".vscode")),
457
+ exists(path5.join(root, ".idea"))
458
+ ]);
459
+ if (hasTrae && !detected.has("Trae")) {
460
+ detected.set("Trae", { ide: "trae", supported: true });
320
461
  }
321
- if (await exists(path5.join(root, ".cursor"))) {
322
- detected.set("Cursor", { ide: "Cursor", supported: false });
462
+ if (hasCursor && !detected.has("Cursor")) {
463
+ detected.set("Cursor", { ide: "cursor", supported: true });
323
464
  }
324
- if (await exists(path5.join(root, ".vscode"))) {
325
- if (!detected.has("vscode")) {
326
- detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
327
- }
465
+ if (hasVscode && !detected.has("vscode")) {
466
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
328
467
  }
329
- if (await exists(path5.join(root, ".idea"))) {
468
+ if (hasIdea && !detected.has("JetBrains IDE")) {
330
469
  detected.set("JetBrains IDE", { ide: "JetBrains IDE", supported: false });
331
470
  }
471
+ if (detected.size === 0 && process.env.TERM_PROGRAM === "vscode") {
472
+ detected.set("vscode", { ide: SUPPORTED_IDE, supported: true });
473
+ }
332
474
  return {
333
475
  detected: Array.from(detected.values())
334
476
  };
335
477
  }
336
478
 
337
- // src/detect/ai-tool.ts
479
+ // src/detect/provider.ts
338
480
  import path6 from "path";
339
481
  var KNOWN_CLI_TOOLS = [
340
482
  { id: "claude-code", bin: "claude", label: "Claude Code", supported: true },
@@ -344,23 +486,24 @@ var KNOWN_CLI_TOOLS = [
344
486
  ];
345
487
  var KNOWN_IDE_PLUGINS = [
346
488
  { id: "claude-code", extId: "anthropic.claude-code", label: "Claude Code", supported: true },
347
- { id: "github-copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
489
+ { id: "copilot", extId: "github.copilot", label: "GitHub Copilot", supported: true },
348
490
  { id: "codex", extId: "openai.chatgpt", label: "Codex (ChatGPT)", supported: true },
349
491
  { id: "gemini", extId: "google.geminicodeassist", label: "Gemini Code Assist", supported: true }
350
492
  ];
351
- async function detectAITools(root) {
493
+ async function detectProviders(root) {
352
494
  const detectedMap = /* @__PURE__ */ new Map();
353
- for (const tool of KNOWN_CLI_TOOLS) {
495
+ const cliChecks = KNOWN_CLI_TOOLS.map(async (tool) => {
354
496
  if (await which(tool.bin)) {
355
497
  detectedMap.set(tool.id, {
356
498
  id: tool.id,
357
499
  label: tool.label,
358
500
  supported: tool.supported,
359
- toolModes: ["cli"],
501
+ providerModes: ["cli"],
360
502
  preferredMode: "cli"
361
503
  });
362
504
  }
363
- }
505
+ });
506
+ await Promise.all(cliChecks);
364
507
  const extensionsJsonPath = path6.join(root, ".vscode", "extensions.json");
365
508
  let recommendedExts = [];
366
509
  if (await exists(extensionsJsonPath)) {
@@ -375,35 +518,49 @@ async function detectAITools(root) {
375
518
  const homeDir = process.env.HOME || process.env.USERPROFILE || "";
376
519
  const globalExtDir = path6.join(homeDir, ".vscode", "extensions");
377
520
  const globalExtExists = await exists(globalExtDir);
521
+ let installedExtensionFolders = [];
522
+ if (globalExtExists) {
523
+ try {
524
+ const { readdir } = await import("fs/promises");
525
+ installedExtensionFolders = await readdir(globalExtDir);
526
+ const obsoletePath = path6.join(globalExtDir, ".obsolete");
527
+ if (await exists(obsoletePath)) {
528
+ try {
529
+ const obsoleteData = await readJSON(obsoletePath);
530
+ if (obsoleteData) {
531
+ const obsoleteKeys = Object.keys(obsoleteData);
532
+ installedExtensionFolders = installedExtensionFolders.filter((folder) => {
533
+ return !obsoleteKeys.includes(folder);
534
+ });
535
+ }
536
+ } catch {
537
+ }
538
+ }
539
+ } catch {
540
+ }
541
+ }
378
542
  for (const plugin of KNOWN_IDE_PLUGINS) {
379
543
  let isInstalled = false;
380
544
  if (recommendedExts.includes(plugin.extId.toLowerCase())) {
381
545
  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
- }
546
+ } else if (installedExtensionFolders.some((f) => {
547
+ const lower = f.toLowerCase();
548
+ return lower === plugin.extId.toLowerCase() || lower.startsWith(plugin.extId.toLowerCase() + "-");
549
+ })) {
550
+ isInstalled = true;
394
551
  }
395
552
  if (isInstalled) {
396
553
  const existing = detectedMap.get(plugin.id);
397
554
  if (existing) {
398
- existing.toolModes.push("plugin");
399
- existing.preferredMode = "plugin";
555
+ existing.providerModes.push("extension");
556
+ existing.preferredMode = "extension";
400
557
  } else {
401
558
  detectedMap.set(plugin.id, {
402
559
  id: plugin.id,
403
560
  label: plugin.label,
404
561
  supported: plugin.supported,
405
- toolModes: ["plugin"],
406
- preferredMode: "plugin"
562
+ providerModes: ["extension"],
563
+ preferredMode: "extension"
407
564
  });
408
565
  }
409
566
  }
@@ -413,207 +570,259 @@ async function detectAITools(root) {
413
570
 
414
571
  // src/inject/ast-injector.ts
415
572
  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'`
573
+ import { loadFile, writeFile as writeAstFile } from "magicast";
574
+
575
+ // src/inject/strategies/vite.ts
576
+ import { addVitePlugin } from "magicast/helpers";
577
+ var ViteStrategy = class {
578
+ name = "Vite";
579
+ supports(tool) {
580
+ return tool === "vite";
581
+ }
582
+ inject({ mod, detection }) {
583
+ addVitePlugin(mod, {
584
+ from: "@inspecto-dev/plugin",
585
+ constructor: "vitePlugin"
586
+ });
587
+ }
588
+ getManualInstructions(detection, reason) {
589
+ return [
590
+ `import { vitePlugin as inspecto } from '@inspecto-dev/plugin'`,
591
+ "",
592
+ "// Add to your plugins array:",
593
+ `plugins: [`,
594
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
595
+ ` ...otherPlugins`,
596
+ `].filter(Boolean)`
597
+ ];
598
+ }
423
599
  };
424
- function getImportStatement(tool, isLegacyRspack) {
425
- if (tool === "rspack" && isLegacyRspack) {
426
- return `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin/legacy/rspack'`;
600
+
601
+ // src/inject/strategies/webpack.ts
602
+ var WebpackStrategy = class {
603
+ name = "Webpack";
604
+ supports(tool) {
605
+ return tool === "webpack";
606
+ }
607
+ inject(options) {
608
+ throw new Error("Webpack requires manual plugin configuration");
609
+ }
610
+ getManualInstructions(detection, reason) {
611
+ const importPkg = detection.isLegacyWebpack ? "@inspecto-dev/plugin/legacy/webpack4" : "@inspecto-dev/plugin";
612
+ const pluginName = detection.isLegacyWebpack ? "webpack4Plugin" : "webpackPlugin";
613
+ const pluginCall = detection.isLegacyWebpack ? `process.env.NODE_ENV !== 'production' && inspecto({
614
+ pathType: 'absolute',
615
+ escapeTags: ['Transition', 'AnimatePresence'],
616
+ })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
617
+ return [
618
+ `import { ${pluginName} as inspecto } from '${importPkg}'`,
619
+ "",
620
+ "// Add to your plugins array:",
621
+ `plugins: [`,
622
+ ` ${pluginCall},`,
623
+ ` ...otherPlugins`,
624
+ `].filter(Boolean)`
625
+ ];
427
626
  }
428
- return IMPORT_MAP[tool];
429
- }
430
- function getPluginExpression(isLegacyRspack) {
431
- if (isLegacyRspack) {
432
- return `process.env.NODE_ENV !== 'production' && inspecto({
627
+ };
628
+
629
+ // src/inject/strategies/rspack.ts
630
+ var RspackStrategy = class {
631
+ name = "Rspack";
632
+ supports(tool) {
633
+ return tool === "rspack";
634
+ }
635
+ inject(options) {
636
+ throw new Error("Rspack requires manual plugin configuration");
637
+ }
638
+ getManualInstructions(detection, reason) {
639
+ const importPkg = detection.isLegacyRspack ? "@inspecto-dev/plugin/legacy/rspack" : "@inspecto-dev/plugin";
640
+ const pluginCall = detection.isLegacyRspack ? `process.env.NODE_ENV !== 'production' && inspecto({
433
641
  pathType: 'absolute',
434
642
  escapeTags: ['Transition', 'AnimatePresence'],
435
- }) as any`;
643
+ })` : `process.env.NODE_ENV !== 'production' && inspecto()`;
644
+ return [
645
+ `import { rspackPlugin as inspecto } from '${importPkg}'`,
646
+ "",
647
+ "// Add to your plugins array:",
648
+ `plugins: [`,
649
+ ` ${pluginCall},`,
650
+ ` ...otherPlugins`,
651
+ `].filter(Boolean)`
652
+ ];
436
653
  }
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),
654
+ };
655
+
656
+ // src/inject/strategies/rsbuild.ts
657
+ var RsbuildStrategy = class {
658
+ name = "Rsbuild";
659
+ supports(tool) {
660
+ return tool === "rsbuild";
661
+ }
662
+ inject(options) {
663
+ throw new Error("Rsbuild requires manual plugin configuration due to nested structure");
664
+ }
665
+ getManualInstructions(detection, reason) {
666
+ return [
667
+ `import { rspackPlugin as inspecto } from '@inspecto-dev/plugin'`,
448
668
  "",
449
669
  "// Add to tools.rspack:",
450
670
  `tools: {`,
451
671
  ` rspack: {`,
452
672
  ` plugins: [`,
453
- ` ${getPluginExpression(isLegacyRspack)},`,
673
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
454
674
  ` ]`,
455
675
  ` }`,
456
676
  `}`
457
- ]);
458
- } else {
459
- log.codeBlock([
460
- getImportStatement(tool, isLegacyRspack),
677
+ ];
678
+ }
679
+ };
680
+
681
+ // src/inject/strategies/esbuild.ts
682
+ var EsbuildStrategy = class {
683
+ name = "esbuild";
684
+ supports(tool) {
685
+ return tool === "esbuild";
686
+ }
687
+ inject(options) {
688
+ throw new Error("Esbuild requires manual plugin configuration");
689
+ }
690
+ getManualInstructions(detection, reason) {
691
+ return [
692
+ `1. Update your esbuild config (${detection.configPath}):`,
693
+ `import { esbuildPlugin as inspecto } from '@inspecto-dev/plugin'`,
461
694
  "",
462
695
  "// Add to your plugins array:",
463
696
  `plugins: [`,
464
- ` ${getPluginExpression(isLegacyRspack)},`,
697
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
465
698
  ` ...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
- }
699
+ `].filter(Boolean)`,
700
+ "",
701
+ "2. Initialize the client in your app entry (e.g., main.js / index.js):",
702
+ `import { mountInspector } from '@inspecto-dev/core'`,
703
+ "",
704
+ "// Call this before your app renders",
705
+ `if (process.env.NODE_ENV !== 'production') {`,
706
+ ` mountInspector()`,
707
+ `}`
708
+ ];
491
709
  }
492
- if (lastImportEnd > 0) {
493
- return content.slice(0, lastImportEnd) + "\n" + importStmt + content.slice(lastImportEnd);
710
+ };
711
+
712
+ // src/inject/strategies/rollup.ts
713
+ var RollupStrategy = class {
714
+ name = "Rollup";
715
+ supports(tool) {
716
+ return tool === "rollup";
717
+ }
718
+ inject(options) {
719
+ throw new Error("Rollup requires manual plugin configuration");
720
+ }
721
+ getManualInstructions(detection, reason) {
722
+ return [
723
+ `1. Update your rollup config (${detection.configPath}):`,
724
+ `import { rollupPlugin as inspecto } from '@inspecto-dev/plugin'`,
725
+ "",
726
+ "// Add to your plugins array:",
727
+ `plugins: [`,
728
+ ` process.env.NODE_ENV !== 'production' && inspecto(),`,
729
+ ` ...otherPlugins`,
730
+ `].filter(Boolean)`,
731
+ "",
732
+ "2. Initialize the client in your app entry (e.g., main.js / index.js):",
733
+ `import { mountInspector } from '@inspecto-dev/core'`,
734
+ "",
735
+ "// Call this before your app renders",
736
+ `if (process.env.NODE_ENV !== 'production') {`,
737
+ ` mountInspector()`,
738
+ `}`
739
+ ];
494
740
  }
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;
741
+ };
742
+
743
+ // src/inject/strategies/index.ts
744
+ var STRATEGIES = [
745
+ new ViteStrategy(),
746
+ new WebpackStrategy(),
747
+ new RspackStrategy(),
748
+ new RsbuildStrategy(),
749
+ new EsbuildStrategy(),
750
+ new RollupStrategy()
751
+ ];
752
+
753
+ // src/inject/ast-injector.ts
754
+ function printManualInstructions(strategy, detection, reason) {
755
+ log.warn(`Could not automatically configure ${detection.configPath}`);
756
+ log.hint(`(reason: ${reason})`);
757
+ log.blank();
758
+ log.hint("Please add the following manually:");
759
+ if (strategy) {
760
+ const instructions = strategy.getManualInstructions(detection, reason);
761
+ log.codeBlock(instructions);
762
+ } else {
763
+ log.error(`Unsupported build tool: ${detection.tool}`);
517
764
  }
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
765
  }
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;
766
+ function isAlreadyInjected(content) {
767
+ return /import\s+.*@inspecto-dev\/plugin/.test(content) || /require\(['"]@inspecto-dev\/plugin['"]\)/.test(content) || /import\s+.*ai-dev-inspector/.test(content) || // Legacy support
768
+ /require\(['"]ai-dev-inspector['"]\)/.test(content);
532
769
  }
533
770
  async function injectPlugin(root, detection, dryRun) {
534
771
  const configPath = path7.join(root, detection.configPath);
535
- const backupPath = configPath + ".bak";
536
772
  const mutations = [];
773
+ const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
537
774
  const content = await readFile(configPath);
538
775
  if (!content) {
539
- printManualInstructions(
540
- detection.tool,
541
- detection.configPath,
542
- "config file not readable",
543
- detection.isLegacyRspack
544
- );
776
+ printManualInstructions(strategy, detection, "config file not readable");
545
777
  return { success: false, mutations, failureReason: "config file not readable" };
546
778
  }
547
779
  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);
780
+ log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
567
781
  mutations.push({
568
782
  type: "file_modified",
569
783
  path: detection.configPath,
570
- backup: detection.configPath + ".bak",
571
- description: "Injected inspecto() plugin"
784
+ description: "Previously configured inspecto() plugin"
572
785
  });
786
+ return { success: true, mutations };
573
787
  }
574
- log.success(`Backed up ${detection.configPath} \u2192 ${detection.configPath}.bak`);
575
- const injected = injectIntoPluginsArray(content, detection);
576
- if (!injected) {
788
+ if (!strategy) {
577
789
  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
790
+ strategy,
791
+ detection,
792
+ `No injection strategy found for ${detection.tool}`
582
793
  );
583
- return {
584
- success: false,
585
- mutations,
586
- failureReason: "could not locate plugins array"
587
- };
794
+ return { success: false, mutations, failureReason: "No strategy found" };
588
795
  }
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
- }
796
+ if (dryRun) {
797
+ log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
798
+ return { success: true, mutations: [] };
799
+ }
800
+ try {
801
+ const mod = await loadFile(configPath);
802
+ strategy.inject({
803
+ mod,
804
+ detection
805
+ });
806
+ await writeAstFile(mod, configPath);
807
+ mutations.push({
808
+ type: "file_modified",
809
+ path: detection.configPath,
810
+ description: "Automatically configured inspecto() plugin"
811
+ });
812
+ log.success(`Configured plugin in ${detection.configPath}`);
813
+ return { success: true, mutations };
814
+ } catch (err) {
597
815
  printManualInstructions(
598
- detection.tool,
599
- detection.configPath,
600
- "injection produced unbalanced brackets",
601
- detection.isLegacyRspack
816
+ strategy,
817
+ detection,
818
+ `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`
602
819
  );
603
820
  return {
604
821
  success: false,
605
- mutations: [],
606
- // Rolled back
607
- failureReason: "injection produced invalid syntax"
822
+ mutations,
823
+ failureReason: "Automatic configuration unavailable"
608
824
  };
609
825
  }
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
826
  }
618
827
 
619
828
  // src/inject/gitignore.ts
@@ -630,7 +839,7 @@ async function updateGitignore(root, shared, dryRun) {
630
839
  if (!dryRun) {
631
840
  await writeFile(gitignorePath, content);
632
841
  }
633
- log.success("Updated .gitignore: .inspecto/settings.json is now trackable");
842
+ log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
634
843
  return;
635
844
  }
636
845
  const missingRules = desiredRules.filter((rule) => !content.includes(rule));
@@ -692,41 +901,54 @@ async function tryOpenURI(uri) {
692
901
  return false;
693
902
  }
694
903
  }
695
- async function installExtension(dryRun) {
904
+ async function installExtension(dryRun, ide) {
696
905
  if (dryRun) {
697
906
  log.dryRun("Would attempt to install VS Code extension");
698
907
  return null;
699
908
  }
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 {
909
+ const isVSCode = !ide || ide === "vscode";
910
+ if (isVSCode) {
911
+ if (await which("code")) {
912
+ try {
913
+ await run("code", ["--install-extension", EXTENSION_ID]);
914
+ log.success("VS Code extension installed via CLI");
915
+ return { type: "extension_installed", id: EXTENSION_ID };
916
+ } catch {
917
+ }
706
918
  }
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 {
919
+ const codePath = await findVSCodeBinary();
920
+ if (codePath) {
921
+ try {
922
+ await run(codePath, ["--install-extension", EXTENSION_ID]);
923
+ log.success("VS Code extension installed via binary path");
924
+ log.info(
925
+ 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
926
+ );
927
+ return { type: "extension_installed", id: EXTENSION_ID };
928
+ } catch {
929
+ }
716
930
  }
931
+ const uri = `vscode:extension/${EXTENSION_ID}`;
932
+ if (await tryOpenURI(uri)) {
933
+ log.warn("Opened extension page in VS Code");
934
+ log.hint('Please click "Install" in the opened VS Code window to complete setup.');
935
+ return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
936
+ }
937
+ log.warn("Could not auto-install VS Code extension");
938
+ log.hint("Please install it manually to enable Inspector features:");
939
+ log.hint(" 1. Open VS Code");
940
+ log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
941
+ log.hint(' 3. Search for "Inspecto"');
942
+ log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
943
+ return null;
717
944
  }
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");
945
+ log.warn(`Could not auto-install extension for ${ide}`);
725
946
  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}`);
947
+ log.hint(" 1. Download the latest .vsix file from Inspecto releases");
948
+ log.hint(` 2. Open ${ide}`);
949
+ log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
950
+ log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
951
+ log.hint(" 5. Select the downloaded .vsix file");
730
952
  return null;
731
953
  }
732
954
  async function isExtensionInstalled() {
@@ -746,70 +968,49 @@ async function isExtensionInstalled() {
746
968
  }
747
969
  }
748
970
 
749
- // src/commands/init.ts
971
+ // src/prompts.ts
972
+ import prompts from "prompts";
750
973
  async function promptIDEChoice(detections) {
751
974
  if (!process.stdin.isTTY) {
752
975
  log.warn("Multiple IDEs detected but stdin is not interactive");
753
976
  log.hint(`Using: ${detections[0].ide} (first match)`);
754
977
  return detections[0];
755
978
  }
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);
979
+ const { choice } = await prompts({
980
+ type: "select",
981
+ name: "choice",
982
+ message: "Detected multiple IDEs, please choose one:",
983
+ choices: detections.map((d, i) => ({
984
+ title: `${d.ide} ${d.supported ? "(supported)" : "(unsupported/limited)"}`,
985
+ value: i
986
+ }))
778
987
  });
988
+ if (choice === void 0) return null;
989
+ return detections[choice];
779
990
  }
780
- async function promptAIToolChoice(detections) {
991
+ async function promptProviderChoice(detections) {
781
992
  if (!process.stdin.isTTY) {
782
993
  log.warn("Multiple AI tools detected but stdin is not interactive");
783
994
  log.hint(`Using: ${detections[0].label} (first match)`);
784
995
  return detections[0];
785
996
  }
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);
997
+ const { choice } = await prompts({
998
+ type: "select",
999
+ name: "choice",
1000
+ message: "Detected multiple providers, please choose one:",
1001
+ choices: detections.map((d, i) => {
1002
+ const modeLabels = d.providerModes.map(
1003
+ (mode) => mode === "extension" ? "VS Code Extension" : "Terminal CLI"
1004
+ );
1005
+ const modeStr = modeLabels.join(" & ");
1006
+ return {
1007
+ title: `${d.label} ${d.supported ? `(supported ${modeStr})` : "(unsupported/limited)"}`,
1008
+ value: i
1009
+ };
1010
+ })
812
1011
  });
1012
+ if (choice === void 0) return null;
1013
+ return detections[choice];
813
1014
  }
814
1015
  async function promptConfigChoice(detections) {
815
1016
  if (!process.stdin.isTTY) {
@@ -817,30 +1018,93 @@ async function promptConfigChoice(detections) {
817
1018
  log.hint(`Using: ${detections[0].label} (first match)`);
818
1019
  return detections[0];
819
1020
  }
820
- console.log();
821
- console.log(" ? Detected multiple build tool configs:");
822
- detections.forEach((d, i) => {
823
- console.log(` ${i + 1}. ${d.label}`);
1021
+ const choices = detections.map((d, i) => ({
1022
+ title: d.label,
1023
+ value: i
1024
+ }));
1025
+ choices.push({
1026
+ title: "Skip (I'll configure manually)",
1027
+ value: -1
824
1028
  });
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);
1029
+ const { choice } = await prompts({
1030
+ type: "select",
1031
+ name: "choice",
1032
+ message: "Detected multiple build tool configs, please choose one to inject:",
1033
+ choices
842
1034
  });
1035
+ if (choice === void 0 || choice === -1) return null;
1036
+ return detections[choice];
1037
+ }
1038
+ async function promptUnsupportedFrameworkContinue() {
1039
+ if (!process.stdin.isTTY) {
1040
+ log.error("Unsupported framework detected in non-interactive environment.");
1041
+ log.hint("Use --force to skip this check and continue anyway.");
1042
+ return false;
1043
+ }
1044
+ const { confirm } = await prompts({
1045
+ type: "confirm",
1046
+ name: "confirm",
1047
+ message: "Inspecto may not work properly. Do you want to continue anyway?",
1048
+ initial: false
1049
+ });
1050
+ return !!confirm;
1051
+ }
1052
+
1053
+ // src/instructions.ts
1054
+ function printNuxtManualInstructions() {
1055
+ log.blank();
1056
+ log.hint("To enable Inspecto in Nuxt, update your nuxt.config.ts:");
1057
+ console.log(`\x1B[36m
1058
+ import { vitePlugin as inspecto } from '@inspecto-dev/plugin'
1059
+ export default defineNuxtConfig({
1060
+ vite: {
1061
+ plugins: [inspecto()]
1062
+ }
1063
+ })
1064
+ \x1B[0m`);
1065
+ log.hint("And create a Nuxt plugin at plugins/inspecto.client.ts:");
1066
+ console.log(`\x1B[36m
1067
+ export default defineNuxtPlugin(() => {
1068
+ if (import.meta.dev) {
1069
+ import('@inspecto-dev/core').then(({ mountInspector }) => {
1070
+ mountInspector()
1071
+ })
1072
+ }
1073
+ })
1074
+ \x1B[0m`);
1075
+ }
1076
+ function printNextJsManualInstructions() {
1077
+ log.blank();
1078
+ log.hint("To enable Inspecto in Next.js, update your next.config.mjs:");
1079
+ console.log(`\x1B[36m
1080
+ import { webpackPlugin as inspecto } from '@inspecto-dev/plugin'
1081
+ const nextConfig = {
1082
+ webpack: (config, { dev, isServer }) => {
1083
+ if (dev && !isServer) config.plugins.push(inspecto())
1084
+ return config
1085
+ }
1086
+ }
1087
+ export default nextConfig
1088
+ \x1B[0m`);
1089
+ log.hint("And initialize the client dynamically in your app/layout.tsx (or pages/_app.tsx):");
1090
+ console.log(`\x1B[36m
1091
+ 'use client'
1092
+ import { useEffect } from 'react'
1093
+
1094
+ export default function RootLayout({ children }) {
1095
+ useEffect(() => {
1096
+ if (process.env.NODE_ENV !== 'production') {
1097
+ import('@inspecto-dev/core').then(({ mountInspector }) => {
1098
+ mountInspector({ serverUrl: 'http://127.0.0.1:5678' })
1099
+ })
1100
+ }
1101
+ }, [])
1102
+ return <html><body>{children}</body></html>
1103
+ }
1104
+ \x1B[0m`);
843
1105
  }
1106
+
1107
+ // src/commands/init.ts
844
1108
  async function init(options) {
845
1109
  const root = process.cwd();
846
1110
  const mutations = [];
@@ -850,37 +1114,60 @@ async function init(options) {
850
1114
  log.hint("Run this command from your project root");
851
1115
  return;
852
1116
  }
853
- const pm = await detectPackageManager(root);
1117
+ const [pm, frameworkResult, buildResult, ideProbe, providerProbe] = await Promise.all([
1118
+ detectPackageManager(root),
1119
+ detectFrameworks(root),
1120
+ detectBuildTools(root),
1121
+ detectIDE(root),
1122
+ detectProviders(root)
1123
+ ]);
854
1124
  log.success(`Detected package manager: ${pm}`);
855
- const frameworkResult = await detectFrameworks(root);
856
1125
  if (frameworkResult.supported.length > 0) {
857
1126
  log.success(`Detected framework: ${frameworkResult.supported.join(", ")}`);
858
1127
  }
859
- if (frameworkResult.unsupported.length > 0) {
1128
+ const isSupported = frameworkResult.supported.length > 0;
1129
+ const hasUnsupported = frameworkResult.unsupported.length > 0;
1130
+ if (!isSupported) {
1131
+ if (hasUnsupported) {
1132
+ const names = frameworkResult.unsupported.map((f) => f.name).join(", ");
1133
+ log.warn(`Detected ${names} \u2014 not supported in v1 (React / Vue only)`);
1134
+ } else {
1135
+ log.warn("No frontend framework detected");
1136
+ log.hint("Inspecto current version supports React and Vue projects");
1137
+ }
1138
+ if (!options.force) {
1139
+ const shouldContinue = await promptUnsupportedFrameworkContinue();
1140
+ if (!shouldContinue) {
1141
+ log.warn("Initialization aborted.");
1142
+ return;
1143
+ }
1144
+ } else {
1145
+ log.warn("Continuing anyway (--force)");
1146
+ }
1147
+ } else if (hasUnsupported) {
860
1148
  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");
1149
+ log.hint(
1150
+ `Note: Inspecto will be configured for ${frameworkResult.supported.join(", ")}. Other detected frameworks (${names}) will be ignored.`
1151
+ );
867
1152
  }
868
- const buildResult = await detectBuildTools(root);
1153
+ let manualConfigRequiredFor = "";
869
1154
  if (buildResult.supported.length > 0) {
870
1155
  buildResult.supported.forEach((bt) => log.success(`Detected: ${bt.label}`));
871
1156
  }
872
1157
  if (buildResult.unsupported.length > 0) {
873
1158
  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");
1159
+ manualConfigRequiredFor = buildResult.unsupported[0] || "";
1160
+ log.warn(`Detected ${names} \u2014 automatic plugin injection is not supported in current version`);
1161
+ log.hint("You can still manually configure it by modifying your configuration file");
877
1162
  }
878
1163
  if (buildResult.supported.length === 0 && buildResult.unsupported.length === 0) {
879
1164
  log.warn("No recognized build tool detected");
880
- log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
1165
+ log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
881
1166
  log.hint("Dependency will be installed but plugin injection will be skipped");
1167
+ log.hint(
1168
+ "Please refer to the manual setup guide: https://inspecto-dev.github.io/inspecto/guide/manual-installation"
1169
+ );
882
1170
  }
883
- const ideProbe = await detectIDE(root);
884
1171
  let selectedIDE = null;
885
1172
  if (ideProbe.detected.length === 0) {
886
1173
  log.error("No IDE detected in current project");
@@ -901,21 +1188,20 @@ async function init(options) {
901
1188
  log.hint(`Features may be severely limited or unavailable in ${selectedIDE.ide}.`);
902
1189
  }
903
1190
  }
904
- const aiToolProbe = await detectAITools(root);
905
- let selectedAITool = null;
906
- if (!options.prefer) {
907
- if (aiToolProbe.detected.length === 0) {
1191
+ let selectedProvider = null;
1192
+ if (!options.provider) {
1193
+ if (providerProbe.detected.length === 0) {
908
1194
  log.warn("No supported AI tools detected");
909
1195
  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}`);
1196
+ } else if (providerProbe.detected.length === 1) {
1197
+ selectedProvider = providerProbe.detected[0];
1198
+ if (selectedProvider.supported) {
1199
+ log.success(`Detected AI tool: ${selectedProvider.label}`);
914
1200
  }
915
1201
  } else {
916
- selectedAITool = await promptAIToolChoice(aiToolProbe.detected);
917
- if (selectedAITool) {
918
- log.success(`Selected AI tool: ${selectedAITool.label}`);
1202
+ selectedProvider = await promptProviderChoice(providerProbe.detected);
1203
+ if (selectedProvider) {
1204
+ log.success(`Selected provider: ${selectedProvider.label}`);
919
1205
  }
920
1206
  }
921
1207
  }
@@ -933,16 +1219,8 @@ async function init(options) {
933
1219
  throw new Error(result.stderr);
934
1220
  }
935
1221
  log.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
936
- mutations.push({
937
- type: "dependency_added",
938
- name: "@inspecto-dev/plugin",
939
- dev: true
940
- });
941
- mutations.push({
942
- type: "dependency_added",
943
- name: "@inspecto-dev/core",
944
- dev: true
945
- });
1222
+ mutations.push({ type: "dependency_added", name: "@inspecto-dev/plugin", dev: true });
1223
+ mutations.push({ type: "dependency_added", name: "@inspecto-dev/core", dev: true });
946
1224
  } catch (err) {
947
1225
  installFailed = true;
948
1226
  log.error(`Failed to install dependency: ${err?.message || "Unknown error"}`);
@@ -969,55 +1247,50 @@ async function init(options) {
969
1247
  }
970
1248
  }
971
1249
  const settingsDir = path9.join(root, ".inspecto");
972
- const settingsPath = path9.join(settingsDir, "settings.json");
973
- const promptsPath = path9.join(settingsDir, "prompts.json");
1250
+ const settingsFileName = options.shared ? "settings.json" : "settings.local.json";
1251
+ const promptsFileName = options.shared ? "prompts.json" : "prompts.local.json";
1252
+ const settingsPath = path9.join(settingsDir, settingsFileName);
1253
+ const promptsPath = path9.join(settingsDir, promptsFileName);
974
1254
  if (await exists(settingsPath)) {
975
1255
  const existingSettings = await readJSON(settingsPath);
976
1256
  if (existingSettings === null) {
977
- log.warn(".inspecto/settings.json exists but contains invalid JSON");
1257
+ log.warn(`.inspecto/${settingsFileName} exists but contains invalid JSON`);
978
1258
  log.hint("Please fix the syntax errors manually, or delete it and re-run init");
979
1259
  } else {
980
- log.success(".inspecto/settings.json already exists (skipped)");
1260
+ log.success(`.inspecto/${settingsFileName} already exists (skipped)`);
981
1261
  }
982
1262
  } else {
983
1263
  const defaultSettings = {};
984
1264
  if (selectedIDE && selectedIDE.supported) {
985
- defaultSettings.ide = selectedIDE.ide === "vscode" ? "vscode" : selectedIDE.ide;
1265
+ defaultSettings.ide = selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : selectedIDE.ide.toLowerCase();
986
1266
  }
987
- if (options.prefer) {
988
- defaultSettings.prefer = options.prefer;
989
- } else if (selectedAITool) {
990
- defaultSettings.prefer = selectedAITool.id;
991
- if (selectedAITool.preferredMode) {
992
- defaultSettings.providers = {
993
- [selectedAITool.id]: {
994
- type: selectedAITool.preferredMode
995
- }
996
- };
997
- }
1267
+ if (options.provider) {
1268
+ const tool = options.provider;
1269
+ const mode = tool === "coco" ? "cli" : "extension";
1270
+ defaultSettings["provider.default"] = `${tool}.${mode}`;
1271
+ } else if (selectedProvider) {
1272
+ const toolId = selectedProvider.id;
1273
+ const mode = selectedProvider.preferredMode === "cli" ? "cli" : "extension";
1274
+ defaultSettings["provider.default"] = `${toolId}.${mode}`;
998
1275
  }
999
1276
  if (options.dryRun) {
1000
- log.dryRun("Would create .inspecto/settings.json");
1277
+ log.dryRun(`Would create .inspecto/${settingsFileName}`);
1001
1278
  } else {
1002
1279
  await writeJSON(settingsPath, defaultSettings);
1003
- log.success("Created .inspecto/settings.json");
1004
- mutations.push({ type: "file_created", path: ".inspecto/settings.json" });
1280
+ log.success(`Created .inspecto/${settingsFileName}`);
1281
+ mutations.push({ type: "file_created", path: `.inspecto/${settingsFileName}` });
1005
1282
  }
1006
1283
  }
1007
1284
  if (await exists(promptsPath)) {
1008
- log.success(".inspecto/prompts.json already exists (skipped)");
1285
+ log.success(`.inspecto/${promptsFileName} already exists (skipped)`);
1009
1286
  } else {
1010
- const defaultPrompts = [
1011
- { id: "code-review", enabled: false },
1012
- { id: "generate-test", enabled: false },
1013
- { id: "performance", enabled: false }
1014
- ];
1287
+ const defaultPrompts = [];
1015
1288
  if (options.dryRun) {
1016
- log.dryRun("Would create .inspecto/prompts.json");
1289
+ log.dryRun(`Would create .inspecto/${promptsFileName}`);
1017
1290
  } else {
1018
1291
  await writeJSON(promptsPath, defaultPrompts);
1019
- log.success("Created .inspecto/prompts.json (disabling low-frequency intents)");
1020
- mutations.push({ type: "file_created", path: ".inspecto/prompts.json" });
1292
+ log.success(`Created .inspecto/${promptsFileName}`);
1293
+ mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
1021
1294
  }
1022
1295
  }
1023
1296
  if (!options.dryRun) {
@@ -1041,10 +1314,10 @@ async function init(options) {
1041
1314
  const shouldInstallExt = !options.noExtension && (!selectedIDE || selectedIDE && selectedIDE.supported);
1042
1315
  let manualExtensionInstallNeeded = false;
1043
1316
  if (options.noExtension) {
1044
- log.warn("Skipping VS Code extension (--no-extension)");
1317
+ log.warn("Skipping IDE extension (--no-extension)");
1045
1318
  } else if (!shouldInstallExt) {
1046
1319
  } else {
1047
- const extMutation = await installExtension(options.dryRun);
1320
+ const extMutation = await installExtension(options.dryRun, selectedIDE?.ide);
1048
1321
  if (extMutation && !options.dryRun) {
1049
1322
  mutations.push(extMutation);
1050
1323
  if (extMutation.manual_action_required) {
@@ -1063,10 +1336,16 @@ async function init(options) {
1063
1336
  if (options.dryRun) {
1064
1337
  log.blank();
1065
1338
  log.warn("Dry run complete. No files were modified.");
1066
- } else if (installFailed || injectionFailed || manualExtensionInstallNeeded) {
1339
+ } else if (installFailed || injectionFailed || manualExtensionInstallNeeded || manualConfigRequiredFor) {
1067
1340
  log.blank();
1068
1341
  log.warn("Setup completed with some manual steps required.");
1069
- log.hint("Please check the logs above and complete the manual steps.");
1342
+ if (manualConfigRequiredFor === "Nuxt") {
1343
+ printNuxtManualInstructions();
1344
+ } else if (manualConfigRequiredFor === "Next.js") {
1345
+ printNextJsManualInstructions();
1346
+ } else {
1347
+ log.hint("Please check the logs above and complete the manual steps.");
1348
+ }
1070
1349
  log.blank();
1071
1350
  } else {
1072
1351
  log.ready("Ready! Hold Alt + Click any element to inspect.");
@@ -1084,7 +1363,14 @@ async function doctor() {
1084
1363
  log.hint("Run this command from your project root");
1085
1364
  return;
1086
1365
  }
1087
- const ideProbe = await detectIDE(root);
1366
+ const [ideProbe, frameworkResult, providerProbe, pm, buildResult, extInstalled] = await Promise.all([
1367
+ detectIDE(root),
1368
+ detectFrameworks(root),
1369
+ detectProviders(root),
1370
+ detectPackageManager(root),
1371
+ detectBuildTools(root),
1372
+ isExtensionInstalled()
1373
+ ]);
1088
1374
  if (ideProbe.detected.length === 0) {
1089
1375
  log.warn("IDE: not detected");
1090
1376
  result.warnings++;
@@ -1096,11 +1382,10 @@ async function doctor() {
1096
1382
  );
1097
1383
  } else {
1098
1384
  const names = ideProbe.detected.map((d) => d.ide).join(", ");
1099
- log.warn(`IDE: ${names} (not supported in v1, VS Code only)`);
1385
+ log.warn(`IDE: ${names} (not supported in v1, VS Code, Cursor, Trae only)`);
1100
1386
  result.warnings++;
1101
1387
  }
1102
1388
  }
1103
- const frameworkResult = await detectFrameworks(root);
1104
1389
  if (frameworkResult.supported.length > 0) {
1105
1390
  log.success(`Framework: ${frameworkResult.supported.join(", ")}`);
1106
1391
  } else if (frameworkResult.unsupported.length > 0) {
@@ -1111,57 +1396,54 @@ async function doctor() {
1111
1396
  log.warn("Framework: not detected (React / Vue expected)");
1112
1397
  result.warnings++;
1113
1398
  }
1114
- const aiProbe = await detectAITools(root);
1115
- if (aiProbe.detected.length === 0) {
1116
- log.warn("AI Tool: none detected");
1399
+ if (providerProbe.detected.length === 0) {
1400
+ log.warn("Provider: none detected");
1117
1401
  log.hint("Inspecto works best with Claude Code, Trae CLI, or GitHub Copilot");
1118
1402
  result.warnings++;
1119
1403
  } else {
1120
- const aiNames = aiProbe.detected.map((d) => {
1121
- const modeLabels = d.toolModes.map(
1122
- (mode) => mode === "plugin" ? "VS Code Extension" : "Terminal CLI"
1404
+ const aiNames = providerProbe.detected.map((d) => {
1405
+ const modeLabels = d.providerModes.map(
1406
+ (mode) => mode === "extension" ? "VS Code Extension" : "Terminal CLI"
1123
1407
  );
1124
1408
  return `${d.label} (${modeLabels.join(" & ")})`;
1125
1409
  }).join(", ");
1126
- log.success(`AI Tool: ${aiNames}`);
1410
+ log.success(`Provider: ${aiNames}`);
1127
1411
  }
1128
- const pluginPath = path10.join(root, "node_modules", "@inspecto", "plugin");
1412
+ const pluginPath = path10.join(root, "node_modules", "@inspecto-dev", "plugin");
1129
1413
  if (await exists(pluginPath)) {
1130
1414
  const pkgJson = await readJSON(path10.join(pluginPath, "package.json"));
1131
1415
  const version = pkgJson?.version ?? "unknown";
1132
1416
  log.success(`@inspecto-dev/plugin@${version} installed`);
1133
1417
  } else {
1134
1418
  log.error("@inspecto-dev/plugin not installed");
1135
- const pm = await detectPackageManager(root);
1136
- log.hint(`Fix: ${getInstallCommand(pm, "@inspecto-dev/plugin")}`);
1419
+ const pm2 = await detectPackageManager(root);
1420
+ log.hint(`Fix: ${getInstallCommand(pm2, "@inspecto-dev/plugin")}`);
1137
1421
  result.errors++;
1138
1422
  }
1139
- const buildResult = await detectBuildTools(root);
1140
1423
  if (buildResult.supported.length > 0) {
1141
1424
  let injected = false;
1142
1425
  for (const bt of buildResult.supported) {
1143
1426
  const content = await readFile(path10.join(root, bt.configPath));
1144
1427
  if (content && content.includes("@inspecto-dev/plugin")) {
1145
- log.success(`Plugin injected in ${bt.configPath}`);
1428
+ log.success(`Plugin configured in ${bt.configPath}`);
1146
1429
  injected = true;
1147
1430
  break;
1148
1431
  }
1149
1432
  }
1150
1433
  if (!injected) {
1151
- log.error("Plugin not injected in any build config");
1434
+ log.error("Plugin not configured in any build config");
1152
1435
  log.hint("Fix: npx @inspecto-dev/cli init");
1153
1436
  result.errors++;
1154
1437
  }
1155
1438
  } else if (buildResult.unsupported.length > 0) {
1156
1439
  const names = buildResult.unsupported.join(", ");
1157
1440
  log.warn(`Build tool: ${names} (not supported in v1)`);
1158
- log.hint("v1 supports: Vite, Webpack, Rspack, esbuild, Rollup");
1441
+ log.hint("current version supports: Vite, Webpack, Rspack, esbuild, Rollup");
1159
1442
  result.warnings++;
1160
1443
  } else {
1161
1444
  log.warn("No recognized build config found");
1162
1445
  result.warnings++;
1163
1446
  }
1164
- const extInstalled = await isExtensionInstalled();
1165
1447
  if (extInstalled) {
1166
1448
  log.success("VS Code extension detected");
1167
1449
  } else {
@@ -1175,20 +1457,25 @@ async function doctor() {
1175
1457
  result.errors++;
1176
1458
  }
1177
1459
  }
1178
- const settingsPath = path10.join(root, ".inspecto", "settings.json");
1179
- if (await exists(settingsPath)) {
1180
- const settings = await readJSON(settingsPath);
1460
+ const settingsJsonPath = path10.join(root, ".inspecto", "settings.json");
1461
+ const settingsLocalPath = path10.join(root, ".inspecto", "settings.local.json");
1462
+ const hasSettingsJson = await exists(settingsJsonPath);
1463
+ const hasSettingsLocal = await exists(settingsLocalPath);
1464
+ if (hasSettingsJson || hasSettingsLocal) {
1465
+ const targetPath = hasSettingsLocal ? settingsLocalPath : settingsJsonPath;
1466
+ const fileName = hasSettingsLocal ? "settings.local.json" : "settings.json";
1467
+ const settings = await readJSON(targetPath);
1181
1468
  if (settings) {
1182
- log.success(".inspecto/settings.json valid");
1469
+ log.success(`.inspecto/${fileName} valid`);
1183
1470
  } else {
1184
- log.error(".inspecto/settings.json has invalid JSON");
1471
+ log.error(`.inspecto/${fileName} has invalid JSON`);
1185
1472
  log.hint(
1186
1473
  "Fix: Manually correct the syntax errors, or delete the file and re-run npx @inspecto-dev/cli init"
1187
1474
  );
1188
1475
  result.errors++;
1189
1476
  }
1190
1477
  } else {
1191
- log.warn(".inspecto/settings.json not found (using defaults)");
1478
+ log.warn("No .inspecto/settings.json or settings.local.json found (using defaults)");
1192
1479
  log.hint("Optional: npx @inspecto-dev/cli init");
1193
1480
  result.warnings++;
1194
1481
  }
@@ -1239,7 +1526,7 @@ async function teardown() {
1239
1526
  }
1240
1527
  await cleanGitignore(root);
1241
1528
  log.success("Cleaned .gitignore entries");
1242
- log.warn("Cannot restore build config (no backup reference)");
1529
+ log.warn("Cannot restore build config auto-magically");
1243
1530
  log.hint("Please manually remove the inspecto() plugin from your build config");
1244
1531
  log.blank();
1245
1532
  return;
@@ -1249,24 +1536,12 @@ async function teardown() {
1249
1536
  for (const mutation of lock.mutations) {
1250
1537
  switch (mutation.type) {
1251
1538
  case "file_modified": {
1252
- if (mutation.backup && mutation.path) {
1253
- const backupPath = path11.join(root, mutation.backup);
1254
- const targetPath = path11.join(root, mutation.path);
1255
- if (mutation.path === ".gitignore") {
1256
- await cleanGitignore(root);
1257
- log.success("Cleaned .gitignore entries");
1258
- await removeFile(backupPath);
1259
- } else if (await exists(backupPath)) {
1260
- await copyFile(backupPath, targetPath);
1261
- await removeFile(backupPath);
1262
- log.success(`Restored ${mutation.path} from backup`);
1263
- } else {
1264
- log.warn(`Backup not found: ${mutation.backup}`);
1265
- log.hint(`Please manually remove inspecto from ${mutation.path}`);
1266
- }
1267
- } else if (mutation.path && mutation.path !== ".gitignore") {
1268
- log.warn(`Cannot auto-restore ${mutation.path} (no backup recorded)`);
1269
- log.hint(`Please manually remove the inspecto() plugin from ${mutation.path}`);
1539
+ if (!mutation.path) break;
1540
+ if (mutation.path === ".gitignore") {
1541
+ log.success("Cleaned .gitignore entries");
1542
+ } else if (mutation.path) {
1543
+ log.warn(`Cannot auto-restore ${mutation.path}`);
1544
+ log.hint(`Please manually remove the inspecto plugin from ${mutation.path}`);
1270
1545
  }
1271
1546
  break;
1272
1547
  }