neon 8.15.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.
package/README.md CHANGED
@@ -1056,6 +1056,8 @@ On a TTY the command asks which agents, then installs. Detected agents start sel
1056
1056
 
1057
1057
  Default scope is `project`. `--global` is `user`. On macOS and Linux, Cursor and Claude Code store the plugin cache under `~/.claude/plugins`; on Windows, Cursor installs into Cursor extensions. `project` vs `user` is the scope field the plugins CLI records, not a directory in the repo. VS Code, GitHub Copilot CLI, and Grok Build only install user-level: they are skipped at the default scope with a warning, and a VS Code-only project fails if nothing else is selected. Pass `--global` for those.
1058
1058
 
1059
+ GitHub Copilot CLI and Grok Build install through their own CLIs (`copilot`, `grok`), so `neon plugins` runs `copilot --version` or `grok --version` first. A command that is missing, fails, or (for Copilot) is older than 0.0.400 is skipped with a warning under `-y`, fails an explicit `--agent`, and in `neon init` gets Neon skills and MCP instead of the plugin. The other agents install by writing files, so their detected config directory is enough.
1060
+
1059
1061
  Supported agents with a plugins mapping: `claude-code`, `claude-desktop`, `codex`, `cursor`, `github-copilot-cli`, `grok-build`, `vscode`. `claude-desktop` installs as Claude Code; detecting both produces one install and lists both names in the table. `mcporter` is a known MCP name that is then skipped.
1060
1062
 
1061
1063
  The plugins CLI installs every plugin it finds in the Neon plugin package. Today that is `neon-postgres` from `neondatabase/agent-skills`. It includes the Neon MCP server (`https://mcp.neon.tech/mcp`) and these skills: `neon`, `neon-ai-gateway`, `neon-auth`, `neon-functions`, `neon-object-storage`, `neon-postgres`, `neon-postgres-branches`, `neon-postgres-egress-optimizer`, `neon-realtime`. It does not include `neon-postgres-agent-platforms`.
@@ -1122,7 +1124,7 @@ Without a template, a terminal asks which one to use; anything else exits with t
1122
1124
 
1123
1125
  A template's `layout` decides what you get. `router` writes one module per operation plus a generated `index.ts` that dispatches on the URL path, and declares one function. `separate` declares each operation as its own function under the slug the template gives it. Without `--operation` or `--all-operations`, a TTY asks which operations to include and anything else uses the template's recommended ones.
1124
1126
 
1125
- Variables a template needs are declared on each function in `neon.ts` (`env: { API_KEY: process.env.API_KEY! }`) and added, without values, to `.env.example` next to it. Values are never written to `.env` or `.env.local` for you: a blank placeholder there would deploy as an empty string and delete the live value. Put the real value in the file `env pull` uses and deploy with `neon config apply --env .env.local`. `neon dev` reads function env from your shell, so export it there as well.
1127
+ Variables a template needs are declared on each function in `neon.ts` (`env: { API_KEY: process.env.API_KEY! }`) and added, without values, to `.env.example` next to it. Values are never written to `.env` or `.env.local` for you: a blank placeholder there would deploy as an empty string and delete the live value. Put the real value in the file `env pull` uses and deploy with `neon config apply --env .env.local`. `neon dev` loads it automatically.
1126
1128
 
1127
1129
  Nothing is written until every check passes: a file that already exists with different contents needs `--force`, and a slug `neon.ts` already declares needs `--slug`, `--force`, or `--no-add-to-config`. A `separate` template sets its own slugs, so `--slug` only changes its directory; remove the existing declarations from `neon.ts`, or use `--force` or `--no-add-to-config`.
1128
1130
 
@@ -128,7 +128,7 @@ function siteSearchHeaders(opts) {
128
128
  "Content-Type": "application/json",
129
129
  "User-Agent": `neon/${opts.version}`,
130
130
  "x-caller-source": "cli",
131
- ...opts.agent ? { "x-caller-client": encodeURIComponent(opts.agent) } : {},
131
+ ...opts.analytics && opts.agent ? { "x-caller-client": encodeURIComponent(opts.agent) } : {},
132
132
  ...opts.analytics ? {} : { "x-caller-analytics": "off" }
133
133
  };
134
134
  }
@@ -5,7 +5,7 @@ import { helpCsv, helpEpilogue } from "../utils/help_text.js";
5
5
  import { getAgentDisplayName } from "../mcp/agents.js";
6
6
  import { NEON_MCP_URL } from "../mcp/install.js";
7
7
  import "../init/agents.js";
8
- import { pluginsInstallableAgents } from "../plugins/targets.js";
8
+ import { pluginsCommandProblemText, pluginsInstallableAgents } from "../plugins/targets.js";
9
9
  import { writer } from "../writer.js";
10
10
  import { noPassthrough } from "../utils/flags.js";
11
11
  import { AGENT_PLUGIN_SKIP_MESSAGE } from "../init/copy.js";
@@ -77,7 +77,7 @@ const installPlugins = async (options) => {
77
77
  });
78
78
  for (const agent of plan.skipped) log.warning("Skipping %s: no plugins mapping.", getAgentDisplayName(agent));
79
79
  for (const agent of plan.userScopeSkipped) log.warning("Skipping %s: plugins are user-level. Pass --global.", getAgentDisplayName(agent));
80
- for (const { agent, command } of plan.missingCommands) log.warning("Skipping %s: the plugin installs through \"%s\", which was not found on PATH.", getAgentDisplayName(agent), command);
80
+ for (const row of plan.unusableCommands) log.warning("Skipping %s: the plugin installs through \"%s\", which %s.", getAgentDisplayName(row.agent), row.command.name, pluginsCommandProblemText(row));
81
81
  const rows = [];
82
82
  const failed = [];
83
83
  const scope = scopeLabel(plan.scope);
package/dist/analytics.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { a as OAUTH, t as storeFor } from "./_chunks/credential_io-DbeSh3Pl.js";
2
- import { getCliAgent, getGithubEnvVars, isCi } from "./env.js";
2
+ import { getCliAgent, getGithubEnvVars, getTty, isCi } from "./env.js";
3
3
  import { log } from "./log.js";
4
4
  import { getOrCreateAnonymousId } from "./anonymous_id.js";
5
5
  import pkg_default from "./pkg.js";
@@ -248,7 +248,8 @@ const settleErrorReports = async (deadline) => {
248
248
  const getErrorAnalyticsEventContext = (_args) => ({
249
249
  version: pkg_default.version,
250
250
  ci: isCi(),
251
- agent: getCliAgent(process.env)
251
+ agent: getCliAgent(process.env),
252
+ tty: getTty()
252
253
  });
253
254
  const CREDENTIAL_TOKEN_ID = /\bnak_live_[0-9a-f]+\b/gi;
254
255
  const redactCredentialTokenIds = (text) => text.replace(CREDENTIAL_TOKEN_ID, "nak_live_<redacted>");
@@ -353,6 +354,7 @@ const getAnalyticsEventProperties = (args) => ({
353
354
  flags: { output: args.output },
354
355
  ci: isCi(),
355
356
  agent: getCliAgent(process.env),
357
+ tty: getTty(),
356
358
  githubEnvVars: getGithubEnvVars(process.env)
357
359
  });
358
360
  export { analyticsIdentity, analyticsMiddleware, closeAnalytics, commandSuccessProperties, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, isAnalyticsActive, recordCommandSuccessExtras, recordScaffoldedTemplate, redactCredentialTokenIds, sendError, storedCredentialAttribution, takeCommandSuccessExtras, telemetryCredential, trackCommandSuccess, trackEvent, useAnalyticsClientForTests };
@@ -1 +1 @@
1
- export { t as DEFAULT_SITE_SEARCH_URL, n as MAX_LIMIT, r as MAX_QUERY_LENGTH, i as SITE_SEARCH_CATEGORIES, a as builder, o as command, s as describe, l as formatSearchResults, u as parseSearchHits, d as resolveSiteSearchUrl, f as siteSearchHeaders } from "../_chunks/docs-CClmgj_Z.js";
1
+ export { t as DEFAULT_SITE_SEARCH_URL, n as MAX_LIMIT, r as MAX_QUERY_LENGTH, i as SITE_SEARCH_CATEGORIES, a as builder, o as command, s as describe, l as formatSearchResults, u as parseSearchHits, d as resolveSiteSearchUrl, f as siteSearchHeaders } from "../_chunks/docs-BpLiSTmm.js";
@@ -169,9 +169,8 @@ const scaffoldFromTemplate = async (options) => {
169
169
  const envFile = displayPath(resolveEnvFilePath(location), cwd);
170
170
  const names = environment.join(", ");
171
171
  if (variables && register) {
172
- const them = environment.length === 1 ? "it" : "them";
173
- nextSteps.push(`Set ${variables} in ${envFile}. \`${cli} dev\` reads ${them} from your shell, so export ${them} there too.`);
174
- next.push(`Set ${names} in ${envFile}, and export ${them} for ${cli} dev`);
172
+ nextSteps.push(`Set ${variables} in ${envFile} (\`${cli} dev\` loads it automatically).`);
173
+ next.push(`Set ${names} in ${envFile} (${cli} dev loads it automatically)`);
175
174
  } else if (variables) {
176
175
  nextSteps.push(`Set ${variables} in your shell.`);
177
176
  next.push(`Set ${names} in your shell`);
@@ -7,7 +7,7 @@ import { c as config_exports } from "../_chunks/config-BjCm5cfr.js";
7
7
  import { u as projects_exports } from "../_chunks/projects-a3X4OFEJ.js";
8
8
  import { a as link_exports } from "../_chunks/link-nAiuWKfg.js";
9
9
  import { o as mcp_exports } from "../_chunks/mcp-BznTRK3w.js";
10
- import { c as plugins_exports } from "../_chunks/plugins-CKOcBuuP.js";
10
+ import { c as plugins_exports } from "../_chunks/plugins-B2Gym8Lw.js";
11
11
  import { l as skills_exports } from "../_chunks/skills-DKgafm65.js";
12
12
  import { t as bootstrap_exports } from "../_chunks/bootstrap-BDkvsAVJ.js";
13
13
  import { r as branches_exports } from "../_chunks/branches-Bp-5z1lM.js";
@@ -21,7 +21,7 @@ import { o as databases_exports } from "../_chunks/databases-DC2h6IW2.js";
21
21
  import { r as deploy_exports } from "../_chunks/deploy-DmEEhux0.js";
22
22
  import { s as dev_exports } from "../_chunks/dev-DlUJFzRs.js";
23
23
  import { i as diff_exports } from "../_chunks/diff-dnxneH0u.js";
24
- import { c as docs_exports } from "../_chunks/docs-CClmgj_Z.js";
24
+ import { c as docs_exports } from "../_chunks/docs-BpLiSTmm.js";
25
25
  import { o as feedback_exports } from "../_chunks/feedback-mQmdTRFn.js";
26
26
  import { o as functions_exports } from "../_chunks/functions-3ypiRwZK.js";
27
27
  import { o as git_exports } from "../_chunks/git-j6xSVo3e.js";
@@ -1 +1 @@
1
- export { t as aliases, n as builder, r as command, i as describe, a as handler, o as installPlugins, s as pluginsInstallError, l as reportPluginsInstall } from "../_chunks/plugins-CKOcBuuP.js";
1
+ export { t as aliases, n as builder, r as command, i as describe, a as handler, o as installPlugins, s as pluginsInstallError, l as reportPluginsInstall } from "../_chunks/plugins-B2Gym8Lw.js";
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/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/run.js CHANGED
@@ -11,7 +11,7 @@ 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";
@@ -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,7 +124,7 @@ 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;
@@ -265,9 +265,18 @@ const runInit = async (props) => {
265
265
  }
266
266
  const recommended = mode === "recommended";
267
267
  const targets = named.length > 0 ? named : detection.detectedAgents;
268
- const missingPluginCommands = missingPluginsCommands(targets, { cwd });
269
- const unavailablePluginAgents = missingPluginCommands.map((row) => row.agent);
270
- 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));
271
280
  let tooling = { setup: "skip" };
272
281
  let mcpAuth = props.mcpAuth;
273
282
  let mcpAuthChosen = props.mcpAuth !== void 0;
@@ -277,9 +286,9 @@ const runInit = async (props) => {
277
286
  let pluginScope = recommended ? "global" : "project";
278
287
  if (recommended) {
279
288
  pluginScope = "global";
280
- 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());
281
290
  if (named.length === 0 && detection.detectedAgents.length === 0) printInitProgress(NO_AGENTS_FALLBACK_STATUS);
282
- warnMissingPluginCommands(missingPluginCommands, tooling);
291
+ warnUnusablePluginCommands(unusablePluginCommands(), tooling);
283
292
  mcpAuth = !detection.authenticated && (tooling.setup === "skills-mcp" || tooling.setup === "mixed") ? "oauth" : mcpAuth;
284
293
  } else {
285
294
  const inferred = inferInitAgentSetup({
@@ -294,8 +303,8 @@ const runInit = async (props) => {
294
303
  else if (inferred.kind === "auto") {
295
304
  const autoAgents = named.length > 0 ? named : detection.detectedAgents;
296
305
  pluginScope = pluginScopeFor(pluginReady(autoAgents), "project");
297
- tooling = splitInitTooling(autoAgents, pluginScope, mcpConfigLocation, unavailablePluginAgents);
298
- warnMissingPluginCommands(missingPluginCommands, tooling);
306
+ tooling = splitInitTooling(autoAgents, pluginScope, mcpConfigLocation, unavailablePluginAgents());
307
+ warnUnusablePluginCommands(unusablePluginCommands(), tooling);
299
308
  if (tooling.setup === "skip" && yes && named.length === 0) {
300
309
  tooling = recommendedTooling([]);
301
310
  printInitProgress(NO_AGENTS_FALLBACK_STATUS);
@@ -310,8 +319,8 @@ const runInit = async (props) => {
310
319
  available,
311
320
  detected: detection.detectedAgents,
312
321
  ...setup === "plugin" ? {
313
- preselected: pluginReady(detection.detectedAgents),
314
- missingCommands: missingPluginCommands
322
+ preselected: detection.detectedAgents.filter((id) => !missingPluginCommands().some((row) => row.agent === id)),
323
+ unusableCommands: missingPluginCommands()
315
324
  } : {},
316
325
  interactive: detection.interactive,
317
326
  setup,
@@ -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
  });
@@ -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 };
@@ -141,10 +141,11 @@ const duplicateInstallNotice = () => {
141
141
  }
142
142
  };
143
143
  const isPackagedExecutable = () => "pkg" in process;
144
- const shouldRunUpdateNotifier = ({ commandPath, currentBranch, disabled, isCi: runningInCi, isPackaged, output, stderrIsTty, stdoutIsTty }) => !runningInCi && !disabled && !isPackaged && output === "table" && stdoutIsTty && stderrIsTty && commandPath.length > 0 && commandPath[0] !== "completion" && !isCurrentBranchProbe({
144
+ const commandSucceeded = () => (process.exitCode ?? 0) === 0;
145
+ const shouldRunUpdateNotifier = ({ commandPath, currentBranch, disabled, isCi: runningInCi, isPackaged, output, stderrIsTty, stdoutIsTty, succeeded }) => !runningInCi && !disabled && !isPackaged && output === "table" && stdoutIsTty && stderrIsTty && commandPath.length > 0 && commandPath[0] !== "completion" && !isCurrentBranchProbe({
145
146
  commandPath,
146
147
  currentBranch
147
- });
148
+ }) && succeeded;
148
149
  /**
149
150
  * The skills notice drops the terminal checks: coding agents run the CLI without a TTY,
150
151
  * and they are the ones that act on it. `neon skills` itself reports skill state.
@@ -157,7 +158,8 @@ const shouldRunSkillsNotifier = ({ commandPath, currentBranch, output }) => comm
157
158
  isPackaged: isPackagedExecutable(),
158
159
  output,
159
160
  stderrIsTty: true,
160
- stdoutIsTty: true
161
+ stdoutIsTty: true,
162
+ succeeded: commandSucceeded()
161
163
  });
162
164
  const notifyIfUpdateAvailable = ({ commandPath, configDir, currentBranch, currentVersion, output }) => {
163
165
  if (!shouldRunUpdateNotifier({
@@ -168,7 +170,8 @@ const notifyIfUpdateAvailable = ({ commandPath, configDir, currentBranch, curren
168
170
  isPackaged: isPackagedExecutable(),
169
171
  output,
170
172
  stderrIsTty: process.stderr.isTTY === true,
171
- stdoutIsTty: process.stdout.isTTY === true
173
+ stdoutIsTty: process.stdout.isTTY === true,
174
+ succeeded: commandSucceeded()
172
175
  })) return;
173
176
  const now = Date.now();
174
177
  if (process.platform !== "win32") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "8.15.0",
3
+ "version": "8.15.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -73,9 +73,9 @@
73
73
  "yargs": "17.7.2",
74
74
  "yoctocolors": "^2.1.2",
75
75
  "@neon/config": "1.10.0",
76
- "@neon/config-runtime": "1.11.0",
77
76
  "@neon/realtime": "0.1.2",
78
- "@neon/sdk": "7.7.0"
77
+ "@neon/sdk": "7.7.0",
78
+ "@neon/config-runtime": "1.11.0"
79
79
  },
80
80
  "optionalDependencies": {
81
81
  "@napi-rs/keyring": "1.3.0",
@@ -114,8 +114,8 @@
114
114
  "@neon-internals/cli-core": "0.0.0",
115
115
  "@neon-internals/env-core": "0.0.38",
116
116
  "@neon/env": "1.6.8",
117
- "@neon/functions": "0.11.0",
118
- "@neon/e2e-harness": "0.0.0"
117
+ "@neon/e2e-harness": "0.0.0",
118
+ "@neon/functions": "0.11.0"
119
119
  },
120
120
  "publishConfig": {
121
121
  "access": "public",