neon 8.14.0 → 8.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +4 -2
  2. package/dist/_chunks/{auth-DPSDYYtG.js → auth-rup6nemJ.js} +38 -1
  3. package/dist/_chunks/{checkout-C6VMMgY6.js → checkout-BpOcFaAi.js} +2 -2
  4. package/dist/_chunks/{config-7SIE_C5O.js → config-BjCm5cfr.js} +56 -4
  5. package/dist/_chunks/{deploy-DmKZcAnR.js → deploy-DmEEhux0.js} +1 -1
  6. package/dist/_chunks/{dev-Dv0HKcFF.js → dev-DlUJFzRs.js} +1 -1
  7. package/dist/_chunks/{docs-CClmgj_Z.js → docs-BpLiSTmm.js} +1 -1
  8. package/dist/_chunks/{git-Dxw9w9dK.js → git-j6xSVo3e.js} +2 -2
  9. package/dist/_chunks/{init-BOOGv-mD.js → init-CcVJb492.js} +1 -1
  10. package/dist/_chunks/{link-CcwTXRny.js → link-nAiuWKfg.js} +1 -1
  11. package/dist/_chunks/{plugins-CKOcBuuP.js → plugins-B2Gym8Lw.js} +2 -2
  12. package/dist/_chunks/{profile-H0d7gFrX.js → profile-BAlM625e.js} +1 -1
  13. package/dist/_chunks/{status-DS_yUXFo.js → status-BC4XXuuE.js} +1 -1
  14. package/dist/analytics.js +4 -2
  15. package/dist/api.js +2 -2
  16. package/dist/auth_recovery.js +1 -1
  17. package/dist/commands/auth.js +1 -1
  18. package/dist/commands/checkout.js +1 -1
  19. package/dist/commands/config.js +1 -1
  20. package/dist/commands/deploy.js +1 -1
  21. package/dist/commands/dev.js +1 -1
  22. package/dist/commands/docs.js +1 -1
  23. package/dist/commands/functions_new.js +3 -4
  24. package/dist/commands/git.js +1 -1
  25. package/dist/commands/index.js +12 -12
  26. package/dist/commands/init.js +1 -1
  27. package/dist/commands/link.js +1 -1
  28. package/dist/commands/plugins.js +1 -1
  29. package/dist/commands/profile.js +1 -1
  30. package/dist/commands/status.js +1 -1
  31. package/dist/config_format.js +9 -1
  32. package/dist/config_template.js +36 -9
  33. package/dist/env.js +19 -4
  34. package/dist/index.js +1 -1
  35. package/dist/init/auth.js +23 -2
  36. package/dist/init/copy.js +3 -2
  37. package/dist/init/link.js +2 -2
  38. package/dist/init/run.js +49 -18
  39. package/dist/init/tooling.js +1 -1
  40. package/dist/init/wizard.js +5 -5
  41. package/dist/plugins/child_env.js +10 -0
  42. package/dist/plugins/plan.js +14 -14
  43. package/dist/plugins/run.js +2 -8
  44. package/dist/plugins/targets.js +129 -11
  45. package/dist/update_notifier.js +7 -4
  46. package/dist/utils/service_picker.js +1 -1
  47. package/package.json +6 -6
package/dist/env.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isatty } from "node:tty";
1
2
  // src/env.ts
2
3
  const isCi = () => {
3
4
  return process.env.CI !== "false" && Boolean(process.env.CI);
@@ -6,16 +7,30 @@ const isInteractiveTerminal = () => !isCi() && Boolean(process.stdin.isTTY) && B
6
7
  const isDebug = () => {
7
8
  return Boolean(process.env.DEBUG);
8
9
  };
10
+ const getTty = () => ({
11
+ stdin: isatty(0),
12
+ stdout: isatty(1)
13
+ });
14
+ const AI_AGENT_NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
15
+ const isNonEmpty = (value) => Boolean(value?.trim());
9
16
  const getCliAgent = (env) => {
10
17
  const active = [
11
18
  ["claude-code", env.CLAUDE_CODE_CHILD_SESSION === "1"],
12
19
  ["codex", env.CODEX_CI === "1" && (Boolean(env.CODEX_THREAD_ID?.trim()) || Boolean(env.CODEX_SESSION_ID?.trim()))],
13
20
  ["cursor", env.CURSOR_AGENT === "1"],
14
- ["opencode", env.OPENCODE === "1"],
21
+ ["opencode", env.OPENCODE === "1" && env.KILO !== "1"],
22
+ ["kilo", env.KILO === "1"],
15
23
  ["gemini-cli", env.GEMINI_CLI === "1"],
16
- ["github-copilot-cli", env.COPILOT_CLI === "1"]
24
+ ["qwen-code", env.QWEN_CODE === "1"],
25
+ ["github-copilot-cli", env.COPILOT_CLI === "1"],
26
+ ["antigravity", isNonEmpty(env.ANTIGRAVITY_AGENT)],
27
+ ["amp", isNonEmpty(env.AMP_CURRENT_THREAD_ID)],
28
+ ["cline", env.CLINE_ACTIVE === "true"],
29
+ ["openclaw", env.OPENCLAW_SHELL === "exec"]
17
30
  ].filter(([, present]) => present);
18
- return active.length === 1 ? active[0][0] : void 0;
31
+ if (active.length > 0) return active.length === 1 ? active[0][0] : void 0;
32
+ const declared = env.AI_AGENT?.trim().toLowerCase();
33
+ return declared && AI_AGENT_NAME.test(declared) ? declared : void 0;
19
34
  };
20
35
  const getGithubEnvVars = (env) => {
21
36
  const vars = [
@@ -38,4 +53,4 @@ const getGithubEnvVars = (env) => {
38
53
  });
39
54
  return Object.fromEntries(map);
40
55
  };
41
- export { getCliAgent, getGithubEnvVars, isCi, isDebug, isInteractiveTerminal };
56
+ export { getCliAgent, getGithubEnvVars, getTty, isCi, isDebug, isInteractiveTerminal };
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.
9
9
  import { analyticsMiddleware, closeAnalytics, initAnalyticsClientMiddleware, isAnalyticsActive, recordCommandSuccessExtras, sendError, takeCommandSuccessExtras, trackCommandSuccess } from "./analytics.js";
10
10
  import { AuthRefreshError, defaultClientID } from "./auth.js";
11
11
  import { getCliName } from "./utils/cli_name.js";
12
- import { c as ensureAuth } from "./_chunks/auth-DPSDYYtG.js";
12
+ import { c as ensureAuth } from "./_chunks/auth-rup6nemJ.js";
13
13
  import { recoverFrom401 } from "./auth_recovery.js";
14
14
  import { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageErrorHelp } from "./help.js";
15
15
  import { lazyCommands, preloadCommand } from "./commands/manifest.js";
package/dist/init/auth.js CHANGED
@@ -2,7 +2,7 @@ import { isNeonApiError } from "../api.js";
2
2
  import { r as defaultDir } from "../_chunks/paths-B55ZfOg1.js";
3
3
  import "../config.js";
4
4
  import "../auth.js";
5
- import { c as ensureAuth } from "../_chunks/auth-DPSDYYtG.js";
5
+ import { c as ensureAuth, l as existingApiClient } from "../_chunks/auth-rup6nemJ.js";
6
6
  import { recoverFrom401 } from "../auth_recovery.js";
7
7
  import { c as pull } from "../_chunks/env-BOt9AZ1j.js";
8
8
  import { c as setupNeonMcp } from "../_chunks/mcp-BznTRK3w.js";
@@ -54,6 +54,27 @@ const runAuthenticatedMcp = async (options) => {
54
54
  }
55
55
  };
56
56
  /**
57
+ * The client and key `neon config init` may read the linked branch with. Init calls this only
58
+ * after a link is verified, but a pin that was already in place can have no credentials on
59
+ * disk, so this never starts a login: with nothing usable it returns an empty result and the
60
+ * scaffold declares the Neon Auth default with a warning, as a failed read does.
61
+ */
62
+ const resolveInitConfigApi = async (options) => {
63
+ const access = await existingApiClient({
64
+ configDir: options.configDir ?? defaultDir,
65
+ apiHost: options.apiHost ?? "https://console.neon.tech/api/v2",
66
+ ...options.profile !== void 0 ? { profile: options.profile } : {},
67
+ ...options.oauthHost !== void 0 ? { oauthHost: options.oauthHost } : {},
68
+ ...options.clientId !== void 0 ? { clientId: options.clientId } : {}
69
+ });
70
+ if (access === null) return {};
71
+ return {
72
+ apiClient: access.apiClient,
73
+ apiKey: access.apiKey,
74
+ apiHost: options.apiHost
75
+ };
76
+ };
77
+ /**
57
78
  * Resolves Neon auth the same way a standalone `neon env pull` invocation would, then runs
58
79
  * `pull` — the same function `link`/`checkout` already call directly via
59
80
  * `autoPullEnvAfterPin` (see `commands/env.ts`).
@@ -77,4 +98,4 @@ const pullInitEnv = async (props) => {
77
98
  throw authenticationFailed();
78
99
  }
79
100
  };
80
- export { pullInitEnv, runAuthenticatedMcp };
101
+ export { pullInitEnv, resolveInitConfigApi, runAuthenticatedMcp };
package/dist/init/copy.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { getCliName } from "../utils/cli_name.js";
2
+ import { pluginsCommandProblemText } from "../plugins/targets.js";
2
3
  // src/init/copy.ts
3
4
  const INIT_SUBTITLE = "Set up coding agents and this directory for Neon.";
4
5
  const MODE_MESSAGE = "How would you like to set up Neon?";
@@ -16,7 +17,7 @@ const AGENT_SETUP_SKIP_DESCRIPTION = "Continue to project setup without changing
16
17
  const AGENT_PICKER_MESSAGE = "Which coding agents should receive Neon tooling? (space to toggle, enter to confirm)";
17
18
  const AGENT_DETECTED_DESCRIPTION = "Detected on this machine or in this directory.";
18
19
  const AGENT_OTHER_DESCRIPTION = "Install for this agent.";
19
- const agentMissingCommandDescription = (command) => `Detected, but "${command}" was not found on PATH. Its plugin cannot install.`;
20
+ const agentUnusableCommandDescription = (row) => `Detected, but "${row.command.name}" ${pluginsCommandProblemText(row)}. Its plugin cannot install.`;
20
21
  const AGENT_PLUGIN_SKIP_MESSAGE = "No agents selected. Continue without installing the Neon plugin?";
21
22
  const AGENT_SKILLS_PICKER_MESSAGE = "Which coding agents should receive Neon skills? (space to toggle, enter to confirm)";
22
23
  const AGENT_MCP_PICKER_MESSAGE = "Which coding agents should receive the Neon MCP server? (space to toggle, enter to confirm)";
@@ -184,4 +185,4 @@ const headingForOutcome = (outcome) => {
184
185
  default: return outcome;
185
186
  }
186
187
  };
187
- export { AGENTS_NOT_INSTALLED, AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, HEADING_CANCELLED, HEADING_COMPLETE, HEADING_FAILED, HEADING_PENDING, INIT_NEEDS_YES, INIT_SUBTITLE, MCP_API_KEY_NEEDS_AUTH, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_OAUTH_FALLBACK, MCP_SCOPED_NEEDS_PROJECT, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, NON_TTY_AGENT_SETUP, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, NO_AGENT_SETUP_CONFLICT, PACKAGE_MANAGER_MESSAGE, PROGRESS, PROJECT_PINNED_OFFLINE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, agentMissingCommandDescription, claimableNext, claimableThenServicesNext, configInitNext, envPullFailedNext, existingConfigNext, headingForOutcome, installFailedNext, mcpAfterLinkNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, mcpKeyFailedNext, namedAgentsUnavailable, newConfigNext, offlinePinNext, skippedLinkNext, toolingFailedNext, unattendedUnauthedNext };
188
+ export { AGENTS_NOT_INSTALLED, AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, HEADING_CANCELLED, HEADING_COMPLETE, HEADING_FAILED, HEADING_PENDING, INIT_NEEDS_YES, INIT_SUBTITLE, MCP_API_KEY_NEEDS_AUTH, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_OAUTH_FALLBACK, MCP_SCOPED_NEEDS_PROJECT, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, NON_TTY_AGENT_SETUP, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, NO_AGENT_SETUP_CONFLICT, PACKAGE_MANAGER_MESSAGE, PROGRESS, PROJECT_PINNED_OFFLINE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, agentUnusableCommandDescription, claimableNext, claimableThenServicesNext, configInitNext, envPullFailedNext, existingConfigNext, headingForOutcome, installFailedNext, mcpAfterLinkNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, mcpKeyFailedNext, namedAgentsUnavailable, newConfigNext, offlinePinNext, skippedLinkNext, toolingFailedNext, unattendedUnauthedNext };
package/dist/init/link.js CHANGED
@@ -2,9 +2,9 @@ import { isNeonApiError } from "../api.js";
2
2
  import { r as defaultDir } from "../_chunks/paths-B55ZfOg1.js";
3
3
  import "../config.js";
4
4
  import "../auth.js";
5
- import { c as ensureAuth } from "../_chunks/auth-DPSDYYtG.js";
5
+ import { c as ensureAuth } from "../_chunks/auth-rup6nemJ.js";
6
6
  import { recoverFrom401 } from "../auth_recovery.js";
7
- import { s as runLink } from "../_chunks/link-CcwTXRny.js";
7
+ import { s as runLink } from "../_chunks/link-nAiuWKfg.js";
8
8
  // src/init/link.ts
9
9
  const isUnauthorized = (error) => isNeonApiError(error) && error.status === 401;
10
10
  const authenticationFailed = () => /* @__PURE__ */ new Error("Authentication failed.");
package/dist/init/run.js CHANGED
@@ -11,17 +11,17 @@ import { CONFIG_INIT_SERVICES, parseConfigInitServices } from "../config_templat
11
11
  import { hasNeonConfigFile, neonConfigFilename } from "../project.js";
12
12
  import { getAgentDisplayName } from "../mcp/agents.js";
13
13
  import { mcpInstallableAgents } from "../mcp/targets.js";
14
- import { missingPluginsCommands, pluginsInstallableAgents } from "../plugins/targets.js";
14
+ import { missingPluginsCommands, pluginsCommandProblemText, pluginsInstallableAgents, unusablePluginsCommands } from "../plugins/targets.js";
15
15
  import { skillsInstallableAgents } from "../skills/targets.js";
16
16
  import { FALLBACK_SKILLS_AGENTS, assertNamedAgentTooling, funnelAgentSetup, planInitToolingSteps, recommendedTooling, resolveNamedAgents, splitInitTooling } from "./plan.js";
17
17
  import { inferPackageManager, resolvePackageManager } from "../utils/package_manager.js";
18
18
  import { AGENTS_NOT_INSTALLED, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, MCP_API_KEY_NEEDS_AUTH, MCP_SCOPED_NEEDS_PROJECT, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, PROGRESS, PROJECT_PINNED_OFFLINE, claimableNext, claimableThenServicesNext, configInitNext, envPullFailedNext, existingConfigNext, installFailedNext, mcpAfterLinkNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, mcpKeyFailedNext, namedAgentsUnavailable, newConfigNext, offlinePinNext, skippedLinkNext, toolingFailedNext, unattendedUnauthedNext } from "./copy.js";
19
19
  import { agentsRowValue, configRowValue, formatInitDone, headingForKind, printInitBanner, printInitDone, printInitProgress, projectRowValue, shouldPrintInitBanner } from "./chrome.js";
20
- import { h as initCmd, t as ConfigInstallFailed } from "../_chunks/config-7SIE_C5O.js";
21
- import { o as quoteFlagValue, s as runLink } from "../_chunks/link-CcwTXRny.js";
20
+ import { h as initCmd, t as ConfigInstallFailed } from "../_chunks/config-BjCm5cfr.js";
21
+ import { o as quoteFlagValue, s as runLink } from "../_chunks/link-nAiuWKfg.js";
22
22
  import { runAuthenticatedLink } from "./link.js";
23
23
  import { CannotMintApiKeyError } from "../mcp/mint.js";
24
- import { pullInitEnv } from "./auth.js";
24
+ import { pullInitEnv, resolveInitConfigApi } from "./auth.js";
25
25
  import { InitCancelled, raceSigint, restoreCursor } from "./cancelled.js";
26
26
  import { pickAgentSetupInteractively, pickInitAgentsInteractively, pickInitConfigInteractively, pickInitMcpAuthInteractively, pickInitMcpConfigLocationInteractively, pickInitModeInteractively, pickInitPackageManagerInteractively, pickInitProjectSetupInteractively, pickInitServicesInteractively, pickInitSkillsInteractively } from "./wizard.js";
27
27
  import { runToolingSteps, toolingStepCommandText } from "./tooling.js";
@@ -67,10 +67,10 @@ const pluginScopeFor = (agents, preferred) => {
67
67
  const project = new Set(pluginsInstallableAgents("project"));
68
68
  return agents.some((id) => !project.has(id) && pluginsInstallableAgents("global").includes(id)) ? "global" : "project";
69
69
  };
70
- const warnMissingPluginCommands = (missing, tooling) => {
70
+ const warnUnusablePluginCommands = (unusable, tooling) => {
71
71
  if (tooling.setup !== "skills-mcp" && tooling.setup !== "mixed") return;
72
72
  const routed = /* @__PURE__ */ new Set([...tooling.skillsAgents, ...tooling.mcpAgents]);
73
- for (const { agent, command } of missing) if (routed.has(agent)) log.warning("%s: \"%s\" was not found on PATH, so its plugin cannot install. Installing Neon skills and MCP instead.", getAgentDisplayName(agent), command);
73
+ for (const row of unusable) if (routed.has(row.agent)) log.warning("%s: \"%s\" %s, so its plugin cannot install. Installing Neon skills and MCP instead.", getAgentDisplayName(row.agent), row.command.name, pluginsCommandProblemText(row));
74
74
  };
75
75
  const nonEmptyAgents = (ids, fallback) => {
76
76
  const [first, ...rest] = ids;
@@ -124,19 +124,34 @@ const pickOrDetectAgents = async (input) => {
124
124
  available: [...input.available],
125
125
  detected,
126
126
  preselected,
127
- ...input.missingCommands !== void 0 ? { missingCommands: input.missingCommands } : {},
127
+ ...input.unusableCommands !== void 0 ? { unusableCommands: input.unusableCommands } : {},
128
128
  setup: input.setup
129
129
  });
130
130
  return detected;
131
131
  };
132
132
  const defaultInitConfig = async (input) => {
133
+ let project = {};
134
+ if (input.project) try {
135
+ project = {
136
+ ...await resolveInitConfigApi(input.project.auth),
137
+ projectId: input.project.projectId,
138
+ ...input.project.branch !== void 0 ? { branch: input.project.branch } : {},
139
+ ...input.project.auth.configDir !== void 0 ? { configDir: input.project.auth.configDir } : {},
140
+ ...input.project.auth.profile !== void 0 ? { profile: input.project.auth.profile } : {},
141
+ ...input.project.auth.oauthHost !== void 0 ? { oauthHost: input.project.auth.oauthHost } : {},
142
+ ...input.project.auth.clientId !== void 0 ? { clientId: input.project.auth.clientId } : {}
143
+ };
144
+ } catch (error) {
145
+ log.warning("Couldn't authenticate to read the Data API on the linked branch (%s); declaring it with Neon Auth.", error instanceof Error ? error.message : String(error));
146
+ }
133
147
  await initCmd({
134
148
  cwd: input.cwd,
135
149
  install: input.install,
136
150
  silent: true,
137
151
  requireInstall: true,
138
152
  ...input.services !== void 0 ? { services: input.services } : {},
139
- ...input.packageManager !== void 0 ? { packageManager: input.packageManager } : {}
153
+ ...input.packageManager !== void 0 ? { packageManager: input.packageManager } : {},
154
+ ...project
140
155
  });
141
156
  };
142
157
  const defaultCreateClaimable = async (input) => {
@@ -250,9 +265,18 @@ const runInit = async (props) => {
250
265
  }
251
266
  const recommended = mode === "recommended";
252
267
  const targets = named.length > 0 ? named : detection.detectedAgents;
253
- const missingPluginCommands = missingPluginsCommands(targets, { cwd });
254
- const unavailablePluginAgents = missingPluginCommands.map((row) => row.agent);
255
- const pluginReady = (agents) => agents.filter((id) => !unavailablePluginAgents.includes(id));
268
+ let probedPluginCommands;
269
+ const unusablePluginCommands = () => {
270
+ probedPluginCommands ??= unusablePluginsCommands(targets, { cwd });
271
+ return probedPluginCommands;
272
+ };
273
+ let missingCommands;
274
+ const missingPluginCommands = () => {
275
+ missingCommands ??= missingPluginsCommands(targets, { cwd });
276
+ return missingCommands;
277
+ };
278
+ const unavailablePluginAgents = () => unusablePluginCommands().map((row) => row.agent);
279
+ const pluginReady = (agents) => agents.filter((id) => !unavailablePluginAgents().includes(id));
256
280
  let tooling = { setup: "skip" };
257
281
  let mcpAuth = props.mcpAuth;
258
282
  let mcpAuthChosen = props.mcpAuth !== void 0;
@@ -262,9 +286,9 @@ const runInit = async (props) => {
262
286
  let pluginScope = recommended ? "global" : "project";
263
287
  if (recommended) {
264
288
  pluginScope = "global";
265
- tooling = named.length > 0 ? splitInitTooling(named, "global", "global", unavailablePluginAgents) : recommendedTooling(detection.detectedAgents, unavailablePluginAgents);
289
+ tooling = named.length > 0 ? splitInitTooling(named, "global", "global", unavailablePluginAgents()) : recommendedTooling(detection.detectedAgents, unavailablePluginAgents());
266
290
  if (named.length === 0 && detection.detectedAgents.length === 0) printInitProgress(NO_AGENTS_FALLBACK_STATUS);
267
- warnMissingPluginCommands(missingPluginCommands, tooling);
291
+ warnUnusablePluginCommands(unusablePluginCommands(), tooling);
268
292
  mcpAuth = !detection.authenticated && (tooling.setup === "skills-mcp" || tooling.setup === "mixed") ? "oauth" : mcpAuth;
269
293
  } else {
270
294
  const inferred = inferInitAgentSetup({
@@ -279,8 +303,8 @@ const runInit = async (props) => {
279
303
  else if (inferred.kind === "auto") {
280
304
  const autoAgents = named.length > 0 ? named : detection.detectedAgents;
281
305
  pluginScope = pluginScopeFor(pluginReady(autoAgents), "project");
282
- tooling = splitInitTooling(autoAgents, pluginScope, mcpConfigLocation, unavailablePluginAgents);
283
- warnMissingPluginCommands(missingPluginCommands, tooling);
306
+ tooling = splitInitTooling(autoAgents, pluginScope, mcpConfigLocation, unavailablePluginAgents());
307
+ warnUnusablePluginCommands(unusablePluginCommands(), tooling);
284
308
  if (tooling.setup === "skip" && yes && named.length === 0) {
285
309
  tooling = recommendedTooling([]);
286
310
  printInitProgress(NO_AGENTS_FALLBACK_STATUS);
@@ -295,8 +319,8 @@ const runInit = async (props) => {
295
319
  available,
296
320
  detected: detection.detectedAgents,
297
321
  ...setup === "plugin" ? {
298
- preselected: pluginReady(detection.detectedAgents),
299
- missingCommands: missingPluginCommands
322
+ preselected: detection.detectedAgents.filter((id) => !missingPluginCommands().some((row) => row.agent === id)),
323
+ unusableCommands: missingPluginCommands()
300
324
  } : {},
301
325
  interactive: detection.interactive,
302
326
  setup,
@@ -557,12 +581,19 @@ const runInit = async (props) => {
557
581
  }
558
582
  if (!existingConfig) printInitProgress(PROGRESS.config);
559
583
  printInitProgress(PROGRESS.install(pm));
584
+ const readableProject = funnel.link !== "skipped" && !pinnedOffline && declared.includes("data-api") ? readContextFile(contextFile) : void 0;
585
+ const readableBranch = readableProject !== void 0 ? contextBranch(readableProject) : void 0;
560
586
  try {
561
587
  await (props.initConfig ?? defaultInitConfig)({
562
588
  cwd,
563
589
  install: true,
564
590
  packageManager: pm,
565
- ...planned !== void 0 ? { services: planned } : {}
591
+ ...planned !== void 0 ? { services: planned } : {},
592
+ ...typeof readableProject?.projectId === "string" ? { project: {
593
+ projectId: readableProject.projectId,
594
+ ...readableBranch !== void 0 ? { branch: readableBranch } : {},
595
+ auth
596
+ } } : {}
566
597
  });
567
598
  } catch (error) {
568
599
  if (error instanceof ConfigInstallFailed) {
@@ -6,7 +6,7 @@ import { CannotMintApiKeyError } from "../mcp/mint.js";
6
6
  import { detectAgent } from "./detect_host.js";
7
7
  import { AgentSelectionSkipped, canPickAgentsInteractively } from "../utils/agent_picker.js";
8
8
  import { a as mcpInstallError, s as reportMcpInstall } from "../_chunks/mcp-BznTRK3w.js";
9
- import { l as reportPluginsInstall, o as installPlugins, s as pluginsInstallError } from "../_chunks/plugins-CKOcBuuP.js";
9
+ import { l as reportPluginsInstall, o as installPlugins, s as pluginsInstallError } from "../_chunks/plugins-B2Gym8Lw.js";
10
10
  import { c as skillsInstallError, o as installSkills, s as reportSkillsInstall } from "../_chunks/skills-DKgafm65.js";
11
11
  import { runAuthenticatedMcp } from "./auth.js";
12
12
  import { raceSigint } from "./cancelled.js";
@@ -2,7 +2,7 @@ import { CONFIG_INIT_SERVICES } from "../config_template.js";
2
2
  import { getAgentDisplayName, listMcpAgentIds } from "../mcp/agents.js";
3
3
  import { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
4
4
  import { installedPackageManagers } from "../utils/package_manager.js";
5
- import { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, PACKAGE_MANAGER_MESSAGE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, agentMissingCommandDescription } from "./copy.js";
5
+ import { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, PACKAGE_MANAGER_MESSAGE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, agentUnusableCommandDescription } from "./copy.js";
6
6
  import { POSTGRES_SERVICE_CHOICE, keepPostgresSelected } from "../utils/service_picker.js";
7
7
  import { canPickAgentsInteractively, pickAgentsInteractively } from "../utils/agent_picker.js";
8
8
  import { NEON_SKILL_CATALOG, listSkillIds } from "../skills/catalog.js";
@@ -72,18 +72,18 @@ const pickAgentSetupInteractively = async () => {
72
72
  const pickInitAgentsInteractively = async (input) => {
73
73
  requireInteractive();
74
74
  const detected = new Set(input.detected);
75
- const missing = new Map((input.missingCommands ?? []).map((row) => [row.agent, row.command]));
75
+ const unusable = new Map((input.unusableCommands ?? []).map((row) => [row.agent, row]));
76
76
  const selected = await pickAgentsInteractively({
77
77
  onCancel: aborted,
78
78
  message: input.setup === "skills" ? AGENT_SKILLS_PICKER_MESSAGE : input.setup === "mcp" ? AGENT_MCP_PICKER_MESSAGE : AGENT_PICKER_MESSAGE,
79
79
  skipMessage: input.setup === "plugin" ? AGENT_PLUGIN_SKIP_MESSAGE : input.setup === "skills" ? AGENT_SKILLS_SKIP_MESSAGE : AGENT_MCP_SKIP_MESSAGE,
80
80
  selected: input.preselected ?? input.detected,
81
81
  choices: input.available.map((id) => {
82
- const command = missing.get(id);
82
+ const row = unusable.get(id);
83
83
  return {
84
84
  id,
85
85
  title: `${getAgentDisplayName(id)} (${id})`,
86
- description: command !== void 0 ? agentMissingCommandDescription(command) : detected.has(id) ? AGENT_DETECTED_DESCRIPTION : AGENT_OTHER_DESCRIPTION
86
+ description: row !== void 0 ? agentUnusableCommandDescription(row) : detected.has(id) ? AGENT_DETECTED_DESCRIPTION : AGENT_OTHER_DESCRIPTION
87
87
  };
88
88
  })
89
89
  });
@@ -207,7 +207,7 @@ const pickInitServicesInteractively = async () => {
207
207
  {
208
208
  value: "data-api",
209
209
  title: "Data API",
210
- description: "Declare an HTTP API for Postgres. Also declares Managed Better Auth."
210
+ description: "Declare an HTTP API for Postgres. Also declares Managed Better Auth, unless the branch's Data API already uses an external identity provider."
211
211
  },
212
212
  {
213
213
  value: "functions",
@@ -0,0 +1,10 @@
1
+ // src/plugins/child_env.ts
2
+ const TELEMETRY_BLOCKERS = ["DISABLE_TELEMETRY", "DO_NOT_TRACK"];
3
+ /** The env the plugins CLI and the agent CLIs it calls run with. */
4
+ const pluginsChildEnv = (base = process.env) => {
5
+ const env = { ...base };
6
+ for (const key of TELEMETRY_BLOCKERS) delete env[key];
7
+ for (const key of Object.keys(env)) if (key.toUpperCase() === "NEON_API_KEY") delete env[key];
8
+ return env;
9
+ };
10
+ export { pluginsChildEnv };
@@ -1,11 +1,11 @@
1
1
  import { getAgentDisplayName, tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
- import { detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, missingPluginsCommands, pluginsInstallableAgents } from "./targets.js";
3
+ import { detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, missingPluginsCommands, pluginsCommandFix, pluginsCommandProblemText, pluginsInstallableAgents, unusablePluginsCommands } from "./targets.js";
4
4
  import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
5
5
  import { detectAgent } from "../init/detect_host.js";
6
6
  import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
7
7
  // src/plugins/plan.ts
8
- const missingPluginsCommandsError = (missing) => new Error(missing.map(({ agent, command }) => `Cannot install the Neon plugin for ${getAgentDisplayName(agent)}: "${command}" was not found on PATH. Install it or add it to PATH, then retry.`).join("\n"));
8
+ const unusablePluginsCommandsError = (unusable) => new Error(unusable.map((row) => `Cannot install the Neon plugin for ${getAgentDisplayName(row.agent)}: "${row.command.name}" ${pluginsCommandProblemText(row)}. ${pluginsCommandFix(row)}, then retry.`).join("\n"));
9
9
  const assertPluginsCanRun = (options) => {
10
10
  if (options.yes || options.interactive || options.hasAgents) return;
11
11
  throw new Error("No interactive terminal. Pass -y to install into detected agents, or --agent <name> to name them.");
@@ -34,18 +34,18 @@ async function resolvePluginsPlan(options) {
34
34
  detectAgent: options.detectAgent ?? detectAgent,
35
35
  acceptHost: (id) => availableSet.has(id) || getPluginsTargetName(id) !== void 0
36
36
  }) : scoped;
37
- const missingDetected = new Map((prompt ? missingPluginsCommands(detected, commandLookup(options)) : []).map((row) => [row.agent, row.command]));
37
+ const unusableDetected = new Map((prompt && options.agents.length === 0 ? missingPluginsCommands(detected, commandLookup(options)) : []).map((row) => [row.agent, row]));
38
38
  const choices = agentChoicesFrom(available, detected).map((choice) => {
39
- const command = missingDetected.get(choice.id);
40
- return command === void 0 ? choice : {
39
+ const row = unusableDetected.get(choice.id);
40
+ return row === void 0 ? choice : {
41
41
  ...choice,
42
- description: `detected, but "${command}" is not on PATH`
42
+ description: `detected, but "${row.command.name}" ${pluginsCommandProblemText(row)}`
43
43
  };
44
44
  });
45
45
  const selected = await resolveAgentSelection({
46
46
  specified: options.agents,
47
47
  choices,
48
- detected: detected.filter((id) => !missingDetected.has(id)),
48
+ detected: detected.filter((id) => !unusableDetected.has(id)),
49
49
  message: "Which coding agents should get the Neon plugin? (space to toggle, enter to confirm)",
50
50
  nonInteractiveMessage: noDetectedAgentsMessage({
51
51
  scope,
@@ -77,17 +77,17 @@ async function resolvePluginsPlan(options) {
77
77
  }
78
78
  agents.push(id);
79
79
  }
80
- const missingCommands = missingPluginsCommands(agents, commandLookup(options));
81
- if (missingCommands.length > 0) {
82
- if (!(options.agents.length === 0 && !prompt)) throw missingPluginsCommandsError(missingCommands);
83
- const missingAgents = new Set(missingCommands.map((row) => row.agent));
80
+ const unusableCommands = unusablePluginsCommands(agents, commandLookup(options));
81
+ if (unusableCommands.length > 0) {
82
+ if (!(options.agents.length === 0 && !prompt)) throw unusablePluginsCommandsError(unusableCommands);
83
+ const missingAgents = new Set(unusableCommands.map((row) => row.agent));
84
84
  const installable = agents.filter((id) => !missingAgents.has(id));
85
85
  return {
86
86
  scope,
87
87
  agents: installable,
88
88
  skipped,
89
89
  userScopeSkipped,
90
- missingCommands,
90
+ unusableCommands,
91
91
  targets: installable.length === 0 ? [] : mappedPluginsTargets(installable, scope)
92
92
  };
93
93
  }
@@ -103,8 +103,8 @@ async function resolvePluginsPlan(options) {
103
103
  agents,
104
104
  skipped,
105
105
  userScopeSkipped,
106
- missingCommands,
106
+ unusableCommands,
107
107
  targets: mappedPluginsTargets(agents, scope)
108
108
  };
109
109
  }
110
- export { assertPluginsCanRun, missingPluginsCommandsError, resolvePluginsPlan };
110
+ export { assertPluginsCanRun, resolvePluginsPlan, unusablePluginsCommandsError };
@@ -1,7 +1,7 @@
1
+ import { pluginsChildEnv } from "./child_env.js";
1
2
  import { spawnSync } from "node:child_process";
2
3
  import { execa } from "execa";
3
4
  // src/plugins/run.ts
4
- const TELEMETRY_BLOCKERS = ["DISABLE_TELEMETRY", "DO_NOT_TRACK"];
5
5
  const PLUGIN_SOURCE = "neondatabase/agent-skills";
6
6
  const NEON_PLUGIN_NAME = "neon-postgres";
7
7
  const PLUGIN_SKILLS = [
@@ -16,12 +16,6 @@ const PLUGIN_SKILLS = [
16
16
  "neon-realtime"
17
17
  ];
18
18
  const PLUGINS_CLI_TIMEOUT_MS = 12e4;
19
- const pluginsChildEnv = (base = process.env) => {
20
- const env = { ...base };
21
- for (const key of TELEMETRY_BLOCKERS) delete env[key];
22
- for (const key of Object.keys(env)) if (key.toUpperCase() === "NEON_API_KEY") delete env[key];
23
- return env;
24
- };
25
19
  const pluginsScopeFlag = (global) => global ? "user" : "project";
26
20
  const pluginsAddArgs = (options) => {
27
21
  if (options.target.length === 0) throw new Error("plugins add needs a -t target.");
@@ -130,4 +124,4 @@ const killProcessTree = (pid) => {
130
124
  };
131
125
  const isCommandMissing = (error) => typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
132
126
  const isExecaFailure = (error) => typeof error === "object" && error !== null && "shortMessage" in error && typeof error.shortMessage === "string";
133
- export { NEON_PLUGIN_NAME, PLUGINS_CLI_TIMEOUT_MS, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, pluginsChildEnv, pluginsCliFailureMessage, pluginsScopeFlag, quoteNpxArg, runPluginsCli };
127
+ export { NEON_PLUGIN_NAME, PLUGINS_CLI_TIMEOUT_MS, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, pluginsCliFailureMessage, pluginsScopeFlag, quoteNpxArg, runPluginsCli };
@@ -1,7 +1,9 @@
1
1
  import { detectInstalledAgents, listMcpAgentIds, tryResolveAddMcpAgentId, uniqueAgentIds } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
+ import { pluginsChildEnv } from "./child_env.js";
3
4
  import { delimiter, dirname, join, resolve } from "node:path";
4
5
  import { detectProjectAgents } from "add-mcp";
6
+ import { spawnSync } from "node:child_process";
5
7
  import which from "which";
6
8
  // src/plugins/targets.ts
7
9
  const CLAUDE_CODE = { target: "claude-code" };
@@ -13,11 +15,20 @@ const PLUGINS_TARGET_BY_TYPE = {
13
15
  codex: { target: "codex" },
14
16
  "github-copilot-cli": {
15
17
  target: "github-copilot",
16
- command: "copilot"
18
+ command: {
19
+ name: "copilot",
20
+ product: "GitHub Copilot CLI",
21
+ versionPrefix: "GitHub Copilot CLI",
22
+ minVersion: "0.0.400"
23
+ }
17
24
  },
18
25
  "grok-build": {
19
26
  target: "grok",
20
- command: "grok"
27
+ command: {
28
+ name: "grok",
29
+ product: "Grok Build",
30
+ versionPrefix: "grok"
31
+ }
21
32
  }
22
33
  };
23
34
  const pluginsTargets = () => [...new Set(Object.values(PLUGINS_TARGET_BY_TYPE))];
@@ -38,20 +49,127 @@ const pluginsCommandSearchPath = (cwd, path) => {
38
49
  }
39
50
  return [...bins, path].join(delimiter);
40
51
  };
41
- const missingPluginsCommands = (agents, options) => {
52
+ const VERSION_TIMEOUT_MS = 1e4;
53
+ const versionParts = (version) => version.split(".").map((part) => Number.parseInt(part, 10));
54
+ /** Exported for tests. */
55
+ const versionAtLeast = (version, minimum) => {
56
+ const have = versionParts(version);
57
+ const need = versionParts(minimum);
58
+ for (let i = 0; i < 3; i++) {
59
+ const a = have[i] ?? 0;
60
+ const b = need[i] ?? 0;
61
+ if (a !== b) return a > b;
62
+ }
63
+ return true;
64
+ };
65
+ /** Exported for tests. */
66
+ const pluginsCommandVersionProblem = (command, output) => {
67
+ const label = `${command.versionPrefix} `;
68
+ const at = output.indexOf(label);
69
+ const version = at === -1 ? void 0 : output.slice(at + label.length).match(/^\d+\.\d+\.\d+/)?.[0];
70
+ if (version === void 0) return { kind: "unrecognized" };
71
+ if (command.minVersion !== void 0 && !versionAtLeast(version, command.minVersion)) return {
72
+ kind: "outdated",
73
+ version
74
+ };
75
+ };
76
+ const firstLine = (text) => text.split("\n").map((line) => line.trim()).find((line) => line.length > 0) ?? "";
77
+ /** The child sees the same PATH the command was resolved on, as under npx. */
78
+ const withPath = (env, path) => {
79
+ const key = Object.keys(env).find((name) => name.toUpperCase() === "PATH");
80
+ return {
81
+ ...env,
82
+ [key ?? "PATH"]: path
83
+ };
84
+ };
85
+ /**
86
+ * Runs `<command> --version` the way the plugins CLI spawns the command (no shell, the npx
87
+ * search PATH, its child env), so a command it could not run is caught before it runs.
88
+ */
89
+ const probePluginsCommand = (command, options) => {
90
+ const resolved = which.sync(command.name, {
91
+ nothrow: true,
92
+ path: options.searchPath
93
+ });
94
+ if (resolved === null) return { kind: "missing" };
95
+ const result = spawnSync(resolved, ["--version"], {
96
+ cwd: options.cwd,
97
+ env: withPath(pluginsChildEnv(), options.searchPath),
98
+ encoding: "utf8",
99
+ stdio: [
100
+ "ignore",
101
+ "pipe",
102
+ "pipe"
103
+ ],
104
+ timeout: VERSION_TIMEOUT_MS,
105
+ windowsHide: true
106
+ });
107
+ if (result.error !== void 0) {
108
+ const code = "code" in result.error && typeof result.error.code === "string" ? result.error.code : void 0;
109
+ return {
110
+ kind: "broken",
111
+ detail: code === "ETIMEDOUT" ? `timed out after ${VERSION_TIMEOUT_MS / 1e3} seconds` : code ?? result.error.message
112
+ };
113
+ }
114
+ const output = `${result.stdout}\n${result.stderr}`;
115
+ if (result.status !== 0) {
116
+ const line = firstLine(result.stderr) || firstLine(result.stdout);
117
+ return {
118
+ kind: "broken",
119
+ detail: line.length > 0 ? line.slice(0, 160).replace(/\.$/, "") : result.signal !== null ? `killed by ${result.signal}` : `exit code ${result.status}`
120
+ };
121
+ }
122
+ return pluginsCommandVersionProblem(command, output);
123
+ };
124
+ const commandRows = (agents, options, check) => {
42
125
  const searchPath = pluginsCommandSearchPath(options.cwd, options.path ?? process.env.PATH ?? "");
43
- const missing = [];
126
+ const rows = [];
44
127
  for (const agent of uniqueAgentIds(agents)) {
45
128
  const command = getPluginsTarget(agent)?.command;
46
- if (command !== void 0 && which.sync(command, {
47
- nothrow: true,
48
- path: searchPath
49
- }) === null) missing.push({
129
+ if (command === void 0) continue;
130
+ const problem = check(command, searchPath);
131
+ if (problem !== void 0) rows.push({
50
132
  agent,
51
- command
133
+ command,
134
+ problem
52
135
  });
53
136
  }
54
- return missing;
137
+ return rows;
138
+ };
139
+ /**
140
+ * Agents whose plugin command is not on the search PATH. Runs nothing, so it is safe for
141
+ * agents that were only detected and may not be installed.
142
+ */
143
+ const missingPluginsCommands = (agents, options) => commandRows(agents, options, (command, searchPath) => which.sync(command.name, {
144
+ nothrow: true,
145
+ path: searchPath
146
+ }) === null ? { kind: "missing" } : void 0);
147
+ /**
148
+ * Agents whose plugin installs through a CLI that is missing, does not run, is not the
149
+ * expected product, or is too old. Runs `<command> --version`, so pass only agents the
150
+ * plugin is about to install into; file-based targets never spawn anything.
151
+ */
152
+ const unusablePluginsCommands = (agents, options) => commandRows(agents, options, (command, searchPath) => probePluginsCommand(command, {
153
+ cwd: options.cwd,
154
+ searchPath
155
+ }));
156
+ /** Completes `"<command>" …` in a warning or error. */
157
+ const pluginsCommandProblemText = (row) => {
158
+ const { command, problem } = row;
159
+ const product = command.product;
160
+ switch (problem.kind) {
161
+ case "missing": return "was not found on PATH";
162
+ case "broken": return `did not run ("${command.name} --version" failed: ${problem.detail})`;
163
+ case "unrecognized": return `is not ${product} ("${command.name} --version" printed no ${product} version)`;
164
+ case "outdated": return `is ${product} ${problem.version}, older than the ${command.minVersion} its plugin needs`;
165
+ default: return problem;
166
+ }
167
+ };
168
+ /** What to do about the problem, as an imperative clause. */
169
+ const pluginsCommandFix = (row) => {
170
+ const { command, problem } = row;
171
+ if (problem.kind === "missing") return "Install it or add it to PATH";
172
+ return `Install or update ${command.product}`;
55
173
  };
56
174
  function getPluginsTarget(agent) {
57
175
  if (Object.prototype.hasOwnProperty.call(PLUGINS_TARGET_BY_TYPE, agent)) return PLUGINS_TARGET_BY_TYPE[agent];
@@ -104,4 +222,4 @@ const mappedPluginsTargets = (agents, scope) => {
104
222
  if (mapped.length === 0) throw new Error(`None of the selected agents can install plugins. Supported agents: ${pluginsInstallableAgents(scope).join(", ")}`);
105
223
  return mapped;
106
224
  };
107
- export { detectInstallablePluginsAgents, detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, missingPluginsCommands, pluginsInstallableAgents, pluginsMappedAgents, pluginsTargets, supportsPlugins };
225
+ export { detectInstallablePluginsAgents, detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, missingPluginsCommands, pluginsCommandFix, pluginsCommandProblemText, pluginsCommandVersionProblem, pluginsInstallableAgents, pluginsMappedAgents, pluginsTargets, supportsPlugins, unusablePluginsCommands, versionAtLeast };