neon 6.2.4 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/init/plan.js CHANGED
@@ -201,8 +201,9 @@ const chooseYesAgentTooling = (agents) => {
201
201
  };
202
202
  };
203
203
  const FALLBACK_SKILLS_AGENTS = ["cursor", "codex"];
204
- const splitInitTooling = (agents, pluginScope, mcpConfigLocation = pluginScope) => {
205
- const pluginSet = new Set(pluginsInstallableAgents(pluginScope));
204
+ const splitInitTooling = (agents, pluginScope, mcpConfigLocation = pluginScope, unavailablePluginAgents = []) => {
205
+ const unavailable = new Set(unavailablePluginAgents);
206
+ const pluginSet = new Set(pluginsInstallableAgents(pluginScope).filter((id) => !unavailable.has(id)));
206
207
  const mcpSet = new Set(mcpInstallableAgents(mcpConfigLocation));
207
208
  const pluginAgents = uniqueAgentIds(agents.filter((id) => pluginSet.has(id)));
208
209
  const rest = uniqueAgentIds(agents.filter((id) => !pluginSet.has(id)));
@@ -226,8 +227,8 @@ const splitInitTooling = (agents, pluginScope, mcpConfigLocation = pluginScope)
226
227
  mcpAgents
227
228
  };
228
229
  };
229
- const recommendedTooling = (detected) => {
230
- const split = splitInitTooling(detected, "global");
230
+ const recommendedTooling = (detected, unavailablePluginAgents = []) => {
231
+ const split = splitInitTooling(detected, "global", "global", unavailablePluginAgents);
231
232
  if (split.setup === "skip") return {
232
233
  setup: "skills",
233
234
  agents: FALLBACK_SKILLS_AGENTS
@@ -297,7 +298,12 @@ const planInitToolingSteps = (input) => {
297
298
  default: return input.tooling;
298
299
  }
299
300
  };
300
- const funnelAgentSetup = (tooling) => tooling.setup;
301
+ const funnelAgentSetup = (tooling) => {
302
+ if (tooling.setup !== "skills-mcp") return tooling.setup;
303
+ if (tooling.skillsAgents.length === 0) return "mcp";
304
+ if (tooling.mcpAgents.length === 0) return "skills";
305
+ return "skills-mcp";
306
+ };
301
307
  const planYesAgentSteps = (tooling) => planToolingSteps(tooling, {
302
308
  yes: true,
303
309
  named: false
package/dist/init/run.js CHANGED
@@ -13,8 +13,9 @@ import { agentsRowValue, configRowValue, formatInitDone, headingForKind, printIn
13
13
  import { inferPackageManager, resolvePackageManager } from "../utils/package_manager.js";
14
14
  import { ConfigInstallFailed, hasNeonConfigFile, initCmd, neonConfigFilename } from "../commands/config.js";
15
15
  import { runAuthenticatedLink } from "./link.js";
16
+ import { getAgentDisplayName } from "../mcp/agents.js";
16
17
  import { mcpInstallableAgents } from "../mcp/targets.js";
17
- import { pluginsInstallableAgents } from "../plugins/targets.js";
18
+ import { missingPluginsCommands, pluginsInstallableAgents } from "../plugins/targets.js";
18
19
  import { skillsInstallableAgents } from "../skills/targets.js";
19
20
  import { FALLBACK_SKILLS_AGENTS, assertNamedAgentTooling, funnelAgentSetup, planInitToolingSteps, recommendedTooling, resolveNamedAgents, splitInitTooling } from "./plan.js";
20
21
  import { pullInitEnv } from "./auth.js";
@@ -56,11 +57,10 @@ const pluginScopeFor = (agents, preferred) => {
56
57
  const project = new Set(pluginsInstallableAgents("project"));
57
58
  return agents.some((id) => !project.has(id) && pluginsInstallableAgents("global").includes(id)) ? "global" : "project";
58
59
  };
59
- const availableForSetup = (setup, scope) => {
60
- if (setup === "plugin") return pluginsInstallableAgents(scope);
61
- if (setup === "skip") return [];
62
- if (setup === "skills") return [...skillsInstallableAgents()];
63
- return [.../* @__PURE__ */ new Set([...skillsInstallableAgents(), ...mcpInstallableAgents(scope === "project" ? "project" : "global")])];
60
+ const warnMissingPluginCommands = (missing, tooling) => {
61
+ if (tooling.setup !== "skills-mcp" && tooling.setup !== "mixed") return;
62
+ const routed = /* @__PURE__ */ new Set([...tooling.skillsAgents, ...tooling.mcpAgents]);
63
+ 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);
64
64
  };
65
65
  const nonEmptyAgents = (ids, fallback) => {
66
66
  const [first, ...rest] = ids;
@@ -75,14 +75,15 @@ const skillsTooling = (ids, fallback) => {
75
75
  agents
76
76
  };
77
77
  };
78
- const skillsMcpTooling = (ids, scope, fallback) => {
79
- const mcpConfigLocation = scope === "project" ? "project" : "global";
80
- let selected = ids;
81
- if (selected.length === 0 && fallback) selected = FALLBACK_SKILLS_AGENTS;
78
+ const skillsMcpTooling = (skillsSelection, mcpSelection, mcpConfigLocation, fallback) => {
79
+ const useFallback = fallback && skillsSelection?.length === 0 && mcpSelection?.length === 0;
80
+ const selectedSkills = (useFallback ? FALLBACK_SKILLS_AGENTS : skillsSelection ?? []).filter((id) => skillsInstallableAgents().includes(id));
81
+ const selectedMcp = (useFallback ? FALLBACK_SKILLS_AGENTS : mcpSelection ?? []).filter((id) => mcpInstallableAgents(mcpConfigLocation).includes(id));
82
+ if (selectedMcp.length === 0) return skillsTooling(selectedSkills, false);
82
83
  return {
83
84
  setup: "skills-mcp",
84
- skillsAgents: selected.filter((id) => skillsInstallableAgents().includes(id)),
85
- mcpAgents: selected.filter((id) => mcpInstallableAgents(mcpConfigLocation).includes(id))
85
+ skillsAgents: selectedSkills,
86
+ mcpAgents: selectedMcp
86
87
  };
87
88
  };
88
89
  const validateMcpConfigLocationSupport = (ids, location) => {
@@ -103,13 +104,18 @@ const pickOrDetectAgents = async (input) => {
103
104
  return selected;
104
105
  }
105
106
  const detected = input.detected.filter((id) => input.available.includes(id));
107
+ const preselected = (input.preselected ?? input.detected).filter((id) => input.available.includes(id));
106
108
  if (input.pickAgents !== void 0) return input.pickAgents({
107
109
  available: [...input.available],
108
- detected
110
+ detected: preselected,
111
+ setup: input.setup
109
112
  });
110
113
  if (input.interactive) return pickInitAgentsInteractively({
111
114
  available: [...input.available],
112
- detected
115
+ detected,
116
+ preselected,
117
+ ...input.missingCommands !== void 0 ? { missingCommands: input.missingCommands } : {},
118
+ setup: input.setup
113
119
  });
114
120
  return detected;
115
121
  };
@@ -229,7 +235,9 @@ const runInit = async (props) => {
229
235
  }
230
236
  const recommended = mode === "recommended";
231
237
  const targets = named.length > 0 ? named : detection.detectedAgents;
232
- let agentSetupChoice = "plugin";
238
+ const missingPluginCommands = missingPluginsCommands(targets, { cwd });
239
+ const unavailablePluginAgents = missingPluginCommands.map((row) => row.agent);
240
+ const pluginReady = (agents) => agents.filter((id) => !unavailablePluginAgents.includes(id));
233
241
  let tooling = { setup: "skip" };
234
242
  let mcpAuth = props.mcpAuth;
235
243
  let mcpConfigLocation = props.mcpConfigLocation ?? "global";
@@ -238,9 +246,9 @@ const runInit = async (props) => {
238
246
  let pluginScope = recommended ? "global" : "project";
239
247
  if (recommended) {
240
248
  pluginScope = "global";
241
- tooling = named.length > 0 ? splitInitTooling(named, "global") : recommendedTooling(detection.detectedAgents);
249
+ tooling = named.length > 0 ? splitInitTooling(named, "global", "global", unavailablePluginAgents) : recommendedTooling(detection.detectedAgents, unavailablePluginAgents);
242
250
  if (named.length === 0 && detection.detectedAgents.length === 0) printInitProgress(NO_AGENTS_FALLBACK_STATUS);
243
- agentSetupChoice = funnelAgentSetup(tooling);
251
+ warnMissingPluginCommands(missingPluginCommands, tooling);
244
252
  mcpAuth = !detection.authenticated && (tooling.setup === "skills-mcp" || tooling.setup === "mixed") ? "oauth" : mcpAuth;
245
253
  } else {
246
254
  const inferred = inferInitAgentSetup({
@@ -251,53 +259,58 @@ const runInit = async (props) => {
251
259
  canAsk: detection.interactive || props.pickAgentSetup !== void 0,
252
260
  ...props.skill !== void 0 ? { skills: props.skill } : {}
253
261
  });
254
- if (inferred.kind === "skip") {
255
- tooling = { setup: "skip" };
256
- agentSetupChoice = "skip";
257
- } else if (inferred.kind === "auto") {
262
+ if (inferred.kind === "skip") tooling = { setup: "skip" };
263
+ else if (inferred.kind === "auto") {
258
264
  const autoAgents = named.length > 0 ? named : detection.detectedAgents;
259
- pluginScope = pluginScopeFor(autoAgents, "project");
260
- tooling = splitInitTooling(autoAgents, pluginScope, mcpConfigLocation);
265
+ pluginScope = pluginScopeFor(pluginReady(autoAgents), "project");
266
+ tooling = splitInitTooling(autoAgents, pluginScope, mcpConfigLocation, unavailablePluginAgents);
267
+ warnMissingPluginCommands(missingPluginCommands, tooling);
261
268
  if (tooling.setup === "skip" && yes && named.length === 0) {
262
269
  tooling = recommendedTooling([]);
263
270
  printInitProgress(NO_AGENTS_FALLBACK_STATUS);
264
271
  }
265
- agentSetupChoice = funnelAgentSetup(tooling);
266
272
  } else {
267
273
  const setup = inferred.kind === "ask" ? await (props.pickAgentSetup ?? pickAgentSetupInteractively)() : inferred.kind;
268
- agentSetupChoice = setup === "skills" ? "skills" : setup;
269
274
  if (setup !== "skip") {
270
275
  pluginScope = pluginScopeFor(targets, "project");
271
- const available = setup === "plugin" ? uniqueAgents([...pluginsInstallableAgents("project"), ...pluginsInstallableAgents("global")]) : availableForSetup(setup, pluginScope);
272
- const selected = await pickOrDetectAgents({
276
+ const namedFallback = named.length === 0;
277
+ const pickAgents = (setup, available) => pickOrDetectAgents({
273
278
  named,
274
279
  available,
275
280
  detected: detection.detectedAgents,
281
+ ...setup === "plugin" ? {
282
+ preselected: pluginReady(detection.detectedAgents),
283
+ missingCommands: missingPluginCommands
284
+ } : {},
276
285
  interactive: detection.interactive,
277
- ...props.pickAgents !== void 0 ? { pickAgents: props.pickAgents } : {}
286
+ setup,
287
+ pickAgents: props.pickAgents
278
288
  });
279
- const namedFallback = named.length === 0;
280
- if (setup === "plugin") {
281
- pluginScope = pluginScopeFor(selected, pluginScope);
282
- tooling = splitInitTooling(selected, pluginScope);
283
- if (tooling.setup !== "plugin" && tooling.setup !== "skip") tooling = splitInitTooling(selected.filter((id) => pluginsInstallableAgents(pluginScope).includes(id)), pluginScope);
284
- agentSetupChoice = funnelAgentSetup(tooling);
285
- } else if (setup === "skills") {
286
- tooling = skillsTooling(selected, namedFallback);
287
- agentSetupChoice = funnelAgentSetup(tooling);
289
+ if (setup === "plugin" || setup === "skills") {
290
+ const selected = await pickAgents(setup, setup === "plugin" ? uniqueAgents([...pluginsInstallableAgents("project"), ...pluginsInstallableAgents("global")]) : [...skillsInstallableAgents()]);
291
+ if (selected === void 0) tooling = { setup: "skip" };
292
+ else if (setup === "plugin") {
293
+ pluginScope = pluginScopeFor(selected, pluginScope);
294
+ tooling = splitInitTooling(selected.filter((id) => pluginsInstallableAgents(pluginScope).includes(id)), pluginScope);
295
+ } else tooling = skillsTooling(selected, namedFallback);
288
296
  } else {
289
- if (selectedSkills === void 0 && inferred.kind === "ask") selectedSkills = props.pickSkills !== void 0 ? await props.pickSkills() : detection.interactive ? await pickInitSkillsInteractively() : void 0;
290
- mcpConfigLocation = props.mcpConfigLocation ?? (yes ? "global" : props.pickMcpConfigLocation !== void 0 ? await props.pickMcpConfigLocation() : detection.interactive ? await pickInitMcpConfigLocationInteractively() : "global");
291
- mcpAuth = props.mcpAuth ?? (yes ? detection.authenticated && props.claimable !== true ? "api-key" : "oauth" : props.pickMcpAuth !== void 0 ? await props.pickMcpAuth({ authenticated: detection.authenticated }) : detection.interactive ? await pickInitMcpAuthInteractively({ authenticated: detection.authenticated }) : detection.authenticated && props.claimable !== true ? "api-key" : "oauth");
292
- validateMcpConfigLocationSupport(selected, mcpConfigLocation);
293
- tooling = skillsMcpTooling(selected, mcpConfigLocation, namedFallback);
294
- agentSetupChoice = "skills-mcp";
297
+ const skillsAvailable = skillsInstallableAgents();
298
+ const skillsSelection = named.length > 0 ? named.filter((id) => skillsAvailable.includes(id)) : await pickAgents("skills", skillsAvailable);
299
+ if (skillsSelection !== void 0 && selectedSkills === void 0 && inferred.kind === "ask") selectedSkills = props.pickSkills !== void 0 ? await props.pickSkills() : detection.interactive ? await pickInitSkillsInteractively() : void 0;
300
+ const mcpSelection = named.length > 0 ? named : await pickAgents("mcp", mcpInstallableAgents("global"));
301
+ if (mcpSelection !== void 0) {
302
+ mcpConfigLocation = props.mcpConfigLocation ?? (yes ? "global" : props.pickMcpConfigLocation !== void 0 ? await props.pickMcpConfigLocation() : detection.interactive ? await pickInitMcpConfigLocationInteractively() : "global");
303
+ if (mcpSelection.length > 0) validateMcpConfigLocationSupport(mcpSelection, mcpConfigLocation);
304
+ const pickAuth = props.pickMcpAuth ?? (detection.interactive ? pickInitMcpAuthInteractively : void 0);
305
+ mcpAuth = props.mcpAuth ?? (yes ? void 0 : await pickAuth?.({ authenticated: detection.authenticated })) ?? (detection.authenticated && props.claimable !== true ? "api-key" : "oauth");
306
+ }
307
+ tooling = skillsMcpTooling(skillsSelection, mcpSelection, mcpConfigLocation, namedFallback);
295
308
  }
296
309
  }
297
310
  }
298
311
  }
299
- funnel.agentSetup = agentSetupChoice === "plugin" || agentSetupChoice === "skills-mcp" || agentSetupChoice === "skills" || agentSetupChoice === "mixed" || agentSetupChoice === "skip" ? agentSetupChoice : funnelAgentSetup(tooling);
300
- const usesMcp = tooling.setup === "skills-mcp" || tooling.setup === "mixed";
312
+ funnel.agentSetup = funnelAgentSetup(tooling);
313
+ const usesMcp = (tooling.setup === "skills-mcp" || tooling.setup === "mixed") && tooling.mcpAgents.length > 0;
301
314
  if (mcpAuth === void 0 && usesMcp && (!detection.authenticated || props.claimable === true)) mcpAuth = "oauth";
302
315
  delayMcp = usesMcp && (mcpAuth === "api-key" || props.mcpProjectScoped === true);
303
316
  const mcpOauth = mcpAuth === "oauth";
@@ -355,7 +368,7 @@ const runInit = async (props) => {
355
368
  funnel.link = "skipped";
356
369
  } else if (props.pickProjectSetup !== void 0 || detection.interactive) projectSetup = await (props.pickProjectSetup ?? pickInitProjectSetupInteractively)();
357
370
  else throw new Error(NON_TTY_LINK_NEEDS_AUTH);
358
- if (mcpAuth === "api-key" && (projectSetup === "claimable" || props.claimable === true)) throw new Error(CLAIMABLE_MCP_API_KEY);
371
+ if (usesMcp && mcpAuth === "api-key" && (projectSetup === "claimable" || props.claimable === true)) throw new Error(CLAIMABLE_MCP_API_KEY);
359
372
  if (projectSetup === "claimable") {
360
373
  printInitProgress(PROGRESS.claim);
361
374
  await (props.createClaimable ?? defaultCreateClaimable)({
@@ -529,8 +542,8 @@ const runInit = async (props) => {
529
542
  {
530
543
  label: "Agents",
531
544
  value: agentsRowValue({
532
- setup: funnel.agentSetup,
533
- agents: funnel.agentsInstalled
545
+ tooling,
546
+ installed: funnel.agentsInstalled
534
547
  })
535
548
  },
536
549
  {
@@ -578,8 +591,8 @@ const runInit = async (props) => {
578
591
  {
579
592
  label: "Agents",
580
593
  value: agentsRowValue({
581
- setup: funnel.agentSetup,
582
- agents: funnel.agentsInstalled
594
+ tooling,
595
+ installed: funnel.agentsInstalled
583
596
  })
584
597
  },
585
598
  {
@@ -3,7 +3,7 @@ import { getCliName } from "../utils/cli_name.js";
3
3
  import { PROGRESS } from "./copy.js";
4
4
  import { assertNamedAgentTooling, chooseYesAgentTooling, collectYesAgents, initYesSupportedAgents, noDetectedAgentsMessage, planAgentSteps, planToolingSteps, planYesAgentSteps, resolveInitAgentSetup } from "./plan.js";
5
5
  import { detectAgent } from "./detect_host.js";
6
- import { canPickAgentsInteractively } from "../utils/agent_picker.js";
6
+ import { AgentSelectionSkipped, canPickAgentsInteractively } from "../utils/agent_picker.js";
7
7
  import { mcpInstallError, reportMcpInstall } from "../commands/mcp.js";
8
8
  import { installPlugins, pluginsInstallError, reportPluginsInstall } from "../commands/plugins.js";
9
9
  import { installSkills, reportSkillsInstall, skillsInstallError } from "../commands/skills.js";
@@ -66,45 +66,61 @@ const runToolingSteps = async (steps, options) => {
66
66
  ...options.operations
67
67
  };
68
68
  const reportProps = { output: options.output };
69
+ const sharedOptions = {
70
+ cwd: options.cwd,
71
+ allowAgentSkip: options.allowAgentSkip
72
+ };
73
+ const completed = [];
69
74
  for (const step of steps) {
70
75
  const label = options.narrate === "human" ? TOOLING_STEP_LABELS[step.kind] : void 0;
71
76
  if (label !== void 0) process.stdout.write(`${label}\n`);
72
77
  else log.info("Running `%s %s`", getCliName(), toolingStepCommandText(step));
73
78
  log.debug("Running `%s %s`", getCliName(), toolingStepCommandText(step));
74
- switch (step.kind) {
75
- case "plugins": {
76
- const outcome = await raceSigint(ops.installPlugins({
77
- ...step.options,
78
- cwd: options.cwd
79
- }));
80
- reportPluginsInstall(reportProps, outcome);
81
- const error = pluginsInstallError(outcome);
82
- if (error) throw error;
83
- break;
84
- }
85
- case "skills": {
86
- const outcome = await raceSigint(ops.installSkills({
87
- ...step.options,
88
- cwd: options.cwd
89
- }));
90
- reportSkillsInstall(reportProps, outcome);
91
- const error = skillsInstallError(outcome);
92
- if (error) throw error;
93
- break;
79
+ try {
80
+ switch (step.kind) {
81
+ case "plugins": {
82
+ const outcome = await raceSigint(ops.installPlugins({
83
+ ...step.options,
84
+ ...sharedOptions
85
+ }));
86
+ reportPluginsInstall(reportProps, outcome);
87
+ const error = pluginsInstallError(outcome);
88
+ if (error) throw error;
89
+ break;
90
+ }
91
+ case "skills": {
92
+ const outcome = await raceSigint(ops.installSkills({
93
+ ...step.options,
94
+ ...sharedOptions
95
+ }));
96
+ reportSkillsInstall(reportProps, outcome);
97
+ const error = skillsInstallError(outcome);
98
+ if (error) throw error;
99
+ break;
100
+ }
101
+ case "mcp": {
102
+ const outcome = await raceSigint(ops.installMcp({
103
+ ...step.options,
104
+ ...sharedOptions
105
+ }));
106
+ reportMcpInstall(reportProps, outcome);
107
+ const error = mcpInstallError(outcome);
108
+ if (error) throw error;
109
+ break;
110
+ }
111
+ default: return step;
94
112
  }
95
- case "mcp": {
96
- const outcome = await raceSigint(ops.installMcp({
97
- ...step.options,
98
- cwd: options.cwd
99
- }));
100
- reportMcpInstall(reportProps, outcome);
101
- const error = mcpInstallError(outcome);
102
- if (error) throw error;
103
- break;
104
- }
105
- default: return step;
113
+ completed.push(step.kind);
114
+ } catch (error) {
115
+ if (!(error instanceof AgentSelectionSkipped)) throw error;
106
116
  }
107
117
  }
118
+ return completed;
119
+ };
120
+ const agentSetupResult = (setup, completed) => {
121
+ if (completed.length === 0) return "skip";
122
+ if (setup === "skills-mcp" && completed.length === 1) return completed[0] === "skills" ? "skills" : "mcp";
123
+ return setup;
108
124
  };
109
125
  const yesAgentsFromOptions = async (options) => collectYesAgents({
110
126
  detected: () => (options.detectProjectAgents ?? defaultProjectAgents)(options.cwd),
@@ -119,47 +135,44 @@ const yesMiss = () => new Error(noDetectedAgentsMessage({
119
135
  const runAgentTooling = async (options) => {
120
136
  const yes = options.yes;
121
137
  const named = options.agents ?? [];
138
+ const runSetup = async (setup, steps) => agentSetupResult(setup, await runToolingSteps(steps, {
139
+ ...options,
140
+ allowAgentSkip: !yes
141
+ }));
122
142
  if (named.length > 0) {
123
143
  assertNamedAgentTooling(named, options.command ?? "init");
124
144
  const tooling = chooseYesAgentTooling(named);
125
- await runToolingSteps(planToolingSteps(tooling, {
145
+ return runSetup(tooling.setup, planToolingSteps(tooling, {
126
146
  yes,
127
147
  named: true
128
- }), options);
129
- return tooling.setup;
130
- }
131
- if (options.agentSetup !== void 0) {
132
- await runToolingSteps(planAgentSteps({
133
- yes,
134
- agentSetup: options.agentSetup
135
- }), options);
136
- return options.agentSetup;
148
+ }));
137
149
  }
150
+ if (options.agentSetup !== void 0) return runSetup(options.agentSetup, planAgentSteps({
151
+ yes,
152
+ agentSetup: options.agentSetup
153
+ }));
138
154
  if (yes) {
139
155
  if (options.hasProjectPlugins !== void 0) {
140
156
  const agentSetup = await options.hasProjectPlugins(options.cwd) ? "plugin" : "skills-mcp";
141
- await runToolingSteps(planAgentSteps({
157
+ return runSetup(agentSetup, planAgentSteps({
142
158
  yes: true,
143
159
  agentSetup
144
- }), options);
145
- return agentSetup;
160
+ }));
146
161
  }
147
162
  const agents = await yesAgentsFromOptions(options);
148
163
  const tooling = chooseYesAgentTooling(agents);
149
164
  if (tooling.setup === "skip") throw yesMiss();
150
- await runToolingSteps(planYesAgentSteps(tooling), options);
151
- return tooling.setup;
165
+ return runSetup(tooling.setup, planYesAgentSteps(tooling));
152
166
  }
153
167
  const interactive = options.pickAgentSetup !== void 0 || canPickAgentsInteractively();
154
168
  const agentSetup = await resolveInitAgentSetup({
155
169
  interactive,
156
170
  pick: options.pickAgentSetup ?? pickAgentSetupInteractively
157
171
  });
158
- await runToolingSteps(planAgentSteps({
172
+ return runSetup(agentSetup, planAgentSteps({
159
173
  yes,
160
174
  agentSetup
161
- }), options);
162
- return agentSetup;
175
+ }));
163
176
  };
164
177
  //#endregion
165
178
  export { runAgentTooling, runToolingSteps };
@@ -1,10 +1,10 @@
1
1
  import { CONFIG_INIT_SERVICES } from "../config_template.js";
2
- import { AGENT_DETECTED_DESCRIPTION, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_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, 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 } from "./copy.js";
2
+ 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";
3
3
  import { installedPackageManagers } from "../utils/package_manager.js";
4
4
  import { POSTGRES_SERVICE_CHOICE, keepPostgresSelected } from "../utils/service_picker.js";
5
5
  import { getAgentDisplayName, listMcpAgentIds } from "../mcp/agents.js";
6
6
  import { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
7
- import { canPickAgentsInteractively } from "../utils/agent_picker.js";
7
+ import { canPickAgentsInteractively, pickAgentsInteractively } from "../utils/agent_picker.js";
8
8
  import { NEON_SKILL_CATALOG, listSkillIds } from "../skills/catalog.js";
9
9
  import { InitCancelled, restoreCursorVisibilityOnAbort } from "./cancelled.js";
10
10
  import chalk from "chalk";
@@ -71,24 +71,23 @@ const pickAgentSetupInteractively = async () => {
71
71
  };
72
72
  const pickInitAgentsInteractively = async (input) => {
73
73
  requireInteractive();
74
- if (input.available.length === 0) throw new Error("No coding agents are available to pick.");
75
74
  const detected = new Set(input.detected);
76
- const { agents } = await prompts({
77
- onState: restoreCursorOnAbort,
78
- type: "multiselect",
79
- name: "agents",
80
- message: AGENT_PICKER_MESSAGE,
81
- instructions: false,
82
- min: 1,
83
- choices: input.available.map((id) => ({
84
- value: id,
85
- title: `${getAgentDisplayName(id)} (${id})`,
86
- description: detected.has(id) ? AGENT_DETECTED_DESCRIPTION : AGENT_OTHER_DESCRIPTION,
87
- selected: detected.has(id)
88
- }))
75
+ const missing = new Map((input.missingCommands ?? []).map((row) => [row.agent, row.command]));
76
+ const selected = await pickAgentsInteractively({
77
+ onCancel: aborted,
78
+ message: input.setup === "skills" ? AGENT_SKILLS_PICKER_MESSAGE : input.setup === "mcp" ? AGENT_MCP_PICKER_MESSAGE : AGENT_PICKER_MESSAGE,
79
+ skipMessage: input.setup === "plugin" ? AGENT_PLUGIN_SKIP_MESSAGE : input.setup === "skills" ? AGENT_SKILLS_SKIP_MESSAGE : AGENT_MCP_SKIP_MESSAGE,
80
+ selected: input.preselected ?? input.detected,
81
+ choices: input.available.map((id) => {
82
+ const command = missing.get(id);
83
+ return {
84
+ id,
85
+ title: `${getAgentDisplayName(id)} (${id})`,
86
+ description: command !== void 0 ? agentMissingCommandDescription(command) : detected.has(id) ? AGENT_DETECTED_DESCRIPTION : AGENT_OTHER_DESCRIPTION
87
+ };
88
+ })
89
89
  });
90
- if (!Array.isArray(agents)) return aborted();
91
- return agents.filter((id) => typeof id === "string");
90
+ return selected.length > 0 ? selected : void 0;
92
91
  };
93
92
  const pickInitSkillsInteractively = async () => {
94
93
  requireInteractive();
@@ -1,14 +1,19 @@
1
1
  import { getAgentDisplayName, tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
- import { detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, pluginsInstallableAgents } from "./targets.js";
3
+ import { detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, missingPluginsCommands, pluginsInstallableAgents } 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
  //#region 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
9
  const assertPluginsCanRun = (options) => {
9
10
  if (options.yes || options.interactive || options.hasAgents) return;
10
11
  throw new Error("No interactive terminal. Pass -y to install into detected agents, or --agent <name> to name them.");
11
12
  };
13
+ const commandLookup = (options) => ({
14
+ cwd: options.cwd,
15
+ ...options.commandPath !== void 0 ? { path: options.commandPath } : {}
16
+ });
12
17
  async function resolvePluginsPlan(options) {
13
18
  assertPluginsCanRun({
14
19
  yes: options.yes,
@@ -29,10 +34,18 @@ async function resolvePluginsPlan(options) {
29
34
  detectAgent: options.detectAgent ?? detectAgent,
30
35
  acceptHost: (id) => availableSet.has(id) || getPluginsTargetName(id) !== void 0
31
36
  }) : scoped;
37
+ const missingDetected = new Map((prompt ? missingPluginsCommands(detected, commandLookup(options)) : []).map((row) => [row.agent, row.command]));
38
+ const choices = agentChoicesFrom(available, detected).map((choice) => {
39
+ const command = missingDetected.get(choice.id);
40
+ return command === void 0 ? choice : {
41
+ ...choice,
42
+ description: `detected, but "${command}" is not on PATH`
43
+ };
44
+ });
32
45
  const selected = await resolveAgentSelection({
33
46
  specified: options.agents,
34
- choices: agentChoicesFrom(available, detected),
35
- detected,
47
+ choices,
48
+ detected: detected.filter((id) => !missingDetected.has(id)),
36
49
  message: "Which coding agents should get the Neon plugin? (space to toggle, enter to confirm)",
37
50
  nonInteractiveMessage: noDetectedAgentsMessage({
38
51
  scope,
@@ -64,6 +77,20 @@ async function resolvePluginsPlan(options) {
64
77
  }
65
78
  agents.push(id);
66
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));
84
+ const installable = agents.filter((id) => !missingAgents.has(id));
85
+ return {
86
+ scope,
87
+ agents: installable,
88
+ skipped,
89
+ userScopeSkipped,
90
+ missingCommands,
91
+ targets: installable.length === 0 ? [] : mappedPluginsTargets(installable, scope)
92
+ };
93
+ }
67
94
  if (agents.length === 0) {
68
95
  if (userScopeSkipped.length > 0 && skipped.length === 0) {
69
96
  const names = userScopeSkipped.map((id) => getAgentDisplayName(id)).join(", ");
@@ -76,8 +103,9 @@ async function resolvePluginsPlan(options) {
76
103
  agents,
77
104
  skipped,
78
105
  userScopeSkipped,
106
+ missingCommands,
79
107
  targets: mappedPluginsTargets(agents, scope)
80
108
  };
81
109
  }
82
110
  //#endregion
83
- export { assertPluginsCanRun, resolvePluginsPlan };
111
+ export { assertPluginsCanRun, missingPluginsCommandsError, resolvePluginsPlan };
@@ -1,27 +1,67 @@
1
1
  import { detectInstalledAgents, listMcpAgentIds, tryResolveAddMcpAgentId, uniqueAgentIds } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
+ import { delimiter, dirname, join, resolve } from "node:path";
4
+ import which from "which";
3
5
  import { detectProjectAgents } from "add-mcp";
4
6
  //#region src/plugins/targets.ts
7
+ const CLAUDE_CODE = { target: "claude-code" };
5
8
  const PLUGINS_TARGET_BY_TYPE = {
6
- cursor: "cursor",
7
- vscode: "vscode",
8
- "claude-code": "claude-code",
9
- "claude-desktop": "claude-code",
10
- codex: "codex",
11
- "github-copilot-cli": "github-copilot",
12
- "grok-build": "grok"
9
+ cursor: { target: "cursor" },
10
+ vscode: { target: "vscode" },
11
+ "claude-code": CLAUDE_CODE,
12
+ "claude-desktop": CLAUDE_CODE,
13
+ codex: { target: "codex" },
14
+ "github-copilot-cli": {
15
+ target: "github-copilot",
16
+ command: "copilot"
17
+ },
18
+ "grok-build": {
19
+ target: "grok",
20
+ command: "grok"
21
+ }
13
22
  };
23
+ const pluginsTargets = () => [...new Set(Object.values(PLUGINS_TARGET_BY_TYPE))];
14
24
  const USER_SCOPE_ONLY_TARGETS = /* @__PURE__ */ new Set([
15
25
  "vscode",
16
26
  "github-copilot",
17
27
  "grok"
18
28
  ]);
19
- function getPluginsTargetName(agent) {
29
+ /** npx runs the plugins CLI with every `node_modules/.bin` from `cwd` up to the root ahead of PATH. */
30
+ const pluginsCommandSearchPath = (cwd, path) => {
31
+ const bins = [];
32
+ let dir = resolve(cwd);
33
+ for (;;) {
34
+ bins.push(join(dir, "node_modules", ".bin"));
35
+ const parent = dirname(dir);
36
+ if (parent === dir) break;
37
+ dir = parent;
38
+ }
39
+ return [...bins, path].join(delimiter);
40
+ };
41
+ const missingPluginsCommands = (agents, options) => {
42
+ const searchPath = pluginsCommandSearchPath(options.cwd, options.path ?? process.env.PATH ?? "");
43
+ const missing = [];
44
+ for (const agent of uniqueAgentIds(agents)) {
45
+ const command = getPluginsTarget(agent)?.command;
46
+ if (command !== void 0 && which.sync(command, {
47
+ nothrow: true,
48
+ path: searchPath
49
+ }) === null) missing.push({
50
+ agent,
51
+ command
52
+ });
53
+ }
54
+ return missing;
55
+ };
56
+ function getPluginsTarget(agent) {
20
57
  if (Object.prototype.hasOwnProperty.call(PLUGINS_TARGET_BY_TYPE, agent)) return PLUGINS_TARGET_BY_TYPE[agent];
21
58
  const id = tryResolveAddMcpAgentId(agent);
22
59
  if (!id) return void 0;
23
60
  return PLUGINS_TARGET_BY_TYPE[id];
24
61
  }
62
+ function getPluginsTargetName(agent) {
63
+ return getPluginsTarget(agent)?.target;
64
+ }
25
65
  function supportsPlugins(agent) {
26
66
  return getPluginsTargetName(agent) !== void 0;
27
67
  }
@@ -65,4 +105,4 @@ const mappedPluginsTargets = (agents, scope) => {
65
105
  return mapped;
66
106
  };
67
107
  //#endregion
68
- export { detectInstallablePluginsAgents, detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, pluginsInstallableAgents, pluginsMappedAgents, supportsPlugins };
108
+ export { detectInstallablePluginsAgents, detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, missingPluginsCommands, pluginsInstallableAgents, pluginsMappedAgents, pluginsTargets, supportsPlugins };