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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -369,7 +369,8 @@ var STRATEGIES = [
369
369
  ];
370
370
 
371
371
  // src/inject/ast-injector.ts
372
- function printManualInstructions(strategy, detection, reason) {
372
+ function printManualInstructions(strategy, detection, reason, quiet = false) {
373
+ if (quiet) return;
373
374
  log.warn(`Could not automatically configure ${detection.configPath}`);
374
375
  log.hint(`(reason: ${reason})`);
375
376
  log.blank();
@@ -398,17 +399,19 @@ function isAlreadyInjected(content) {
398
399
  }
399
400
  return false;
400
401
  }
401
- async function injectPlugin(root, detection, dryRun) {
402
+ async function injectPlugin(root, detection, dryRun, quiet = false) {
402
403
  const configPath = path3.join(root, detection.configPath);
403
404
  const mutations = [];
404
405
  const strategy = STRATEGIES.find((s) => s.supports(detection.tool));
405
406
  const content = await readFile(configPath);
406
407
  if (!content) {
407
- printManualInstructions(strategy, detection, "config file not readable");
408
+ printManualInstructions(strategy, detection, "config file not readable", quiet);
408
409
  return { success: false, mutations, failureReason: "config file not readable" };
409
410
  }
410
411
  if (isAlreadyInjected(content)) {
411
- log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
412
+ if (!quiet) {
413
+ log.success(`Plugin already configured in ${detection.configPath} (skipped)`);
414
+ }
412
415
  mutations.push({
413
416
  type: "file_modified",
414
417
  path: detection.configPath,
@@ -420,12 +423,15 @@ async function injectPlugin(root, detection, dryRun) {
420
423
  printManualInstructions(
421
424
  strategy,
422
425
  detection,
423
- `No injection strategy found for ${detection.tool}`
426
+ `No injection strategy found for ${detection.tool}`,
427
+ quiet
424
428
  );
425
429
  return { success: false, mutations, failureReason: "No strategy found" };
426
430
  }
427
431
  if (dryRun) {
428
- log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
432
+ if (!quiet) {
433
+ log.dryRun(`Would automatically configure plugin in ${detection.configPath}`);
434
+ }
429
435
  return { success: true, mutations: [] };
430
436
  }
431
437
  try {
@@ -440,13 +446,16 @@ async function injectPlugin(root, detection, dryRun) {
440
446
  path: detection.configPath,
441
447
  description: "Automatically configured inspecto() plugin"
442
448
  });
443
- log.success(`Configured plugin in ${detection.configPath}`);
449
+ if (!quiet) {
450
+ log.success(`Configured plugin in ${detection.configPath}`);
451
+ }
444
452
  return { success: true, mutations };
445
453
  } catch (err) {
446
454
  printManualInstructions(
447
455
  strategy,
448
456
  detection,
449
- `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`
457
+ `Automatic configuration unavailable: ${err instanceof Error ? err.message : String(err)}`,
458
+ quiet
450
459
  );
451
460
  return {
452
461
  success: false,
@@ -529,9 +538,11 @@ async function tryOpenURI(uri) {
529
538
  return false;
530
539
  }
531
540
  }
532
- async function installExtension(dryRun, ide) {
541
+ async function installExtension(dryRun, ide, quiet = false) {
533
542
  if (dryRun) {
534
- log.dryRun("Would attempt to install VS Code extension");
543
+ if (!quiet) {
544
+ log.dryRun("Would attempt to install VS Code extension");
545
+ }
535
546
  return null;
536
547
  }
537
548
  const isVSCode = !ide || ide === "vscode";
@@ -539,7 +550,9 @@ async function installExtension(dryRun, ide) {
539
550
  if (await which("code")) {
540
551
  try {
541
552
  await run("code", ["--install-extension", EXTENSION_ID]);
542
- log.success("VS Code extension installed via CLI");
553
+ if (!quiet) {
554
+ log.success("VS Code extension installed via CLI");
555
+ }
543
556
  return { type: "extension_installed", id: EXTENSION_ID };
544
557
  } catch {
545
558
  }
@@ -548,35 +561,45 @@ async function installExtension(dryRun, ide) {
548
561
  if (codePath) {
549
562
  try {
550
563
  await run(codePath, ["--install-extension", EXTENSION_ID]);
551
- log.success("VS Code extension installed via binary path");
552
- log.info(
553
- 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
554
- );
564
+ if (!quiet) {
565
+ log.success("VS Code extension installed via binary path");
566
+ log.info(
567
+ 'Tip: Add "code" to your PATH to help Inspecto detect other AI tools in the future'
568
+ );
569
+ }
555
570
  return { type: "extension_installed", id: EXTENSION_ID };
556
571
  } catch {
557
572
  }
558
573
  }
559
574
  const uri = `vscode:extension/${EXTENSION_ID}`;
560
575
  if (await tryOpenURI(uri)) {
561
- log.warn("Opened extension page in VS Code");
562
- log.hint('Please click "Install" in the opened VS Code window to complete setup.');
576
+ if (!quiet) {
577
+ log.warn("Opened extension page in VS Code");
578
+ log.hint('Please click "Install" in the opened VS Code window to complete setup.');
579
+ }
563
580
  return { type: "extension_installed", id: EXTENSION_ID, manual_action_required: true };
564
581
  }
565
- log.warn("Could not auto-install VS Code extension");
566
- log.hint("Please install it manually to enable Inspector features:");
567
- log.hint(" 1. Open VS Code");
568
- log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
569
- log.hint(' 3. Search for "Inspecto"');
570
- log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
582
+ if (!quiet) {
583
+ log.warn("Could not auto-install VS Code extension");
584
+ log.hint("Please install it manually to enable Inspector features:");
585
+ log.hint(" 1. Open VS Code");
586
+ log.hint(" 2. Press Ctrl+Shift+X (or Cmd+Shift+X)");
587
+ log.hint(' 3. Search for "Inspecto"');
588
+ log.hint(` Or visit: https://marketplace.visualstudio.com/items?itemName=${EXTENSION_ID}`);
589
+ }
571
590
  return null;
572
591
  }
573
- log.warn(`Could not auto-install extension for ${ide}`);
574
- log.hint("Please install it manually to enable Inspector features:");
575
- log.hint(" 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)");
576
- log.hint(` 2. Open ${ide}`);
577
- log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
578
- log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
579
- log.hint(" 5. Select the downloaded .vsix file");
592
+ if (!quiet) {
593
+ log.warn(`Could not auto-install extension for ${ide}`);
594
+ log.hint("Please install it manually to enable Inspector features:");
595
+ log.hint(
596
+ " 1. Download the latest .vsix file (Open VSX: https://open-vsx.org/extension/inspecto/inspecto)"
597
+ );
598
+ log.hint(` 2. Open ${ide}`);
599
+ log.hint(" 3. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)");
600
+ log.hint(' 4. Type and select "Extensions: Install from VSIX..."');
601
+ log.hint(" 5. Select the downloaded .vsix file");
602
+ }
580
603
  return null;
581
604
  }
582
605
  async function isExtensionInstalled() {
@@ -600,7 +623,7 @@ async function isExtensionInstalled() {
600
623
  import path4 from "path";
601
624
  var DEFAULT_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
602
625
  var SHARED_RULES = [".inspecto/install.lock", ".inspecto/cache.json", ".inspecto/*.local.json"];
603
- async function updateGitignore(root, shared, dryRun) {
626
+ async function updateGitignore(root, shared, dryRun, quiet = false) {
604
627
  const gitignorePath = path4.join(root, ".gitignore");
605
628
  let content = await readFile(gitignorePath) ?? "";
606
629
  const desiredRules = shared ? SHARED_RULES : DEFAULT_RULES;
@@ -610,7 +633,9 @@ async function updateGitignore(root, shared, dryRun) {
610
633
  if (!dryRun) {
611
634
  await writeFile(gitignorePath, content);
612
635
  }
613
- log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
636
+ if (!quiet) {
637
+ log.success("Updated .gitignore: .inspecto/ is no longer fully ignored");
638
+ }
614
639
  return;
615
640
  }
616
641
  const missingRules = desiredRules.filter((rule) => !content.includes(rule));
@@ -620,10 +645,14 @@ async function updateGitignore(root, shared, dryRun) {
620
645
  const section = "\n# Inspecto\n" + missingRules.join("\n") + "\n";
621
646
  content = content.trimEnd() + "\n" + section;
622
647
  if (dryRun) {
623
- log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
648
+ if (!quiet) {
649
+ log.dryRun(`Would update .gitignore with: ${missingRules.join(", ")}`);
650
+ }
624
651
  } else {
625
652
  await writeFile(gitignorePath, content);
626
- log.success("Updated .gitignore");
653
+ if (!quiet) {
654
+ log.success("Updated .gitignore");
655
+ }
627
656
  }
628
657
  }
629
658
  async function cleanGitignore(root) {
@@ -635,6 +664,26 @@ async function cleanGitignore(root) {
635
664
  }
636
665
 
637
666
  // src/onboarding/apply.ts
667
+ function shellQuote(value) {
668
+ return `'${value.replace(/'/g, `'\\''`)}'`;
669
+ }
670
+ function resolveRuntimePackages() {
671
+ const devRepo = process.env.INSPECTO_DEV_REPO;
672
+ if (!devRepo) {
673
+ return {
674
+ installSpec: "@inspecto-dev/plugin @inspecto-dev/core",
675
+ installedDependencyNames: ["@inspecto-dev/plugin", "@inspecto-dev/core"]
676
+ };
677
+ }
678
+ const normalizedRepo = path5.resolve(devRepo);
679
+ return {
680
+ installSpec: [
681
+ shellQuote(path5.join(normalizedRepo, "packages/plugin")),
682
+ shellQuote(path5.join(normalizedRepo, "packages/core"))
683
+ ].join(" "),
684
+ installedDependencyNames: ["@inspecto-dev/plugin", "@inspecto-dev/core"]
685
+ };
686
+ }
638
687
  function resultStatus(nextSteps) {
639
688
  return nextSteps.length > 0 ? "warning" : "ok";
640
689
  }
@@ -724,10 +773,8 @@ async function applyOnboardingPlanInternal(input) {
724
773
  const promptsFileName = input.options.shared ? "prompts.json" : "prompts.local.json";
725
774
  const settingsPath = path5.join(settingsDir, settingsFileName);
726
775
  const promptsPath = path5.join(settingsDir, promptsFileName);
727
- const installCmd = getInstallCommand(
728
- input.packageManager,
729
- "@inspecto-dev/plugin @inspecto-dev/core"
730
- );
776
+ const runtimePackages = resolveRuntimePackages();
777
+ const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec);
731
778
  const nextSteps = [];
732
779
  let installFailed = false;
733
780
  if (input.options.skipInstall) {
@@ -744,8 +791,9 @@ async function applyOnboardingPlanInternal(input) {
744
791
  await shell(installCmd, input.projectRoot);
745
792
  spinner.succeed("Dependencies installed successfully");
746
793
  reporter.success("Installed @inspecto-dev/plugin and @inspecto-dev/core as devDependencies");
747
- mutations.push({ type: "dependency_added", name: "@inspecto-dev/plugin", dev: true });
748
- mutations.push({ type: "dependency_added", name: "@inspecto-dev/core", dev: true });
794
+ for (const name of runtimePackages.installedDependencyNames) {
795
+ mutations.push({ type: "dependency_added", name, dev: true });
796
+ }
749
797
  } catch (error) {
750
798
  spinner.fail("Dependency installation failed");
751
799
  installFailed = true;
@@ -758,7 +806,12 @@ async function applyOnboardingPlanInternal(input) {
758
806
  }
759
807
  let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig);
760
808
  for (const target of input.supportedBuildTargets) {
761
- const result = await injectPlugin(input.repoRoot, target, input.options.dryRun);
809
+ const result = await injectPlugin(
810
+ input.repoRoot,
811
+ target,
812
+ input.options.dryRun,
813
+ input.options.quiet ?? false
814
+ );
762
815
  if (result.success) {
763
816
  mutations.push(...result.mutations);
764
817
  } else {
@@ -800,7 +853,12 @@ async function applyOnboardingPlanInternal(input) {
800
853
  mutations.push({ type: "file_created", path: `.inspecto/${promptsFileName}` });
801
854
  }
802
855
  if (!input.options.dryRun) {
803
- await updateGitignore(input.projectRoot, input.options.shared, input.options.dryRun);
856
+ await updateGitignore(
857
+ input.projectRoot,
858
+ input.options.shared,
859
+ input.options.dryRun,
860
+ input.options.quiet ?? false
861
+ );
804
862
  mutations.push({
805
863
  type: "file_modified",
806
864
  path: ".gitignore",
@@ -822,7 +880,11 @@ async function applyOnboardingPlanInternal(input) {
822
880
  if (input.options.noExtension) {
823
881
  reporter.warn("Skipping IDE extension (--no-extension)");
824
882
  } else if (shouldInstallExt) {
825
- const extMutation = await installExtension(input.options.dryRun, input.selectedIDE?.ide);
883
+ const extMutation = await installExtension(
884
+ input.options.dryRun,
885
+ input.selectedIDE?.ide,
886
+ input.options.quiet ?? false
887
+ );
826
888
  if (extMutation && !input.options.dryRun) {
827
889
  mutations.push(extMutation);
828
890
  if (extMutation.manual_action_required) {
@@ -1496,7 +1558,9 @@ function unsupportedEnvironmentWarnings(context) {
1496
1558
  }
1497
1559
  const unsupportedIdes = context.ides.filter((ide) => !ide.supported).map((ide) => ide.ide);
1498
1560
  if (unsupportedIdes.length > 0) {
1499
- warnings.push(message("unsupported-ide", `Unsupported IDE(s) detected: ${unsupportedIdes.join(", ")}`));
1561
+ warnings.push(
1562
+ message("unsupported-ide", `Unsupported IDE(s) detected: ${unsupportedIdes.join(", ")}`)
1563
+ );
1500
1564
  }
1501
1565
  const unsupportedProviders = context.providers.filter((provider) => !provider.supported).map((provider) => provider.label);
1502
1566
  if (unsupportedProviders.length > 0) {
@@ -1635,6 +1699,9 @@ function createPlanResult(context) {
1635
1699
  defaults
1636
1700
  };
1637
1701
  }
1702
+ function planManualFollowUp(result) {
1703
+ return result.actions.filter((action) => action.type === "manual_step").map((action) => action.description);
1704
+ }
1638
1705
 
1639
1706
  // src/commands/apply.ts
1640
1707
  function getProviderDefault(providerId, preferredMode) {
@@ -1680,7 +1747,11 @@ function printApplyResult(result) {
1680
1747
  if (result.plan.warnings.length > 0) {
1681
1748
  return;
1682
1749
  }
1683
- log.ready("Ready! Hold Alt + Click any element to inspect.");
1750
+ log.ready("Ready! Inspecto is set up.");
1751
+ log.info("Next:");
1752
+ log.hint("1. Start or restart your dev server.");
1753
+ log.hint("2. Open your app in the browser.");
1754
+ log.hint("3. Hold Alt + Click any element to inspect.");
1684
1755
  }
1685
1756
  async function apply(options = {}) {
1686
1757
  const root = process.cwd();
@@ -1763,6 +1834,89 @@ async function detect(json = false) {
1763
1834
  // src/commands/init.ts
1764
1835
  import path10 from "path";
1765
1836
 
1837
+ // src/onboarding/target-resolution.ts
1838
+ function normalizePackagePath(packagePath) {
1839
+ if (!packagePath || packagePath === ".") return "";
1840
+ return packagePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
1841
+ }
1842
+ function looksLikeAppPackage(packagePath) {
1843
+ if (!packagePath) return true;
1844
+ return /(^|\/)(app|apps|web|client|frontend|site)(\/|$)/i.test(packagePath);
1845
+ }
1846
+ function looksLikeAuxiliaryPackage(packagePath) {
1847
+ return /(^|\/)(docs?|example|examples|playground|storybook|demo)(\/|$)/i.test(packagePath);
1848
+ }
1849
+ function buildCandidates(input) {
1850
+ return input.buildTools.map((buildTool) => {
1851
+ const packagePath = normalizePackagePath(buildTool.packagePath);
1852
+ return {
1853
+ packagePath,
1854
+ configPath: buildTool.configPath,
1855
+ buildTool: buildTool.tool,
1856
+ frameworks: input.frameworkSupportByPackage[packagePath] ?? [],
1857
+ automaticInjection: true
1858
+ };
1859
+ });
1860
+ }
1861
+ function rankCandidate(candidate) {
1862
+ let score = 0;
1863
+ if (candidate.frameworks.length > 0) score += 4;
1864
+ if (candidate.automaticInjection) score += 2;
1865
+ if (looksLikeAppPackage(candidate.packagePath)) score += 1;
1866
+ if (looksLikeAuxiliaryPackage(candidate.packagePath)) score -= 2;
1867
+ return score;
1868
+ }
1869
+ function rankCandidates(candidates) {
1870
+ return candidates.map((candidate) => ({
1871
+ candidate,
1872
+ score: rankCandidate(candidate)
1873
+ })).sort((left, right) => right.score - left.score);
1874
+ }
1875
+ function resolveOnboardingTarget(input) {
1876
+ const candidates = buildCandidates(input);
1877
+ if (candidates.length === 0) {
1878
+ return {
1879
+ status: "needs_selection",
1880
+ candidates,
1881
+ reason: "No supported targets were detected."
1882
+ };
1883
+ }
1884
+ const explicitlySelected = normalizePackagePath(input.selectedPackagePath);
1885
+ if (input.selectedPackagePath !== void 0) {
1886
+ const selected = candidates.find((candidate) => candidate.packagePath === explicitlySelected);
1887
+ if (selected) {
1888
+ return {
1889
+ status: "resolved",
1890
+ selected,
1891
+ candidates,
1892
+ reason: `Using the explicitly selected target: ${selected.packagePath || "."}.`
1893
+ };
1894
+ }
1895
+ }
1896
+ if (candidates.length === 1) {
1897
+ return {
1898
+ status: "resolved",
1899
+ selected: candidates[0],
1900
+ candidates,
1901
+ reason: "Only one supported target was detected."
1902
+ };
1903
+ }
1904
+ const ranked = rankCandidates(candidates);
1905
+ if (ranked.length > 1 && ranked[0].score === ranked[1].score) {
1906
+ return {
1907
+ status: "needs_selection",
1908
+ candidates,
1909
+ reason: "Multiple supported targets look equally plausible."
1910
+ };
1911
+ }
1912
+ return {
1913
+ status: "resolved",
1914
+ selected: ranked[0].candidate,
1915
+ candidates,
1916
+ reason: `Preselected ${ranked[0].candidate.packagePath || "."} because it has the strongest supported app signal.`
1917
+ };
1918
+ }
1919
+
1766
1920
  // src/prompts.ts
1767
1921
  import prompts from "prompts";
1768
1922
  async function promptIDEChoice(detections) {
@@ -1841,7 +1995,9 @@ async function promptMonorepoPackageChoice(detections) {
1841
1995
  }
1842
1996
  if (!process.stdin.isTTY) {
1843
1997
  log.error("Monorepo root detected, but stdin is not interactive.");
1844
- log.hint("Re-run `inspecto init` inside a specific app directory, or pass --packages <app-path>.");
1998
+ log.hint(
1999
+ "Re-run `inspecto init` inside a specific app directory, or pass --packages <app-path>."
2000
+ );
1845
2001
  return null;
1846
2002
  }
1847
2003
  const { choice } = await prompts({
@@ -1976,27 +2132,40 @@ async function init(options) {
1976
2132
  verifiedPackages.length > 0 ? verifiedPackages : void 0
1977
2133
  );
1978
2134
  if (verifiedPackages.length === 0) {
1979
- const monorepoTargets = Array.from(
1980
- new Set(
1981
- buildResult.supported.map((d) => d.packagePath).filter((value) => !!value)
1982
- )
1983
- );
1984
- if (monorepoTargets.length > 1) {
1985
- log.warn("Monorepo root detected with multiple candidate apps.");
1986
- const selectedPackage = await promptMonorepoPackageChoice(buildResult.supported);
1987
- if (!selectedPackage) {
1988
- log.hint("Run `inspecto init` inside the target app, or pass --packages <app-path>.");
1989
- return;
2135
+ const monorepoCandidates = buildResult.supported.filter((detection) => !!detection.packagePath);
2136
+ if (monorepoCandidates.length > 0) {
2137
+ const frameworkSupportByPackage = await detectFrameworkSupportByPackage(
2138
+ repoRoot,
2139
+ monorepoCandidates
2140
+ );
2141
+ const targetResolution = resolveOnboardingTarget({
2142
+ repoRoot,
2143
+ buildTools: monorepoCandidates,
2144
+ frameworkSupportByPackage
2145
+ });
2146
+ if (targetResolution.status === "needs_selection") {
2147
+ log.warn("Monorepo root detected with multiple candidate apps.");
2148
+ const selectedPackage = await promptMonorepoPackageChoice(
2149
+ monorepoCandidates.filter(
2150
+ (detection) => targetResolution.candidates.some(
2151
+ (candidate) => candidate.packagePath === (detection.packagePath ?? "")
2152
+ )
2153
+ )
2154
+ );
2155
+ if (!selectedPackage) {
2156
+ log.hint("Run `inspecto init` inside the target app, or pass --packages <app-path>.");
2157
+ return;
2158
+ }
2159
+ projectRoot = path10.join(repoRoot, selectedPackage);
2160
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
2161
+ log.info(`Continuing initialization in ${selectedPackage}`);
2162
+ } else if (targetResolution.selected?.packagePath) {
2163
+ const selectedPackage = targetResolution.selected.packagePath;
2164
+ projectRoot = path10.join(repoRoot, selectedPackage);
2165
+ buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
2166
+ log.warn(`Monorepo root detected. Using the only candidate app: ${selectedPackage}`);
2167
+ log.hint("Run `inspecto init` inside that app next time to skip this prompt.");
1990
2168
  }
1991
- projectRoot = path10.join(repoRoot, selectedPackage);
1992
- buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
1993
- log.info(`Continuing initialization in ${selectedPackage}`);
1994
- } else if (monorepoTargets.length === 1) {
1995
- const [selectedPackage] = monorepoTargets;
1996
- projectRoot = path10.join(repoRoot, selectedPackage);
1997
- buildResult = await detectBuildTools(repoRoot, [selectedPackage]);
1998
- log.warn(`Monorepo root detected. Using the only candidate app: ${selectedPackage}`);
1999
- log.hint("Run `inspecto init` inside that app next time to skip this prompt.");
2000
2169
  }
2001
2170
  }
2002
2171
  const [frameworkResult, ideProbe, providerProbe] = await Promise.all([
@@ -2173,7 +2342,11 @@ async function init(options) {
2173
2342
  log.hint("Complete the items above.");
2174
2343
  log.blank();
2175
2344
  } else {
2176
- log.ready("Ready! Hold Alt + Click any element to inspect.");
2345
+ log.ready("Ready! Inspecto is set up.");
2346
+ log.info("Next:");
2347
+ log.hint("1. Start or restart your dev server.");
2348
+ log.hint("2. Open your app in the browser.");
2349
+ log.hint("3. Hold Alt + Click any element to inspect.");
2177
2350
  }
2178
2351
  }
2179
2352
  }
@@ -2198,6 +2371,19 @@ function matchesAnyPackage(detection, packages) {
2198
2371
  if (packages.length === 0) return true;
2199
2372
  return packages.some((pkg) => matchesPackage(detection, pkg));
2200
2373
  }
2374
+ async function detectFrameworkSupportByPackage(repoRoot, buildTools) {
2375
+ const packagePaths = Array.from(
2376
+ new Set(buildTools.map((buildTool) => buildTool.packagePath).filter((value) => !!value))
2377
+ );
2378
+ const supportByPackage = {};
2379
+ await Promise.all(
2380
+ packagePaths.map(async (packagePath) => {
2381
+ const result = await detectFrameworks(path10.join(repoRoot, packagePath));
2382
+ supportByPackage[packagePath] = result.supported;
2383
+ })
2384
+ );
2385
+ return supportByPackage;
2386
+ }
2201
2387
 
2202
2388
  // src/commands/doctor.ts
2203
2389
  import path11 from "path";
@@ -2245,12 +2431,9 @@ function printDoctorResult(result) {
2245
2431
  async function collectDoctorResult(root = process.cwd()) {
2246
2432
  const checks = [];
2247
2433
  if (!await exists(path11.join(root, "package.json"))) {
2248
- const diagnostic = createDiagnostic(
2249
- "missing-package-json",
2250
- "error",
2251
- "No package.json found",
2252
- ["Run this command from your project root"]
2253
- );
2434
+ const diagnostic = createDiagnostic("missing-package-json", "error", "No package.json found", [
2435
+ "Run this command from your project root"
2436
+ ]);
2254
2437
  checks.push(diagnostic);
2255
2438
  return {
2256
2439
  status: "blocked",
@@ -2374,20 +2557,28 @@ async function collectDoctorResult(root = process.cwd()) {
2374
2557
  }
2375
2558
  if (!injected) {
2376
2559
  checks.push(
2377
- createDiagnostic("plugin-not-configured", "error", "Plugin not configured in any build config", [
2378
- "Fix: npx @inspecto-dev/cli init"
2379
- ])
2560
+ createDiagnostic(
2561
+ "plugin-not-configured",
2562
+ "error",
2563
+ "Plugin not configured in any build config",
2564
+ ["Fix: npx @inspecto-dev/cli init"]
2565
+ )
2380
2566
  );
2381
2567
  }
2382
2568
  } else if (buildResult.unsupported.length > 0) {
2383
2569
  const names = buildResult.unsupported.join(", ");
2384
2570
  checks.push(
2385
- createDiagnostic(`build-tool-unsupported`, "warning", `Build tool: ${names} (not supported in v1)`, [
2386
- "current version supports: Vite, Webpack, Rspack, esbuild, Rollup"
2387
- ])
2571
+ createDiagnostic(
2572
+ `build-tool-unsupported`,
2573
+ "warning",
2574
+ `Build tool: ${names} (not supported in v1)`,
2575
+ ["current version supports: Vite, Webpack, Rspack, esbuild, Rollup"]
2576
+ )
2388
2577
  );
2389
2578
  } else {
2390
- checks.push(createDiagnostic("build-tool-missing", "warning", "No recognized build config found"));
2579
+ checks.push(
2580
+ createDiagnostic("build-tool-missing", "warning", "No recognized build config found")
2581
+ );
2391
2582
  }
2392
2583
  if (extInstalled) {
2393
2584
  checks.push(createDiagnostic("extension-installed", "ok", "VS Code extension detected"));
@@ -2483,6 +2674,350 @@ async function doctor(options = {}) {
2483
2674
  return writeCommandOutput(result, json, printDoctorResult);
2484
2675
  }
2485
2676
 
2677
+ // src/onboarding/session.ts
2678
+ import path12 from "path";
2679
+ function normalizePackagePath2(packagePath) {
2680
+ if (!packagePath || packagePath === ".") return "";
2681
+ return packagePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2682
+ }
2683
+ function getProviderDefault2(providerId, preferredMode) {
2684
+ if (!providerId) return void 0;
2685
+ const mode = preferredMode ?? (providerId === "coco" ? "cli" : "extension");
2686
+ return `${providerId}.${mode}`;
2687
+ }
2688
+ function getVerificationCommand(packageManager) {
2689
+ switch (packageManager) {
2690
+ case "pnpm":
2691
+ return "pnpm dev";
2692
+ case "yarn":
2693
+ return "yarn dev";
2694
+ case "bun":
2695
+ return "bun run dev";
2696
+ case "npm":
2697
+ default:
2698
+ return "npm run dev";
2699
+ }
2700
+ }
2701
+ async function buildVerification(projectRoot, packageManager) {
2702
+ const packageJson = await readJSON(
2703
+ path12.join(projectRoot, "package.json")
2704
+ );
2705
+ if (packageJson?.scripts?.dev) {
2706
+ const devCommand = getVerificationCommand(packageManager);
2707
+ return {
2708
+ available: true,
2709
+ devCommand,
2710
+ message: `Start the local dev server with \`${devCommand}\` to verify Inspecto in the browser.`
2711
+ };
2712
+ }
2713
+ return {
2714
+ available: false,
2715
+ message: "Start your normal local dev server command to verify Inspecto in the browser."
2716
+ };
2717
+ }
2718
+ function buildIdeExtensionStatus(input) {
2719
+ if (!input.required) {
2720
+ return {
2721
+ required: false,
2722
+ installed: false,
2723
+ manualRequired: false
2724
+ };
2725
+ }
2726
+ return {
2727
+ required: true,
2728
+ installed: input.installed,
2729
+ manualRequired: input.manualRequired,
2730
+ installCommand: "code --install-extension inspecto.inspecto",
2731
+ marketplaceUrl: "https://marketplace.visualstudio.com/items?itemName=inspecto.inspecto",
2732
+ openVsxUrl: "https://open-vsx.org/extension/inspecto/inspecto"
2733
+ };
2734
+ }
2735
+ async function detectFrameworkSupportByPackage2(repoRoot, context) {
2736
+ const packagePaths = new Set(
2737
+ context.buildTools.supported.map((item) => normalizePackagePath2(item.packagePath))
2738
+ );
2739
+ const supportByPackage = {};
2740
+ await Promise.all(
2741
+ Array.from(packagePaths).map(async (packagePath) => {
2742
+ const frameworkResult = await detectFrameworks(
2743
+ packagePath ? path12.join(repoRoot, packagePath) : repoRoot
2744
+ );
2745
+ supportByPackage[packagePath] = frameworkResult.supported;
2746
+ })
2747
+ );
2748
+ return supportByPackage;
2749
+ }
2750
+ async function buildTargetedContext(rootContext, packagePath) {
2751
+ const projectRoot = packagePath ? path12.join(rootContext.root, packagePath) : rootContext.root;
2752
+ const [frameworks, ides, providers] = await Promise.all([
2753
+ detectFrameworks(projectRoot),
2754
+ detectIDE(projectRoot),
2755
+ detectProviders(projectRoot)
2756
+ ]);
2757
+ return {
2758
+ root: projectRoot,
2759
+ packageManager: rootContext.packageManager,
2760
+ buildTools: {
2761
+ supported: rootContext.buildTools.supported.filter((item) => {
2762
+ return normalizePackagePath2(item.packagePath) === packagePath;
2763
+ }),
2764
+ unsupported: []
2765
+ },
2766
+ frameworks: {
2767
+ supported: frameworks.supported,
2768
+ unsupported: frameworks.unsupported.map((item) => item.name)
2769
+ },
2770
+ ides: ides.detected.map(({ ide, supported }) => ({ ide, supported })),
2771
+ providers: providers.detected.map(({ id, label, supported, preferredMode }) => ({
2772
+ id,
2773
+ label,
2774
+ supported,
2775
+ preferredMode
2776
+ }))
2777
+ };
2778
+ }
2779
+ function buildOnboardingSummary(plan2, projectRoot) {
2780
+ const changes = plan2.actions.filter((action) => action.type !== "manual_step").map((action) => action.description);
2781
+ const risks = [...plan2.warnings.map((item) => item.message)];
2782
+ const manualFollowUp = planManualFollowUp(plan2);
2783
+ let headline = `Inspecto is ready to onboard ${projectRoot}.`;
2784
+ if (manualFollowUp.length > 0) {
2785
+ headline = `Inspecto can partially onboard ${projectRoot}, but manual follow-up remains.`;
2786
+ } else if (plan2.status === "blocked") {
2787
+ headline = `Inspecto could not build an automatic onboarding path for ${projectRoot}.`;
2788
+ }
2789
+ return {
2790
+ headline,
2791
+ changes,
2792
+ risks,
2793
+ manualFollowUp
2794
+ };
2795
+ }
2796
+ function buildConfirmation(plan2, summary, session, options) {
2797
+ if (options.yes) {
2798
+ return { required: false };
2799
+ }
2800
+ const reasons = [];
2801
+ if (session.targetWasHeuristic) reasons.push("a monorepo target was preselected");
2802
+ if (summary.manualFollowUp.length > 0) reasons.push("manual follow-up will remain after setup");
2803
+ if (options.noExtension || options.skipInstall) reasons.push("non-core setup steps are being skipped");
2804
+ if (session.supportedIdeCount > 1 || session.supportedProviderCount > 1) {
2805
+ reasons.push("multiple IDE or provider choices are still relevant");
2806
+ }
2807
+ if (plan2.warnings.length > 0) reasons.push("the CLI detected non-blocking risk");
2808
+ if (reasons.length === 0) {
2809
+ return { required: false };
2810
+ }
2811
+ return {
2812
+ required: true,
2813
+ reason: reasons.join("; "),
2814
+ question: "Proceed with Inspecto onboarding using the proposed default target and settings?"
2815
+ };
2816
+ }
2817
+ function buildPreApplyResult(status, session) {
2818
+ const diagnostics = session.summary.risks.length > 0 || session.summary.manualFollowUp.length > 0 || session.plan.blockers.length > 0 ? {
2819
+ warnings: session.summary.risks,
2820
+ errors: session.plan.blockers.map((item) => item.message),
2821
+ nextSteps: session.summary.manualFollowUp
2822
+ } : void 0;
2823
+ return {
2824
+ status,
2825
+ target: session.target,
2826
+ summary: session.summary,
2827
+ confirmation: session.confirmation,
2828
+ ideExtension: buildIdeExtensionStatus({
2829
+ required: session.plan.defaults.extension,
2830
+ installed: false,
2831
+ manualRequired: session.plan.defaults.extension
2832
+ }),
2833
+ verification: session.verification,
2834
+ diagnostics
2835
+ };
2836
+ }
2837
+ function buildExecutionResult(session, applyResult) {
2838
+ return {
2839
+ changedFiles: Array.from(
2840
+ new Set(applyResult.mutations.map((item) => item.path).filter((value) => !!value))
2841
+ ),
2842
+ installedDependencies: applyResult.mutations.map((item) => item.name).filter((value) => !!value),
2843
+ selectedProviderDefault: session.providerDefault,
2844
+ selectedIDE: session.selectedIDE?.ide,
2845
+ mutations: applyResult.mutations
2846
+ };
2847
+ }
2848
+ function buildExecutionDiagnostics(session, applyResult) {
2849
+ const warnings = [...session.plan.warnings.map((item) => item.message)];
2850
+ const errors = [];
2851
+ if (applyResult.postInstall.installFailed) {
2852
+ errors.push("Dependency installation failed during onboarding.");
2853
+ }
2854
+ if (applyResult.postInstall.injectionFailed) {
2855
+ warnings.push("Automatic plugin injection did not finish cleanly.");
2856
+ }
2857
+ if (applyResult.postInstall.manualExtensionInstallNeeded) {
2858
+ warnings.push("IDE extension installation still needs manual completion.");
2859
+ }
2860
+ if (warnings.length === 0 && errors.length === 0 && applyResult.postInstall.nextSteps.length === 0) {
2861
+ return void 0;
2862
+ }
2863
+ return {
2864
+ warnings,
2865
+ errors,
2866
+ nextSteps: applyResult.postInstall.nextSteps
2867
+ };
2868
+ }
2869
+ async function resolveOnboardingSession(root, options = {}) {
2870
+ const rootContext = await buildOnboardingContext(root);
2871
+ const rootVerification = await buildVerification(root, rootContext.packageManager);
2872
+ const frameworkSupportByPackage = await detectFrameworkSupportByPackage2(root, rootContext);
2873
+ const target = resolveOnboardingTarget({
2874
+ repoRoot: root,
2875
+ buildTools: rootContext.buildTools.supported,
2876
+ frameworkSupportByPackage,
2877
+ selectedPackagePath: options.target
2878
+ });
2879
+ if (target.candidates.length === 0) {
2880
+ const plan3 = createPlanResult(rootContext);
2881
+ return {
2882
+ status: "error",
2883
+ target,
2884
+ summary: buildOnboardingSummary(plan3, root),
2885
+ confirmation: { required: false },
2886
+ verification: rootVerification,
2887
+ context: rootContext,
2888
+ plan: plan3,
2889
+ projectRoot: root
2890
+ };
2891
+ }
2892
+ if (target.status === "needs_selection") {
2893
+ const plan3 = createPlanResult(rootContext);
2894
+ const summary2 = {
2895
+ headline: "Inspecto found multiple plausible app targets and needs one selection.",
2896
+ changes: [],
2897
+ risks: [],
2898
+ manualFollowUp: []
2899
+ };
2900
+ return {
2901
+ status: "needs_target_selection",
2902
+ target,
2903
+ summary: summary2,
2904
+ confirmation: { required: false },
2905
+ verification: rootVerification,
2906
+ context: rootContext,
2907
+ plan: plan3,
2908
+ projectRoot: root
2909
+ };
2910
+ }
2911
+ const packagePath = normalizePackagePath2(target.selected?.packagePath);
2912
+ const context = await buildTargetedContext(rootContext, packagePath);
2913
+ const verification = await buildVerification(context.root, context.packageManager);
2914
+ const plan2 = createPlanResult(context);
2915
+ const summary = buildOnboardingSummary(plan2, context.root);
2916
+ const confirmation = buildConfirmation(
2917
+ plan2,
2918
+ summary,
2919
+ {
2920
+ targetWasHeuristic: target.candidates.length > 1 && options.target === void 0,
2921
+ supportedIdeCount: context.ides.filter((item) => item.supported).length,
2922
+ supportedProviderCount: context.providers.filter((item) => item.supported).length
2923
+ },
2924
+ options
2925
+ );
2926
+ const selectedProvider = context.providers.find((provider) => provider.id === plan2.defaults.provider) ?? null;
2927
+ const selectedIDE = context.ides.find((ide) => ide.ide === plan2.defaults.ide) ?? context.ides.find((ide) => ide.supported) ?? null;
2928
+ let status = "success";
2929
+ if (plan2.status === "blocked") {
2930
+ status = "error";
2931
+ } else if (confirmation.required) {
2932
+ status = "needs_confirmation";
2933
+ } else if (summary.manualFollowUp.length > 0 || plan2.warnings.length > 0) {
2934
+ status = "partial_success";
2935
+ }
2936
+ return {
2937
+ status,
2938
+ target,
2939
+ summary,
2940
+ confirmation,
2941
+ verification,
2942
+ context,
2943
+ plan: plan2,
2944
+ projectRoot: context.root,
2945
+ selectedIDE,
2946
+ providerDefault: getProviderDefault2(plan2.defaults.provider, selectedProvider?.preferredMode)
2947
+ };
2948
+ }
2949
+ async function applyResolvedOnboardingSession(session, options = {}) {
2950
+ const verification = await buildVerification(session.projectRoot, session.context.packageManager);
2951
+ const applyResult = await applyOnboardingPlan({
2952
+ repoRoot: process.cwd(),
2953
+ projectRoot: session.projectRoot,
2954
+ packageManager: session.context.packageManager,
2955
+ supportedBuildTargets: session.context.buildTools.supported,
2956
+ options: {
2957
+ shared: options.shared ?? session.plan.defaults.shared,
2958
+ skipInstall: options.skipInstall ?? false,
2959
+ dryRun: options.dryRun ?? false,
2960
+ noExtension: options.noExtension ?? !session.plan.defaults.extension,
2961
+ quiet: options.json ?? false
2962
+ },
2963
+ selectedIDE: session.selectedIDE,
2964
+ providerDefault: session.providerDefault,
2965
+ plan: session.plan
2966
+ });
2967
+ const diagnostics = buildExecutionDiagnostics(session, applyResult);
2968
+ const status = applyResult.postInstall.installFailed && session.context.buildTools.supported.length === 0 ? "error" : diagnostics?.nextSteps.length || diagnostics?.errors.length || diagnostics?.warnings.length ? "partial_success" : "success";
2969
+ return {
2970
+ status,
2971
+ target: session.target,
2972
+ summary: session.summary,
2973
+ confirmation: session.confirmation,
2974
+ ideExtension: buildIdeExtensionStatus({
2975
+ required: session.plan.defaults.extension,
2976
+ installed: session.plan.defaults.extension && !applyResult.postInstall.manualExtensionInstallNeeded,
2977
+ manualRequired: applyResult.postInstall.manualExtensionInstallNeeded
2978
+ }),
2979
+ verification,
2980
+ result: buildExecutionResult(session, applyResult),
2981
+ diagnostics
2982
+ };
2983
+ }
2984
+ function buildDeferredOnboardResult(session) {
2985
+ return buildPreApplyResult(session.status, session);
2986
+ }
2987
+
2988
+ // src/commands/onboard.ts
2989
+ function printOnboardResult(result) {
2990
+ log.header("Inspecto Onboard");
2991
+ log.info(`Status: ${result.status}`);
2992
+ log.info(result.summary.headline);
2993
+ for (const change of result.summary.changes) {
2994
+ log.hint(change);
2995
+ }
2996
+ for (const step of result.diagnostics?.nextSteps ?? []) {
2997
+ log.warn(step);
2998
+ }
2999
+ if (result.confirmation.required && result.confirmation.question) {
3000
+ log.warn(result.confirmation.question);
3001
+ }
3002
+ const extensionReady = !result.ideExtension?.required || result.ideExtension.installed && !result.ideExtension.manualRequired;
3003
+ if (extensionReady && (result.status === "success" || result.status === "partial_success") && result.verification?.message) {
3004
+ log.info(result.verification.message);
3005
+ }
3006
+ }
3007
+ async function onboard(options = {}) {
3008
+ const root = process.cwd();
3009
+ const session = await resolveOnboardingSession(root, options);
3010
+ if (session.status === "error" || session.status === "needs_target_selection" || session.status === "needs_confirmation") {
3011
+ return writeCommandOutput(
3012
+ buildDeferredOnboardResult(session),
3013
+ options.json ?? false,
3014
+ printOnboardResult
3015
+ );
3016
+ }
3017
+ const result = await applyResolvedOnboardingSession(session, options);
3018
+ return writeCommandOutput(result, options.json ?? false, printOnboardResult);
3019
+ }
3020
+
2486
3021
  // src/commands/plan.ts
2487
3022
  function printPlanResult(result) {
2488
3023
  log.header("Inspecto Plan");
@@ -2517,11 +3052,11 @@ async function plan(json = false) {
2517
3052
  }
2518
3053
 
2519
3054
  // src/commands/teardown.ts
2520
- import path12 from "path";
3055
+ import path13 from "path";
2521
3056
  async function teardown() {
2522
3057
  const root = process.cwd();
2523
3058
  log.header("Inspecto Teardown");
2524
- const lockPath = path12.join(root, ".inspecto", "install.lock");
3059
+ const lockPath = path13.join(root, ".inspecto", "install.lock");
2525
3060
  const lock = await readJSON(lockPath);
2526
3061
  if (!lock) {
2527
3062
  log.warn("No .inspecto/install.lock found. Running in best-effort mode.");
@@ -2534,8 +3069,8 @@ async function teardown() {
2534
3069
  } catch {
2535
3070
  log.warn("Could not remove @inspecto-dev/plugin (may not be installed)");
2536
3071
  }
2537
- if (await exists(path12.join(root, ".inspecto"))) {
2538
- await removeDir(path12.join(root, ".inspecto"));
3072
+ if (await exists(path13.join(root, ".inspecto"))) {
3073
+ await removeDir(path13.join(root, ".inspecto"));
2539
3074
  log.success("Deleted .inspecto/ directory");
2540
3075
  }
2541
3076
  await cleanGitignore(root);
@@ -2584,7 +3119,7 @@ async function teardown() {
2584
3119
  }
2585
3120
  }
2586
3121
  }
2587
- await removeDir(path12.join(root, ".inspecto"));
3122
+ await removeDir(path13.join(root, ".inspecto"));
2588
3123
  log.success("Deleted .inspecto/ directory");
2589
3124
  await cleanGitignore(root);
2590
3125
  log.blank();
@@ -2593,6 +3128,9 @@ async function teardown() {
2593
3128
  }
2594
3129
 
2595
3130
  export {
3131
+ exists,
3132
+ writeFile,
3133
+ log,
2596
3134
  writeCommandOutput,
2597
3135
  reportCommandError,
2598
3136
  apply,
@@ -2600,6 +3138,7 @@ export {
2600
3138
  init,
2601
3139
  collectDoctorResult,
2602
3140
  doctor,
3141
+ onboard,
2603
3142
  plan,
2604
3143
  teardown
2605
3144
  };