@inspecto-dev/cli 0.3.3 → 0.3.4

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.
@@ -869,6 +869,34 @@ async function cleanGitignore(root) {
869
869
  }
870
870
 
871
871
  // src/onboarding/apply.ts
872
+ function normalizeSupportedIde(ide) {
873
+ if (!ide) return void 0;
874
+ return ide.toLowerCase() === "vscode" ? "vscode" : ide.toLowerCase();
875
+ }
876
+ async function readInheritedSettingsDefaults(repoRoot, projectRoot, settingsFileName) {
877
+ if (path6.resolve(repoRoot) === path6.resolve(projectRoot)) {
878
+ return {};
879
+ }
880
+ const inheritedSettingsPath = path6.join(repoRoot, ".inspecto", settingsFileName);
881
+ if (!await exists(inheritedSettingsPath)) {
882
+ return {};
883
+ }
884
+ const inheritedSettings = await readJSON(inheritedSettingsPath);
885
+ if (!inheritedSettings || typeof inheritedSettings !== "object") {
886
+ return {};
887
+ }
888
+ const inheritedDefaults = {};
889
+ if (typeof inheritedSettings.ide === "string") {
890
+ const normalizedIde = normalizeSupportedIde(inheritedSettings.ide);
891
+ if (normalizedIde) {
892
+ inheritedDefaults.ide = normalizedIde;
893
+ }
894
+ }
895
+ if (typeof inheritedSettings["provider.default"] === "string") {
896
+ inheritedDefaults.providerDefault = inheritedSettings["provider.default"];
897
+ }
898
+ return inheritedDefaults;
899
+ }
872
900
  function shellQuote(value) {
873
901
  return `'${value.replace(/'/g, `'\\''`)}'`;
874
902
  }
@@ -892,11 +920,12 @@ function resolveRuntimePackages() {
892
920
  function resultStatus(nextSteps) {
893
921
  return nextSteps.length > 0 ? "warning" : "ok";
894
922
  }
895
- function manualPlanSteps(plan2) {
896
- return [
897
- ...plan2.blockers.map((blocker) => blocker.message),
898
- ...plan2.actions.filter((action) => action.type === "manual_step").map((action) => action.description)
899
- ];
923
+ function manualPlanSteps(plan2, includeBlockers = true) {
924
+ const steps = plan2.actions.filter((action) => action.type === "manual_step").map((action) => action.description);
925
+ if (!includeBlockers) {
926
+ return steps;
927
+ }
928
+ return [...plan2.blockers.map((blocker) => blocker.message), ...steps];
900
929
  }
901
930
  async function applyOnboardingPlan(input) {
902
931
  return applyOnboardingPlanInternal(input);
@@ -960,6 +989,7 @@ function createSpinner(text, quiet = false) {
960
989
  }
961
990
  async function applyOnboardingPlanInternal(input) {
962
991
  const reporter = createReporter(input.options.quiet);
992
+ const additiveManualPlan = input.plan?.strategy === "manual" && input.allowManualPlanApply && input.plan.blockers.length === 0;
963
993
  if (input.plan && input.plan.strategy !== "supported" && !input.allowManualPlanApply) {
964
994
  return {
965
995
  status: input.plan.status,
@@ -968,7 +998,7 @@ async function applyOnboardingPlanInternal(input) {
968
998
  installFailed: false,
969
999
  injectionFailed: false,
970
1000
  manualExtensionInstallNeeded: false,
971
- nextSteps: manualPlanSteps(input.plan)
1001
+ nextSteps: manualPlanSteps(input.plan, true)
972
1002
  }
973
1003
  };
974
1004
  }
@@ -978,6 +1008,11 @@ async function applyOnboardingPlanInternal(input) {
978
1008
  const promptsFileName = input.options.shared ? "prompts.json" : "prompts.local.json";
979
1009
  const settingsPath = path6.join(settingsDir, settingsFileName);
980
1010
  const promptsPath = path6.join(settingsDir, promptsFileName);
1011
+ const inheritedDefaults = await readInheritedSettingsDefaults(
1012
+ input.repoRoot,
1013
+ input.projectRoot,
1014
+ settingsFileName
1015
+ );
981
1016
  const runtimePackages = resolveRuntimePackages();
982
1017
  const installCmd = getInstallCommand(input.packageManager, runtimePackages.installSpec);
983
1018
  const nextSteps = [];
@@ -1010,17 +1045,19 @@ async function applyOnboardingPlanInternal(input) {
1010
1045
  }
1011
1046
  }
1012
1047
  let injectionFailed = Boolean(input.injectionSkippedRequiresManualConfig);
1013
- for (const target of input.supportedBuildTargets) {
1014
- const result = await injectPlugin(
1015
- input.repoRoot,
1016
- target,
1017
- input.options.dryRun,
1018
- input.options.quiet ?? false
1019
- );
1020
- if (result.success) {
1021
- mutations.push(...result.mutations);
1022
- } else {
1023
- injectionFailed = true;
1048
+ if (!additiveManualPlan) {
1049
+ for (const target of input.supportedBuildTargets) {
1050
+ const result = await injectPlugin(
1051
+ input.repoRoot,
1052
+ target,
1053
+ input.options.dryRun,
1054
+ input.options.quiet ?? false
1055
+ );
1056
+ if (result.success) {
1057
+ mutations.push(...result.mutations);
1058
+ } else {
1059
+ injectionFailed = true;
1060
+ }
1024
1061
  }
1025
1062
  }
1026
1063
  if (await exists(settingsPath)) {
@@ -1030,15 +1067,43 @@ async function applyOnboardingPlanInternal(input) {
1030
1067
  reporter.hint("Please fix the syntax errors manually, or delete it and re-run init");
1031
1068
  nextSteps.push(`Fix .inspecto/${settingsFileName} or delete it and rerun Inspecto setup.`);
1032
1069
  } else {
1033
- reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`);
1070
+ const mergedSettings = existingSettings && typeof existingSettings === "object" ? { ...existingSettings } : {};
1071
+ let settingsChanged = false;
1072
+ const desiredIde = inheritedDefaults.ide ?? (input.selectedIDE?.supported ? normalizeSupportedIde(input.selectedIDE.ide) : void 0);
1073
+ if (desiredIde && !mergedSettings.ide) {
1074
+ mergedSettings.ide = desiredIde;
1075
+ settingsChanged = true;
1076
+ }
1077
+ const desiredProviderDefault = inheritedDefaults.providerDefault ?? input.providerDefault;
1078
+ if (desiredProviderDefault && !mergedSettings["provider.default"]) {
1079
+ mergedSettings["provider.default"] = desiredProviderDefault;
1080
+ settingsChanged = true;
1081
+ }
1082
+ if (settingsChanged) {
1083
+ if (input.options.dryRun) {
1084
+ reporter.dryRun(`Would update .inspecto/${settingsFileName}`);
1085
+ } else {
1086
+ await writeJSON(settingsPath, mergedSettings);
1087
+ reporter.success(`Updated .inspecto/${settingsFileName} with missing defaults`);
1088
+ mutations.push({
1089
+ type: "file_modified",
1090
+ path: `.inspecto/${settingsFileName}`,
1091
+ description: "Merged missing Inspecto defaults into existing settings"
1092
+ });
1093
+ }
1094
+ } else {
1095
+ reporter.success(`.inspecto/${settingsFileName} already exists (skipped)`);
1096
+ }
1034
1097
  }
1035
1098
  } else {
1036
1099
  const defaultSettings = {};
1037
- if (input.selectedIDE?.supported) {
1038
- defaultSettings.ide = input.selectedIDE.ide.toLowerCase() === "vscode" ? "vscode" : input.selectedIDE.ide.toLowerCase();
1100
+ const desiredIde = inheritedDefaults.ide ?? (input.selectedIDE?.supported ? normalizeSupportedIde(input.selectedIDE.ide) : void 0);
1101
+ const desiredProviderDefault = inheritedDefaults.providerDefault ?? input.providerDefault;
1102
+ if (desiredIde) {
1103
+ defaultSettings.ide = desiredIde;
1039
1104
  }
1040
- if (input.providerDefault) {
1041
- defaultSettings["provider.default"] = input.providerDefault;
1105
+ if (desiredProviderDefault) {
1106
+ defaultSettings["provider.default"] = desiredProviderDefault;
1042
1107
  }
1043
1108
  if (input.options.dryRun) {
1044
1109
  reporter.dryRun(`Would create .inspecto/${settingsFileName}`);
@@ -1117,6 +1182,9 @@ async function applyOnboardingPlanInternal(input) {
1117
1182
  if (manualExtensionInstallNeeded) {
1118
1183
  nextSteps.push("Install the Inspecto IDE extension manually");
1119
1184
  }
1185
+ if (additiveManualPlan && input.plan) {
1186
+ nextSteps.push(...manualPlanSteps(input.plan, false));
1187
+ }
1120
1188
  if (input.manualConfigRequiredFor === "Nuxt") {
1121
1189
  nextSteps.push(
1122
1190
  "Nuxt detected\u2014please follow the Nuxt instructions printed above to finish setup."
@@ -1167,6 +1235,28 @@ async function getResolvedPackageVersion(pkgName, root) {
1167
1235
  return null;
1168
1236
  }
1169
1237
  }
1238
+ function parseFirstSemver(version) {
1239
+ if (!version) return null;
1240
+ const match = version.match(/(\d+)\.(\d+)\.(\d+)/);
1241
+ if (!match) {
1242
+ return null;
1243
+ }
1244
+ return {
1245
+ major: Number(match[1]),
1246
+ minor: Number(match[2]),
1247
+ patch: Number(match[3])
1248
+ };
1249
+ }
1250
+ function isLegacyRspackVersion(version) {
1251
+ const parsed = parseFirstSemver(version);
1252
+ if (!parsed) return false;
1253
+ return parsed.major === 0 && parsed.minor < 4;
1254
+ }
1255
+ function isLegacyWebpackVersion(version) {
1256
+ const parsed = parseFirstSemver(version);
1257
+ if (!parsed) return false;
1258
+ return parsed.major === 4;
1259
+ }
1170
1260
  var SUPPORTED_PATTERNS = [
1171
1261
  {
1172
1262
  tool: "vite",
@@ -1192,7 +1282,22 @@ var SUPPORTED_PATTERNS = [
1192
1282
  },
1193
1283
  {
1194
1284
  tool: "webpack",
1195
- files: ["webpack.config.js", "webpack.config.ts", "webpack.config.mjs", "webpack.config.cjs"],
1285
+ files: [
1286
+ "webpack.config.js",
1287
+ "webpack.config.ts",
1288
+ "webpack.config.mjs",
1289
+ "webpack.config.cjs",
1290
+ "webpack.config.common.js",
1291
+ "webpack.config.common.ts",
1292
+ "webpack.config.dev.js",
1293
+ "webpack.config.dev.ts",
1294
+ "webpack.config.prod.js",
1295
+ "webpack.config.prod.ts",
1296
+ "webpack.config.esbuild.js",
1297
+ "webpack.config.esbuild.ts",
1298
+ "webpack.config.build-pre.js",
1299
+ "webpack.config.build-pre.ts"
1300
+ ],
1196
1301
  label: "Webpack"
1197
1302
  },
1198
1303
  {
@@ -1337,6 +1442,89 @@ async function detectBuildTools(root, packagePaths) {
1337
1442
  }
1338
1443
  return { supported, unsupported: Array.from(unsupported) };
1339
1444
  }
1445
+ function rankScriptCommand(name, command) {
1446
+ const haystack = `${name} ${command}`.toLowerCase();
1447
+ let score = 0;
1448
+ if (/(^|[\s:_-])(start|dev|serve|watch)([\s:_-]|$)/.test(haystack)) score += 8;
1449
+ if (/(^|[\s:_-])(prod|build|release|stats)([\s:_-]|$)/.test(haystack)) score -= 3;
1450
+ if (/(^|[\s:_-])(dll|vendor)([\s:_-]|$)/.test(haystack)) score -= 6;
1451
+ if (haystack.includes("webpack-dev-server")) score += 3;
1452
+ if (haystack.includes("webpack")) score += 1;
1453
+ if (haystack.includes("rspack")) score += 1;
1454
+ return score;
1455
+ }
1456
+ function extractConfigArgs(scriptContent) {
1457
+ return Array.from(scriptContent.matchAll(/(?:-c|--config)\s+([^\s'"`;]+)/g)).map((match) => match[1]).filter((value) => Boolean(value));
1458
+ }
1459
+ async function resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate) {
1460
+ const normalizedCandidate = candidate.replace(/^['"`]|['"`]$/g, "");
1461
+ const normalizedRelativeCandidate = path7.normalize(normalizedCandidate);
1462
+ const possiblePaths = [];
1463
+ if (normalizedRelativeCandidate.startsWith("..")) {
1464
+ possiblePaths.push(
1465
+ path7.normalize(path7.join(path7.dirname(scriptPath), normalizedRelativeCandidate))
1466
+ );
1467
+ } else {
1468
+ possiblePaths.push(normalizedRelativeCandidate);
1469
+ possiblePaths.push(
1470
+ path7.normalize(path7.join(path7.dirname(scriptPath), normalizedRelativeCandidate))
1471
+ );
1472
+ }
1473
+ for (const possiblePath of possiblePaths) {
1474
+ if (await exists(path7.join(targetRoot, possiblePath))) {
1475
+ return possiblePath.split(path7.sep).join("/");
1476
+ }
1477
+ }
1478
+ return null;
1479
+ }
1480
+ async function resolveRspackConfigFromScript(targetRoot, scriptPath) {
1481
+ const scriptContent = await readFile(path7.join(targetRoot, scriptPath));
1482
+ if (!scriptContent) {
1483
+ return null;
1484
+ }
1485
+ for (const candidate of extractConfigArgs(scriptContent)) {
1486
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate);
1487
+ if (resolved) {
1488
+ return resolved;
1489
+ }
1490
+ }
1491
+ const matches = scriptContent.matchAll(
1492
+ /['"`]([^'"`\n]*rspack[^'"`\n]*config[^'"`\n]*\.(?:js|ts|mjs|cjs))['"`]/g
1493
+ );
1494
+ for (const match of matches) {
1495
+ const candidate = match[1];
1496
+ if (!candidate) continue;
1497
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, scriptPath, candidate);
1498
+ if (resolved) {
1499
+ return resolved;
1500
+ }
1501
+ }
1502
+ return null;
1503
+ }
1504
+ async function resolveWebpackBaseConfigFromFile(targetRoot, configPath) {
1505
+ const configContent = await readFile(path7.join(targetRoot, configPath));
1506
+ if (!configContent) {
1507
+ return null;
1508
+ }
1509
+ for (const candidate of extractConfigArgs(configContent)) {
1510
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, configPath, candidate);
1511
+ if (resolved) {
1512
+ return resolved;
1513
+ }
1514
+ }
1515
+ const matches = configContent.matchAll(
1516
+ /(?:configPath\s*=\s*|require\()\s*['"`]([^'"`\n]*webpack[^'"`\n]*config[^'"`\n]*\.(?:js|ts|mjs|cjs))['"`]/g
1517
+ );
1518
+ for (const match of matches) {
1519
+ const candidate = match[1];
1520
+ if (!candidate) continue;
1521
+ const resolved = await resolveScriptRelativeCandidate(targetRoot, configPath, candidate);
1522
+ if (resolved) {
1523
+ return resolved;
1524
+ }
1525
+ }
1526
+ return null;
1527
+ }
1340
1528
  async function detectPattern({
1341
1529
  pattern,
1342
1530
  workspaceRoot,
@@ -1376,13 +1564,38 @@ async function detectPattern({
1376
1564
  }
1377
1565
  }
1378
1566
  if (hasDep && !detectedFile && (pattern.tool === "esbuild" || pattern.tool === "rollup" || pattern.tool === "webpack" || pattern.tool === "rspack" || pattern.tool === "rsbuild")) {
1379
- for (const cmd of Object.values(scripts)) {
1567
+ const rankedScripts = Object.entries(scripts).sort(
1568
+ ([leftName, leftCommand], [rightName, rightCommand]) => rankScriptCommand(rightName, rightCommand) - rankScriptCommand(leftName, leftCommand)
1569
+ );
1570
+ for (const [, cmd] of rankedScripts) {
1571
+ if (pattern.tool === "webpack" || pattern.tool === "rspack") {
1572
+ for (const configArg of extractConfigArgs(cmd)) {
1573
+ const resolvedConfig = await resolveScriptRelativeCandidate(targetRoot, "", configArg);
1574
+ if (resolvedConfig && (cmd.includes(pattern.tool) || cmd.includes(`${pattern.tool}-`))) {
1575
+ if (pattern.tool === "webpack") {
1576
+ detectedFile = await resolveWebpackBaseConfigFromFile(targetRoot, resolvedConfig) ?? resolvedConfig;
1577
+ } else {
1578
+ detectedFile = await resolveRspackConfigFromScript(targetRoot, resolvedConfig) ?? resolvedConfig;
1579
+ }
1580
+ break;
1581
+ }
1582
+ }
1583
+ if (detectedFile) {
1584
+ break;
1585
+ }
1586
+ }
1380
1587
  if (cmd.includes("node ")) {
1381
1588
  const match = cmd.match(/node\s+([^\s]+\.(js|mjs|cjs|ts))/);
1382
1589
  if (match && match[1]) {
1383
1590
  if (await exists(path7.join(targetRoot, match[1]))) {
1384
1591
  if (cmd.includes(pattern.tool) || match[1].includes(pattern.tool)) {
1385
- detectedFile = match[1];
1592
+ if (pattern.tool === "rspack") {
1593
+ detectedFile = await resolveRspackConfigFromScript(targetRoot, match[1]) ?? match[1];
1594
+ } else if (pattern.tool === "webpack") {
1595
+ detectedFile = await resolveWebpackBaseConfigFromFile(targetRoot, match[1]) ?? match[1];
1596
+ } else {
1597
+ detectedFile = match[1];
1598
+ }
1386
1599
  break;
1387
1600
  }
1388
1601
  }
@@ -1419,13 +1632,11 @@ async function detectPattern({
1419
1632
  let isLegacyRspack = false;
1420
1633
  let isLegacyWebpack = false;
1421
1634
  if (pattern.tool === "rspack") {
1422
- const version = resolvedVersion;
1423
- if (version && (version.includes("0.3.") || version.includes("0.2.") || version.includes("0.1."))) {
1635
+ if (isLegacyRspackVersion(resolvedVersion)) {
1424
1636
  isLegacyRspack = true;
1425
1637
  }
1426
1638
  } else if (pattern.tool === "webpack") {
1427
- const version = resolvedVersion;
1428
- if (version && version.includes("^4") || version?.startsWith("4.")) {
1639
+ if (isLegacyWebpackVersion(resolvedVersion)) {
1429
1640
  isLegacyWebpack = true;
1430
1641
  }
1431
1642
  }
@@ -1737,6 +1948,28 @@ function buildToolBlockers(context) {
1737
1948
  }
1738
1949
  return [message("missing-build-tool", "No supported build tool detected")];
1739
1950
  }
1951
+ function buildToolWarnings(context) {
1952
+ const warnings = [];
1953
+ if (context.buildTools.supported.length === 1) {
1954
+ const buildTool = context.buildTools.supported[0];
1955
+ if (buildTool.tool === "rspack" && buildTool.isLegacyRspack) {
1956
+ warnings.push(
1957
+ message(
1958
+ "legacy-rspack-requires-manual-config",
1959
+ `Legacy Rspack detected at ${buildTool.configPath}. Inspecto must use the legacy Rspack plugin entry and manual config steps.`
1960
+ )
1961
+ );
1962
+ } else if (buildTool.tool === "webpack" && buildTool.isLegacyWebpack) {
1963
+ warnings.push(
1964
+ message(
1965
+ "legacy-webpack4-requires-manual-config",
1966
+ `Webpack 4 detected at ${buildTool.configPath}. Inspecto must use the legacy Webpack 4 plugin entry and manual config steps.`
1967
+ )
1968
+ );
1969
+ }
1970
+ }
1971
+ return warnings;
1972
+ }
1740
1973
  function frameworkBlockers(context) {
1741
1974
  if (context.frameworks.supported.length > 0) {
1742
1975
  return [];
@@ -1798,6 +2031,35 @@ function manualBuildToolActions(context) {
1798
2031
  }
1799
2032
  ];
1800
2033
  }
2034
+ const buildTool = context.buildTools.supported[0];
2035
+ if (buildTool?.tool === "rspack" && buildTool.isLegacyRspack) {
2036
+ return [
2037
+ {
2038
+ type: "install_dependency",
2039
+ target: "@inspecto-dev/plugin @inspecto-dev/core",
2040
+ description: `Install the Inspecto runtime packages with ${context.packageManager}.`
2041
+ },
2042
+ {
2043
+ type: "manual_step",
2044
+ target: buildTool.configPath,
2045
+ description: `Update ${buildTool.configPath} to import \`rspackPlugin\` from \`@inspecto-dev/plugin/legacy/rspack\` and add it to the Rspack plugins array.`
2046
+ }
2047
+ ];
2048
+ }
2049
+ if (buildTool?.tool === "webpack" && buildTool.isLegacyWebpack) {
2050
+ return [
2051
+ {
2052
+ type: "install_dependency",
2053
+ target: "@inspecto-dev/plugin @inspecto-dev/core",
2054
+ description: `Install the Inspecto runtime packages with ${context.packageManager}.`
2055
+ },
2056
+ {
2057
+ type: "manual_step",
2058
+ target: buildTool.configPath,
2059
+ description: `Update ${buildTool.configPath} to import \`webpackPlugin\` from \`@inspecto-dev/plugin/legacy/webpack4\` and add it to the Webpack plugins array.`
2060
+ }
2061
+ ];
2062
+ }
1801
2063
  return [
1802
2064
  {
1803
2065
  type: "manual_step",
@@ -1826,7 +2088,10 @@ function manualFrameworkActions(context) {
1826
2088
  }
1827
2089
  async function createDetectionResult(root) {
1828
2090
  const context = await buildOnboardingContext(root);
1829
- const warnings = uniqueMessages([...unsupportedEnvironmentWarnings(context)]);
2091
+ const warnings = uniqueMessages([
2092
+ ...unsupportedEnvironmentWarnings(context),
2093
+ ...buildToolWarnings(context)
2094
+ ]);
1830
2095
  const buildToolResult = buildToolBlockers(context);
1831
2096
  const frameworkResult = frameworkBlockers(context);
1832
2097
  const blockers = uniqueMessages([...buildToolResult, ...frameworkResult]);
@@ -1849,13 +2114,22 @@ async function createDetectionResult(root) {
1849
2114
  };
1850
2115
  }
1851
2116
  function createPlanResult(context) {
1852
- const warnings = uniqueMessages(unsupportedEnvironmentWarnings(context));
2117
+ const warnings = uniqueMessages([
2118
+ ...unsupportedEnvironmentWarnings(context),
2119
+ ...buildToolWarnings(context)
2120
+ ]);
1853
2121
  const blockers = uniqueMessages([...buildToolBlockers(context), ...frameworkBlockers(context)]);
1854
2122
  const actions = [];
1855
2123
  let strategy = "supported";
1856
- if (blockers.length > 0) {
2124
+ const hasLegacyRspackManualPlan = context.buildTools.supported.length === 1 && context.buildTools.supported[0]?.tool === "rspack" && context.buildTools.supported[0]?.isLegacyRspack;
2125
+ const hasLegacyWebpackManualPlan = context.buildTools.supported.length === 1 && context.buildTools.supported[0]?.tool === "webpack" && context.buildTools.supported[0]?.isLegacyWebpack;
2126
+ if (blockers.length > 0 || hasLegacyRspackManualPlan || hasLegacyWebpackManualPlan) {
1857
2127
  strategy = "manual";
1858
- if (context.buildTools.unsupported.length > 0 || context.buildTools.supported.length === 0 || context.buildTools.supported.length > 1) {
2128
+ if (context.buildTools.unsupported.length > 0 || context.buildTools.supported.length === 0 || context.buildTools.supported.length > 1 || context.buildTools.supported.some(
2129
+ (buildTool) => buildTool.tool === "rspack" && buildTool.isLegacyRspack
2130
+ ) || context.buildTools.supported.some(
2131
+ (buildTool) => buildTool.tool === "webpack" && buildTool.isLegacyWebpack
2132
+ )) {
1859
2133
  actions.push(...manualBuildToolActions(context));
1860
2134
  }
1861
2135
  if (frameworkBlockers(context).length > 0) {
@@ -2040,10 +2314,26 @@ async function detect(json = false) {
2040
2314
  import path11 from "path";
2041
2315
 
2042
2316
  // src/onboarding/target-resolution.ts
2317
+ function buildCandidateId(candidate) {
2318
+ return [candidate.packagePath || ".", candidate.buildTool, candidate.configPath].join(":");
2319
+ }
2043
2320
  function normalizePackagePath(packagePath) {
2044
2321
  if (!packagePath || packagePath === ".") return "";
2045
2322
  return packagePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2046
2323
  }
2324
+ function normalizeTargetValue(target) {
2325
+ if (!target) return "";
2326
+ return target.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
2327
+ }
2328
+ function buildSelectionPurpose() {
2329
+ return "Choose the build target that runs your local development build so Inspecto can attach the right plugin and settings.";
2330
+ }
2331
+ function buildSelectionInstructions(hasCandidates) {
2332
+ if (!hasCandidates) {
2333
+ return "If auto-detection missed your build entrypoint, rerun with --target <configPath> using the config file or wrapper script your dev command actually starts.";
2334
+ }
2335
+ return "Rerun with --target <candidateId> using one returned candidateId. The CLI also accepts an exact configPath from the candidate list as a compatibility fallback.";
2336
+ }
2047
2337
  function looksLikeAppPackage(packagePath) {
2048
2338
  if (!packagePath) return true;
2049
2339
  return /(^|\/)(app|apps|web|client|frontend|site)(\/|$)/i.test(packagePath);
@@ -2054,13 +2344,19 @@ function looksLikeAuxiliaryPackage(packagePath) {
2054
2344
  function buildCandidates(input) {
2055
2345
  return input.buildTools.map((buildTool) => {
2056
2346
  const packagePath = normalizePackagePath(buildTool.packagePath);
2057
- return {
2347
+ const candidate = {
2058
2348
  packagePath,
2059
2349
  configPath: buildTool.configPath,
2350
+ label: buildTool.label,
2060
2351
  buildTool: buildTool.tool,
2352
+ isLegacyRspack: buildTool.isLegacyRspack,
2353
+ isLegacyWebpack: buildTool.isLegacyWebpack,
2061
2354
  frameworks: input.frameworkSupportByPackage[packagePath] ?? [],
2062
2355
  automaticInjection: true
2063
2356
  };
2357
+ candidate.id = buildCandidateId(candidate);
2358
+ candidate.candidateId = candidate.id;
2359
+ return candidate;
2064
2360
  });
2065
2361
  }
2066
2362
  function rankCandidate(candidate) {
@@ -2083,18 +2379,52 @@ function resolveOnboardingTarget(input) {
2083
2379
  return {
2084
2380
  status: "needs_selection",
2085
2381
  candidates,
2086
- reason: "No supported targets were detected."
2382
+ reason: "No supported targets were detected.",
2383
+ selectionPurpose: buildSelectionPurpose(),
2384
+ selectionInstructions: buildSelectionInstructions(false)
2087
2385
  };
2088
2386
  }
2089
2387
  const explicitlySelected = normalizePackagePath(input.selectedPackagePath);
2388
+ const explicitlySelectedValue = normalizeTargetValue(input.selectedPackagePath);
2090
2389
  if (input.selectedPackagePath !== void 0) {
2091
- const selected = candidates.find((candidate) => candidate.packagePath === explicitlySelected);
2390
+ const selectedById = candidates.find(
2391
+ (candidate) => candidate.id === input.selectedPackagePath || candidate.candidateId === input.selectedPackagePath
2392
+ );
2393
+ if (selectedById) {
2394
+ return {
2395
+ status: "resolved",
2396
+ selected: selectedById,
2397
+ candidates,
2398
+ reason: `Using the explicitly selected target: ${selectedById.configPath}.`,
2399
+ selectionPurpose: buildSelectionPurpose(),
2400
+ selectionInstructions: buildSelectionInstructions(true)
2401
+ };
2402
+ }
2403
+ const selectedByConfigPath = candidates.find(
2404
+ (candidate) => normalizeTargetValue(candidate.configPath) === explicitlySelectedValue
2405
+ );
2406
+ if (selectedByConfigPath) {
2407
+ return {
2408
+ status: "resolved",
2409
+ selected: selectedByConfigPath,
2410
+ candidates,
2411
+ reason: `Using the explicitly selected config path: ${selectedByConfigPath.configPath}.`,
2412
+ selectionPurpose: buildSelectionPurpose(),
2413
+ selectionInstructions: buildSelectionInstructions(true)
2414
+ };
2415
+ }
2416
+ const matchingPackageCandidates = candidates.filter(
2417
+ (candidate) => candidate.packagePath === explicitlySelected
2418
+ );
2419
+ const selected = matchingPackageCandidates.length === 1 ? matchingPackageCandidates[0] : void 0;
2092
2420
  if (selected) {
2093
2421
  return {
2094
2422
  status: "resolved",
2095
2423
  selected,
2096
2424
  candidates,
2097
- reason: `Using the explicitly selected target: ${selected.packagePath || "."}.`
2425
+ reason: `Using the explicitly selected target: ${selected.packagePath || "."}.`,
2426
+ selectionPurpose: buildSelectionPurpose(),
2427
+ selectionInstructions: buildSelectionInstructions(true)
2098
2428
  };
2099
2429
  }
2100
2430
  }
@@ -2103,7 +2433,9 @@ function resolveOnboardingTarget(input) {
2103
2433
  status: "resolved",
2104
2434
  selected: candidates[0],
2105
2435
  candidates,
2106
- reason: "Only one supported target was detected."
2436
+ reason: "Only one supported target was detected.",
2437
+ selectionPurpose: buildSelectionPurpose(),
2438
+ selectionInstructions: buildSelectionInstructions(true)
2107
2439
  };
2108
2440
  }
2109
2441
  const ranked = rankCandidates(candidates);
@@ -2111,14 +2443,18 @@ function resolveOnboardingTarget(input) {
2111
2443
  return {
2112
2444
  status: "needs_selection",
2113
2445
  candidates,
2114
- reason: "Multiple supported targets look equally plausible."
2446
+ reason: "Multiple supported targets look equally plausible.",
2447
+ selectionPurpose: buildSelectionPurpose(),
2448
+ selectionInstructions: buildSelectionInstructions(true)
2115
2449
  };
2116
2450
  }
2117
2451
  return {
2118
2452
  status: "resolved",
2119
2453
  selected: ranked[0].candidate,
2120
2454
  candidates,
2121
- reason: `Preselected ${ranked[0].candidate.packagePath || "."} because it has the strongest supported app signal.`
2455
+ reason: `Preselected ${ranked[0].candidate.packagePath || "."} because it has the strongest supported app signal.`,
2456
+ selectionPurpose: buildSelectionPurpose(),
2457
+ selectionInstructions: buildSelectionInstructions(true)
2122
2458
  };
2123
2459
  }
2124
2460
 
@@ -2819,6 +3155,25 @@ async function collectDoctorResult(root = process.cwd()) {
2819
3155
  const settings = await readJSON(targetPath);
2820
3156
  if (settings) {
2821
3157
  checks.push(createDiagnostic("settings-valid", "ok", `.inspecto/${fileName} valid`));
3158
+ const configuredIde = typeof settings.ide === "string" ? settings.ide : void 0;
3159
+ const detectedIdeCandidates = ideProbe.detected.map((item) => item.ide);
3160
+ if (configuredIde && detectedIdeCandidates.length > 0 && !detectedIdeCandidates.includes(configuredIde)) {
3161
+ checks.push(
3162
+ createDiagnostic(
3163
+ "settings-ide-mismatch",
3164
+ "warning",
3165
+ `.inspecto/${fileName} sets ide=${configuredIde}, but the current environment looks like ${detectedIdeCandidates.join(", ")}. Inspecto will use the configured IDE from ${fileName}.`,
3166
+ [
3167
+ `Update .inspecto/${fileName} if you want Inspecto to target the currently detected IDE instead.`
3168
+ ],
3169
+ {
3170
+ configuredIde,
3171
+ detectedIdeCandidates,
3172
+ precedence: `configured ide from ${fileName}`
3173
+ }
3174
+ )
3175
+ );
3176
+ }
2822
3177
  } else {
2823
3178
  checks.push(
2824
3179
  createDiagnostic(
@@ -2954,7 +3309,8 @@ async function detectFrameworkSupportByPackage2(repoRoot, context) {
2954
3309
  );
2955
3310
  return supportByPackage;
2956
3311
  }
2957
- async function buildTargetedContext(rootContext, packagePath) {
3312
+ async function buildTargetedContext(rootContext, target) {
3313
+ const packagePath = normalizePackagePath2(target.packagePath);
2958
3314
  const projectRoot = packagePath ? path13.join(rootContext.root, packagePath) : rootContext.root;
2959
3315
  const [frameworks, ides, providers] = await Promise.all([
2960
3316
  detectFrameworks(projectRoot),
@@ -2966,7 +3322,11 @@ async function buildTargetedContext(rootContext, packagePath) {
2966
3322
  packageManager: rootContext.packageManager,
2967
3323
  buildTools: {
2968
3324
  supported: rootContext.buildTools.supported.filter((item) => {
2969
- return normalizePackagePath2(item.packagePath) === packagePath;
3325
+ const itemPackagePath = normalizePackagePath2(item.packagePath);
3326
+ if (target.id) {
3327
+ return `${itemPackagePath || "."}:${item.tool}:${item.configPath}` === target.id;
3328
+ }
3329
+ return itemPackagePath === packagePath;
2970
3330
  }),
2971
3331
  unsupported: []
2972
3332
  },
@@ -3102,7 +3462,7 @@ async function resolveOnboardingSession(root, options = {}) {
3102
3462
  if (target.status === "needs_selection") {
3103
3463
  const plan3 = createPlanResult(rootContext);
3104
3464
  const summary2 = {
3105
- headline: "Inspecto found multiple plausible app targets and needs one selection.",
3465
+ headline: "Inspecto needs one build target selection before setup so it knows which local dev build should receive the plugin and settings.",
3106
3466
  changes: [],
3107
3467
  risks: [],
3108
3468
  manualFollowUp: []
@@ -3118,8 +3478,7 @@ async function resolveOnboardingSession(root, options = {}) {
3118
3478
  projectRoot: root
3119
3479
  };
3120
3480
  }
3121
- const packagePath = normalizePackagePath2(target.selected?.packagePath);
3122
- const context = await buildTargetedContext(rootContext, packagePath);
3481
+ const context = await buildTargetedContext(rootContext, target.selected);
3123
3482
  const verification = await buildVerification(context.root, context.packageManager);
3124
3483
  const plan2 = createPlanResult(context);
3125
3484
  const summary = buildOnboardingSummary(plan2, context.root);
@@ -3172,7 +3531,8 @@ async function applyResolvedOnboardingSession(session, options = {}) {
3172
3531
  },
3173
3532
  selectedIDE: session.selectedIDE,
3174
3533
  providerDefault: session.providerDefault,
3175
- plan: session.plan
3534
+ plan: session.plan,
3535
+ allowManualPlanApply: session.plan.strategy === "manual" && session.plan.blockers.length === 0
3176
3536
  });
3177
3537
  const diagnostics = buildExecutionDiagnostics(session, applyResult);
3178
3538
  const status = applyResult.postInstall.installFailed && session.context.buildTools.supported.length === 0 ? "error" : diagnostics?.nextSteps.length || diagnostics?.errors.length || diagnostics?.warnings.length ? "partial_success" : "success";
@@ -3215,6 +3575,19 @@ function printOnboardResult(result) {
3215
3575
  log.header("Inspecto Onboard");
3216
3576
  log.info(`Status: ${result.status}`);
3217
3577
  log.info(result.summary.headline);
3578
+ if (result.status === "needs_target_selection") {
3579
+ if (result.target.selectionPurpose) {
3580
+ log.warn(result.target.selectionPurpose);
3581
+ }
3582
+ for (const candidate of result.target.candidates) {
3583
+ const identifier = candidate.candidateId ?? candidate.id ?? candidate.configPath;
3584
+ const label = candidate.label ?? candidate.configPath;
3585
+ log.hint(`${identifier}: ${label}`);
3586
+ }
3587
+ if (result.target.selectionInstructions) {
3588
+ log.hint(result.target.selectionInstructions);
3589
+ }
3590
+ }
3218
3591
  for (const change of result.summary.changes) {
3219
3592
  log.hint(change);
3220
3593
  }
@@ -3889,6 +4262,10 @@ function getDispatchModeLabel(assistant, ide, mode) {
3889
4262
  }
3890
4263
 
3891
4264
  // src/commands/integration-install.ts
4265
+ import {
4266
+ DEFAULT_PROVIDER_MODE,
4267
+ VALID_MODES
4268
+ } from "@inspecto-dev/types";
3892
4269
  var REPO_RAW_BASE = "https://raw.githubusercontent.com/inspecto-dev/inspecto/main";
3893
4270
  var TOTAL_STEPS2 = 6;
3894
4271
  var INTEGRATION_MANIFESTS = [
@@ -3937,7 +4314,7 @@ var INTEGRATION_MANIFESTS = [
3937
4314
  {
3938
4315
  assistant: "coco",
3939
4316
  type: "native-skill",
3940
- installTarget: ".traecli/skills/inspecto-onboarding/",
4317
+ installTarget: ".trae/skills/inspecto-onboarding/",
3941
4318
  preferredInstall: "npx @inspecto-dev/cli integrations install coco --host-ide <vscode|cursor|trae|trae-cn>",
3942
4319
  cliSupported: true
3943
4320
  }
@@ -3996,6 +4373,9 @@ ${content}`;
3996
4373
  }
3997
4374
  }
3998
4375
  }
4376
+ if (shouldPersistProjectOnboardingDefaults(options)) {
4377
+ await persistProjectOnboardingDefaults(assistant, options);
4378
+ }
3999
4379
  const stepOneMessage = options.preview ? formatIntegrationStep2(1, `Previewing ${getAssistantLabel2(assistant)} integration assets`) : formatIntegrationStep2(1, `Installed ${getAssistantLabel2(assistant)} integration assets`);
4000
4380
  if (!silent) {
4001
4381
  if (options.preview) {
@@ -4081,6 +4461,46 @@ ${content}`;
4081
4461
  }
4082
4462
  return result;
4083
4463
  }
4464
+ function shouldPersistProjectOnboardingDefaults(options) {
4465
+ return !options.preview && !shouldSkipAutomationForInstall(options) && isSupportedHostIde(options.ide);
4466
+ }
4467
+ function isProviderAssistant(value) {
4468
+ return Object.prototype.hasOwnProperty.call(VALID_MODES, value);
4469
+ }
4470
+ async function resolveProviderDefaultForAssistant(assistant, ide) {
4471
+ if (!isProviderAssistant(assistant)) {
4472
+ return void 0;
4473
+ }
4474
+ let mode;
4475
+ if (assistant === "codex" || assistant === "claude-code" || assistant === "gemini") {
4476
+ const dispatchMode = await resolveIntegrationDispatchMode({ assistant, hostIde: ide });
4477
+ mode = dispatchMode.mode ?? DEFAULT_PROVIDER_MODE[assistant];
4478
+ } else {
4479
+ mode = DEFAULT_PROVIDER_MODE[assistant];
4480
+ }
4481
+ if (!mode || !VALID_MODES[assistant].includes(mode)) {
4482
+ return void 0;
4483
+ }
4484
+ return `${assistant}.${mode}`;
4485
+ }
4486
+ async function persistProjectOnboardingDefaults(assistant, options) {
4487
+ const settingsPath = path17.join(process.cwd(), ".inspecto", "settings.local.json");
4488
+ const existingSettings = await readJSON(settingsPath);
4489
+ const resolvedHostIde = await resolveIntegrationHostIde({
4490
+ explicitIde: options.ide,
4491
+ cwd: process.cwd()
4492
+ });
4493
+ const providerDefault = resolvedHostIde.ide && resolvedHostIde.confidence !== "low" ? await resolveProviderDefaultForAssistant(assistant, resolvedHostIde.ide) : void 0;
4494
+ const mergedSettings = existingSettings && typeof existingSettings === "object" ? {
4495
+ ...existingSettings,
4496
+ ide: options.ide,
4497
+ ...providerDefault ? { "provider.default": providerDefault } : {}
4498
+ } : {
4499
+ ide: options.ide,
4500
+ ...providerDefault ? { "provider.default": providerDefault } : {}
4501
+ };
4502
+ await writeJSON(settingsPath, mergedSettings);
4503
+ }
4084
4504
  function shouldSkipAutomationForInstall(options) {
4085
4505
  return options.scope === "user" && !options.preview;
4086
4506
  }
@@ -4143,6 +4563,12 @@ function resolveInstallPlan(assistant, options) {
4143
4563
  source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/SKILL.md`,
4144
4564
  target: ".trae/skills/inspecto-onboarding/SKILL.md",
4145
4565
  localSource: "skills/inspecto-onboarding-trae/SKILL.md"
4566
+ },
4567
+ {
4568
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/scripts/run-inspecto.sh`,
4569
+ target: ".trae/skills/inspecto-onboarding/scripts/run-inspecto.sh",
4570
+ localSource: "skills/inspecto-onboarding-trae/scripts/run-inspecto.sh",
4571
+ executable: true
4146
4572
  }
4147
4573
  ],
4148
4574
  successMessage: "Installed Trae skill to .trae/skills/inspecto-onboarding/SKILL.md",
@@ -4153,11 +4579,17 @@ function resolveInstallPlan(assistant, options) {
4153
4579
  assets: [
4154
4580
  {
4155
4581
  source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/SKILL.md`,
4156
- target: ".traecli/skills/inspecto-onboarding/SKILL.md",
4582
+ target: ".trae/skills/inspecto-onboarding/SKILL.md",
4157
4583
  localSource: "skills/inspecto-onboarding-trae/SKILL.md"
4584
+ },
4585
+ {
4586
+ source: `${REPO_RAW_BASE}/skills/inspecto-onboarding-trae/scripts/run-inspecto.sh`,
4587
+ target: ".trae/skills/inspecto-onboarding/scripts/run-inspecto.sh",
4588
+ localSource: "skills/inspecto-onboarding-trae/scripts/run-inspecto.sh",
4589
+ executable: true
4158
4590
  }
4159
4591
  ],
4160
- successMessage: "Installed Coco skill to .traecli/skills/inspecto-onboarding/SKILL.md",
4592
+ successMessage: "Installed Coco skill to .trae/skills/inspecto-onboarding/SKILL.md",
4161
4593
  nextStep: "Start a new Coco session."
4162
4594
  };
4163
4595
  default: