neon 4.10.1 → 4.11.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
@@ -1,23 +1,223 @@
1
+ import { getCliName } from "../utils/cli_name.js";
2
+ import { tryResolveAddMcpAgentId, uniqueAgentIds } from "../mcp/agents.js";
3
+ import { mcpInstallableAgents } from "../mcp/targets.js";
4
+ import { supportsSkills } from "./agents.js";
5
+ import { pluginsInstallableAgents } from "../plugins/targets.js";
6
+ import { skillsInstallableAgents } from "../skills/targets.js";
7
+ import { agentArgv } from "../utils/agent_flag.js";
1
8
  import { isAbsolute, join, relative, resolve } from "node:path";
2
9
  //#region src/init/plan.ts
3
10
  const directoryIsEmpty = (names) => names.filter((name) => name !== ".git").length === 0;
4
- const bootstrapInitStep = (yes) => yes ? [
11
+ const bootstrapInitStep = (yes, agents = []) => [...yes ? [
5
12
  "bootstrap",
6
13
  ".",
7
14
  "--default"
8
- ] : ["bootstrap", "."];
15
+ ] : ["bootstrap", "."], ...agentArgv(agents)];
9
16
  const projectContextFile = (projectDir, contextFile) => {
10
17
  const resolved = isAbsolute(contextFile) ? contextFile : resolve(projectDir, contextFile);
11
18
  const rel = relative(projectDir, resolved);
12
19
  if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return join(projectDir, ".neon");
13
20
  return resolved;
14
21
  };
22
+ /** Linking a template with neon.ts requires installed dependencies because env pull evaluates it. */
23
+ const postScaffoldActions = (input) => {
24
+ const actions = [];
25
+ if (input.git) actions.push("git");
26
+ if (input.agentSetup !== "skip") actions.push("agent");
27
+ const canLink = input.link && !(input.hasNeonConfig && !input.install);
28
+ if (canLink && input.hasNeonConfig && input.install) {
29
+ actions.push("install");
30
+ actions.push("link");
31
+ return actions;
32
+ }
33
+ if (canLink) actions.push("link");
34
+ if (input.install) actions.push("install");
35
+ return actions;
36
+ };
15
37
  const planAgentSteps = (input) => {
16
38
  const y = input.yes ? ["-y"] : [];
17
39
  if (input.agentSetup === "plugin") return [["plugins", ...y]];
18
40
  if (input.agentSetup === "skills-mcp") return [["skills", ...y], ["mcp", ...y]];
19
41
  return [];
20
42
  };
43
+ const planToolingSteps = (tooling, options) => {
44
+ const prefix = options.yes ? ["-y"] : [];
45
+ const flagsFor = (ids) => options.named ? [...prefix, ...agentArgv(ids)] : [...prefix];
46
+ switch (tooling.setup) {
47
+ case "skip": return [];
48
+ case "plugin": return [["plugins", ...flagsFor(tooling.agents)]];
49
+ case "skills-mcp": {
50
+ const steps = [];
51
+ if (tooling.skillsAgents.length > 0) steps.push(["skills", ...flagsFor(tooling.skillsAgents)]);
52
+ if (tooling.mcpAgents.length > 0) steps.push(["mcp", ...flagsFor(tooling.mcpAgents)]);
53
+ return steps;
54
+ }
55
+ default: return tooling;
56
+ }
57
+ };
58
+ const noDetectedAgentsMessage = (input) => {
59
+ const lead = input.scope === "project" ? "No coding agents detected in this project." : "No coding agents detected.";
60
+ const name = input.nameAgent === true;
61
+ let hint;
62
+ if (input.fix === "pass-yes") {
63
+ const detected = input.scope === "project" ? "Pass -y to use project folders, else the host CLI agent" : "Pass -y to use installed apps, else the host CLI agent";
64
+ hint = name ? `${detected}, --agent <name> to name them, or run from a terminal to pick one.` : `${detected}, or run from a terminal to pick one.`;
65
+ } else hint = name ? "Pass --agent <name>, run this command from a supported agent, or omit -y in a terminal to pick one." : "Run this command from a supported agent, or omit -y in a terminal to pick one.";
66
+ return `${lead} ${hint} Supported agents: ${input.supported.join(", ")}`;
67
+ };
68
+ const INIT_NEEDS_YES_OR_TERMINAL = "No interactive terminal. Pass -y to use defaults, or run this command in a terminal to pick.";
69
+ const UNKNOWN_AGENT_COMMANDS = ["link"];
70
+ const unknownAgentArgHint = (command, cliName) => `${cliName} ${command} has no --agent. Pass --project-id <id> to link without a TTY, or run ${cliName} ${command} in a terminal.`;
71
+ const BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
72
+ "-y",
73
+ "--yes",
74
+ "--default",
75
+ "--force",
76
+ "-h",
77
+ "--help",
78
+ "-v",
79
+ "--version",
80
+ "--color",
81
+ "--no-color",
82
+ "--analytics",
83
+ "--no-analytics",
84
+ "--oauth",
85
+ "--project",
86
+ "--global",
87
+ "--read-only",
88
+ "--readonly",
89
+ "--clear",
90
+ "--checks",
91
+ "--no-checks",
92
+ "--env-pull",
93
+ "--no-env-pull",
94
+ "--no-agent-setup",
95
+ "--no-link",
96
+ "--list",
97
+ "--list-templates",
98
+ "--force-auth",
99
+ "--no-force-auth"
100
+ ]);
101
+ const isUnknownAgentCommand = (token) => {
102
+ for (const command of UNKNOWN_AGENT_COMMANDS) if (command === token) return true;
103
+ return false;
104
+ };
105
+ const commandFromArgv = (argv) => {
106
+ let skipValue = false;
107
+ for (const token of argv) {
108
+ if (skipValue) {
109
+ skipValue = false;
110
+ continue;
111
+ }
112
+ if (token === "--") break;
113
+ if (token.startsWith("-")) {
114
+ const flag = token.split("=")[0];
115
+ if (flag !== void 0 && !token.includes("=") && !BOOLEAN_FLAGS.has(flag)) skipValue = true;
116
+ continue;
117
+ }
118
+ if (isUnknownAgentCommand(token)) return token;
119
+ }
120
+ };
121
+ const rewriteUnknownAgentArg = (input) => {
122
+ if (input.message !== "Unknown argument: agent" && input.message !== "Unknown argument: a") return;
123
+ const command = commandFromArgv(input.argv);
124
+ if (command === void 0) return;
125
+ return unknownAgentArgHint(command, input.cliName);
126
+ };
127
+ const initYesSupportedAgents = () => uniqueAgentIds([
128
+ ...pluginsInstallableAgents("project"),
129
+ ...skillsInstallableAgents(),
130
+ ...mcpInstallableAgents("global")
131
+ ]);
132
+ const initPluginAgents = () => pluginsInstallableAgents("project");
133
+ const initSkillsMcpAgents = () => {
134
+ const plugin = new Set(initPluginAgents());
135
+ return initYesSupportedAgents().filter((id) => !plugin.has(id));
136
+ };
137
+ const resolveYesAgentList = (input) => {
138
+ if (input.detected.length > 0) return uniqueAgentIds(input.detected);
139
+ if (input.host !== null) return [input.host];
140
+ return [];
141
+ };
142
+ async function collectYesAgents(sources) {
143
+ const detected = await sources.detected();
144
+ if (detected.length > 0) return resolveYesAgentList({
145
+ detected,
146
+ host: null
147
+ });
148
+ const rawHost = sources.detectAgent();
149
+ const accept = sources.acceptHost ?? (() => true);
150
+ const host = rawHost !== null && accept(rawHost) ? rawHost : null;
151
+ return resolveYesAgentList({
152
+ detected,
153
+ host
154
+ });
155
+ }
156
+ const chooseYesAgentTooling = (agents) => {
157
+ const projectPlugin = new Set(pluginsInstallableAgents("project"));
158
+ const [pluginFirst, ...pluginRest] = agents.filter((id) => projectPlugin.has(id));
159
+ if (pluginFirst !== void 0) return {
160
+ setup: "plugin",
161
+ agents: [pluginFirst, ...pluginRest]
162
+ };
163
+ const globalMcp = new Set(mcpInstallableAgents("global"));
164
+ const skillsAgents = agents.filter((id) => supportsSkills(id));
165
+ const mcpAgents = agents.filter((id) => globalMcp.has(id));
166
+ if (skillsAgents.length === 0 && mcpAgents.length === 0) return { setup: "skip" };
167
+ return {
168
+ setup: "skills-mcp",
169
+ skillsAgents,
170
+ mcpAgents
171
+ };
172
+ };
173
+ const planYesAgentSteps = (tooling) => planToolingSteps(tooling, {
174
+ yes: true,
175
+ named: false
176
+ });
177
+ const NAMED_AGENTS_UNSUPPORTED = "None of the selected agents can install the Neon plugin, skills, or MCP.";
178
+ const NAMED_AGENTS_MIXED = "Cannot install the plugin and skills/MCP in one run.";
179
+ const quoteIfNeeded = (value) => /[\s"'\\]/.test(value) ? `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"` : value;
180
+ const mixedRerun = (command, ids, context) => {
181
+ const tokens = [getCliName(), command];
182
+ if (command === "bootstrap" && context?.directory !== void 0 && context.directory.length > 0) tokens.push(quoteIfNeeded(context.directory));
183
+ if (context?.yes === true) tokens.push(command === "bootstrap" ? "--default" : "-y");
184
+ tokens.push(...ids.flatMap((id) => ["--agent", id]));
185
+ return `\`${tokens.join(" ")}\``;
186
+ };
187
+ const namedAgentsMixedMessage = (named, command, context) => {
188
+ const plugin = new Set(initPluginAgents());
189
+ const pluginNamed = uniqueAgentIds(named.filter((id) => plugin.has(id)));
190
+ const otherNamed = uniqueAgentIds(named.filter((id) => !plugin.has(id)));
191
+ return `${NAMED_AGENTS_MIXED} Plugin: ${pluginNamed.join(", ")}. Skills/MCP: ${otherNamed.join(", ")}. Re-run ${mixedRerun(command, pluginNamed, context)} or ${mixedRerun(command, otherNamed, context)}.`;
192
+ };
193
+ const namedAgentsNeedSplit = (named, tooling) => {
194
+ if (tooling.setup === "skip") return false;
195
+ if (tooling.setup === "plugin") {
196
+ const used = new Set(tooling.agents);
197
+ const leftover = named.filter((id) => !used.has(id));
198
+ return chooseYesAgentTooling(leftover).setup !== "skip";
199
+ }
200
+ const used = /* @__PURE__ */ new Set([...tooling.skillsAgents, ...tooling.mcpAgents]);
201
+ return named.some((id) => !used.has(id));
202
+ };
203
+ const assertNamedAgentTooling = (named, command = "init", context) => {
204
+ if (named.length === 0) return;
205
+ const tooling = chooseYesAgentTooling(named);
206
+ if (tooling.setup === "skip") throw new Error(`${NAMED_AGENTS_UNSUPPORTED} Supported agents: ${initYesSupportedAgents().join(", ")}`);
207
+ if (namedAgentsNeedSplit(named, tooling)) throw new Error(namedAgentsMixedMessage(named, command, context));
208
+ };
209
+ const resolveNamedAgents = (raw) => {
210
+ if (raw.length === 0) return [];
211
+ const available = initYesSupportedAgents();
212
+ const resolved = [];
213
+ for (const item of raw) {
214
+ if (item === "*") throw new Error("--agent * is not accepted. Pass --agent <name> for each coding agent, or omit --agent to detect or pick.");
215
+ const id = tryResolveAddMcpAgentId(item);
216
+ if (!id) throw new Error(`Unknown agent: "${item}". Supported agents: ${available.join(", ")}`);
217
+ resolved.push(id);
218
+ }
219
+ return uniqueAgentIds(resolved);
220
+ };
21
221
  const planExistingInit = (input) => {
22
222
  const steps = [...planAgentSteps(input)];
23
223
  if (!input.linked) steps.push(input.yes ? ["link", "--yes"] : ["link"]);
@@ -30,9 +230,8 @@ const planExistingInit = (input) => {
30
230
  return steps;
31
231
  };
32
232
  const resolveInitAgentSetup = async (input) => {
33
- if (input.yes) return input.hasProjectPlugins ? "plugin" : "skills-mcp";
34
233
  if (input.interactive) return input.pick();
35
- return "skills-mcp";
234
+ throw new Error(INIT_NEEDS_YES_OR_TERMINAL);
36
235
  };
37
236
  const childArgv = (step, forward) => {
38
237
  const args = [...step];
@@ -44,4 +243,4 @@ const childArgv = (step, forward) => {
44
243
  return args;
45
244
  };
46
245
  //#endregion
47
- export { bootstrapInitStep, childArgv, directoryIsEmpty, planAgentSteps, planExistingInit, projectContextFile, resolveInitAgentSetup };
246
+ export { INIT_NEEDS_YES_OR_TERMINAL, NAMED_AGENTS_MIXED, NAMED_AGENTS_UNSUPPORTED, assertNamedAgentTooling, bootstrapInitStep, childArgv, chooseYesAgentTooling, collectYesAgents, directoryIsEmpty, initPluginAgents, initSkillsMcpAgents, initYesSupportedAgents, namedAgentsMixedMessage, namedAgentsNeedSplit, noDetectedAgentsMessage, planAgentSteps, planExistingInit, planToolingSteps, planYesAgentSteps, postScaffoldActions, projectContextFile, resolveInitAgentSetup, resolveNamedAgents, resolveYesAgentList, rewriteUnknownAgentArg };
@@ -1,17 +1,13 @@
1
1
  import { log } from "../log.js";
2
2
  import { getCliName } from "../utils/cli_name.js";
3
3
  import { AUTH_CHILD } from "./child.js";
4
- import { childArgv, planAgentSteps, resolveInitAgentSetup } from "./plan.js";
5
- import { detectInstallablePluginsAgents } from "../plugins/targets.js";
4
+ import { assertNamedAgentTooling, childArgv, chooseYesAgentTooling, collectYesAgents, initYesSupportedAgents, noDetectedAgentsMessage, planAgentSteps, planToolingSteps, planYesAgentSteps, resolveInitAgentSetup } from "./plan.js";
6
5
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
6
+ import { detectAgent } from "./detect_host.js";
7
7
  import { pickAgentSetupInteractively } from "./wizard.js";
8
+ import { detectProjectAgents } from "add-mcp";
8
9
  //#region src/init/tooling.ts
9
- const defaultHasProjectPlugins = async (cwd) => {
10
- return (await detectInstallablePluginsAgents({
11
- scope: "project",
12
- cwd
13
- })).length > 0;
14
- };
10
+ const defaultProjectAgents = (cwd) => detectProjectAgents(cwd);
15
11
  const runInitSteps = async (steps, options) => {
16
12
  for (const step of steps) {
17
13
  log.info("Running `%s %s`", getCliName(), step.join(" "));
@@ -20,20 +16,60 @@ const runInitSteps = async (steps, options) => {
20
16
  if (!await options.run(argv, options.cwd, command !== void 0 && AUTH_CHILD.has(command) ? options.authEnv : void 0)) throw new Error(`\`${getCliName()} ${step.join(" ")}\` failed.`);
21
17
  }
22
18
  };
19
+ const yesAgentsFromOptions = async (options) => collectYesAgents({
20
+ detected: () => (options.detectProjectAgents ?? defaultProjectAgents)(options.cwd),
21
+ detectAgent: options.detectAgent ?? detectAgent
22
+ });
23
+ const yesMiss = () => new Error(noDetectedAgentsMessage({
24
+ scope: "project",
25
+ supported: initYesSupportedAgents(),
26
+ fix: "run-without-yes",
27
+ nameAgent: true
28
+ }));
23
29
  const runAgentTooling = async (options) => {
24
30
  const yes = options.yes;
25
- const hasProjectPlugins = yes ? await (options.hasProjectPlugins ?? defaultHasProjectPlugins)(options.cwd) : false;
26
- const interactive = !yes && (options.pickAgentSetup !== void 0 || canPickAgentsInteractively());
31
+ const named = options.agents ?? [];
32
+ if (named.length > 0) {
33
+ assertNamedAgentTooling(named, options.command ?? "init");
34
+ const tooling = chooseYesAgentTooling(named);
35
+ await runInitSteps(planToolingSteps(tooling, {
36
+ yes,
37
+ named: true
38
+ }), options);
39
+ return tooling.setup;
40
+ }
41
+ if (options.agentSetup !== void 0) {
42
+ await runInitSteps(planAgentSteps({
43
+ yes,
44
+ agentSetup: options.agentSetup
45
+ }), options);
46
+ return options.agentSetup;
47
+ }
48
+ if (yes) {
49
+ if (options.hasProjectPlugins !== void 0) {
50
+ const agentSetup = await options.hasProjectPlugins(options.cwd) ? "plugin" : "skills-mcp";
51
+ await runInitSteps(planAgentSteps({
52
+ yes: true,
53
+ agentSetup
54
+ }), options);
55
+ return agentSetup;
56
+ }
57
+ const agents = await yesAgentsFromOptions(options);
58
+ const tooling = chooseYesAgentTooling(agents);
59
+ if (tooling.setup === "skip") throw yesMiss();
60
+ await runInitSteps(planYesAgentSteps(tooling), options);
61
+ return tooling.setup;
62
+ }
63
+ const interactive = options.pickAgentSetup !== void 0 || canPickAgentsInteractively();
27
64
  const agentSetup = await resolveInitAgentSetup({
28
- yes,
29
65
  interactive,
30
- hasProjectPlugins,
31
66
  pick: options.pickAgentSetup ?? pickAgentSetupInteractively
32
67
  });
33
68
  await runInitSteps(planAgentSteps({
34
69
  yes,
35
70
  agentSetup
36
71
  }), options);
72
+ return agentSetup;
37
73
  };
38
74
  const runScaffoldFollowUp = async (options) => {
39
75
  if (!options.skipAgentSetup) await runAgentTooling(options);
@@ -1,3 +1,4 @@
1
+ import { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
1
2
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
2
3
  import { NEON_PLUGIN_NAME } from "../plugins/run.js";
3
4
  import prompts from "prompts";
@@ -10,7 +11,7 @@ const restoreCursorOnAbort = (state) => {
10
11
  }
11
12
  };
12
13
  const pickAgentSetupInteractively = async () => {
13
- if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass -y to use defaults, or run this command in a terminal.");
14
+ if (!canPickAgentsInteractively()) throw new Error(INIT_NEEDS_YES_OR_TERMINAL);
14
15
  const { setup } = await prompts({
15
16
  onState: restoreCursorOnAbort,
16
17
  type: "select",
package/dist/mcp/plan.js CHANGED
@@ -1,22 +1,35 @@
1
1
  import { tryResolveAddMcpAgentId } from "./agents.js";
2
- import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
3
2
  import { detectMcpAgents, mcpInstallableAgents } from "./targets.js";
3
+ import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
4
+ import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
5
+ import { detectAgent } from "../init/detect_host.js";
4
6
  import { pickMcpAuth, pickMcpProjectPin, pickMcpScope } from "./wizard.js";
5
7
  //#region src/mcp/plan.ts
6
8
  async function resolveMcpPlan(options) {
7
9
  const prompt = options.interactive && !options.yes;
8
10
  const scope = options.project ? "project" : prompt ? await (options.pickScope ?? pickMcpScope)() : "global";
9
11
  const available = mcpInstallableAgents(scope);
10
- const detected = await detectMcpAgents({
12
+ const availableSet = new Set(available);
13
+ const scoped = await detectMcpAgents({
11
14
  scope,
12
15
  cwd: options.cwd
13
16
  });
17
+ const detected = options.yes && options.agents.length === 0 ? await collectYesAgents({
18
+ detected: () => scoped,
19
+ detectAgent: options.detectAgent ?? detectAgent,
20
+ acceptHost: (id) => availableSet.has(id)
21
+ }) : scoped;
14
22
  const agents = await resolveAgentSelection({
15
23
  specified: options.agents,
16
24
  choices: agentChoicesFrom(available, detected),
17
25
  detected,
18
26
  message: "Which coding agents should get the Neon MCP server? (space to toggle, enter to confirm)",
19
- nonInteractiveMessage: scope === "project" ? `No coding agents detected in this project. Pass --agent <name>. Supported agents: ${available.join(", ")}` : `No coding agents detected. Pass --agent <name>. Supported agents: ${available.join(", ")}`,
27
+ nonInteractiveMessage: noDetectedAgentsMessage({
28
+ scope,
29
+ supported: available,
30
+ fix: options.yes ? "run-without-yes" : "pass-yes",
31
+ nameAgent: true
32
+ }),
20
33
  resolveSpecified: (raw) => {
21
34
  const id = tryResolveAddMcpAgentId(raw);
22
35
  if (!id) throw new Error(`Unknown agent: "${raw}". Supported agents: ${available.join(", ")}`);
@@ -1,11 +1,13 @@
1
1
  import { getAgentDisplayName, tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
3
  import { detectPluginsAgents, getPluginsTargetName, isUserScopeOnlyPluginsTarget, mappedPluginsTargets, pluginsInstallableAgents } from "./targets.js";
4
+ import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
4
5
  import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
6
+ import { detectAgent } from "../init/detect_host.js";
5
7
  //#region src/plugins/plan.ts
6
8
  const assertPluginsCanRun = (options) => {
7
9
  if (options.yes || options.interactive || options.hasAgents) return;
8
- throw new Error("No interactive terminal. Pass -y to install into detected agents, or --agent <name>.");
10
+ throw new Error("No interactive terminal. Pass -y to install into detected agents, or --agent <name> to name them.");
9
11
  };
10
12
  async function resolvePluginsPlan(options) {
11
13
  assertPluginsCanRun({
@@ -16,16 +18,28 @@ async function resolvePluginsPlan(options) {
16
18
  const prompt = options.interactive && !options.yes;
17
19
  const scope = options.global ? "global" : "project";
18
20
  const available = pluginsInstallableAgents(scope);
19
- const detected = await detectPluginsAgents({
21
+ const availableSet = new Set(available);
22
+ const scoped = await detectPluginsAgents({
20
23
  scope,
21
- cwd: options.cwd
24
+ cwd: options.cwd,
25
+ ...options.detectInstalledAgents ? { detectInstalledAgents: options.detectInstalledAgents } : {}
22
26
  });
27
+ const detected = options.yes && options.agents.length === 0 ? await collectYesAgents({
28
+ detected: () => scoped,
29
+ detectAgent: options.detectAgent ?? detectAgent,
30
+ acceptHost: (id) => availableSet.has(id) || getPluginsTargetName(id) !== void 0
31
+ }) : scoped;
23
32
  const selected = await resolveAgentSelection({
24
33
  specified: options.agents,
25
34
  choices: agentChoicesFrom(available, detected),
26
35
  detected,
27
36
  message: "Which coding agents should get the Neon plugin? (space to toggle, enter to confirm)",
28
- nonInteractiveMessage: scope === "project" ? `No coding agents detected in this project. Pass --agent <name>. Supported agents: ${available.join(", ")}` : `No coding agents detected. Pass --agent <name>. Supported agents: ${available.join(", ")}`,
37
+ nonInteractiveMessage: noDetectedAgentsMessage({
38
+ scope,
39
+ supported: available,
40
+ fix: options.yes ? "run-without-yes" : "pass-yes",
41
+ nameAgent: true
42
+ }),
29
43
  resolveSpecified: (raw) => {
30
44
  if (raw === "*") throw new Error("neon plugins does not accept --agent *. Pass --agent <name> for each coding agent, or omit --agent to use detected agents.");
31
45
  const id = tryResolveAddMcpAgentId(raw);
@@ -35,7 +35,7 @@ const pluginsInstallableAgents = (scope) => pluginsMappedAgents().filter((id) =>
35
35
  return target !== void 0 && !isUserScopeOnlyPluginsTarget(target);
36
36
  });
37
37
  const detectPluginsAgents = async (options) => {
38
- const detected = options.scope === "project" ? detectProjectAgents(options.cwd) : await detectInstalledAgents();
38
+ const detected = options.scope === "project" ? detectProjectAgents(options.cwd) : await (options.detectInstalledAgents ?? detectInstalledAgents)();
39
39
  return uniqueAgentIds(detected).filter((id) => supportsPlugins(id));
40
40
  };
41
41
  const detectInstallablePluginsAgents = async (options) => {
@@ -3,11 +3,6 @@ import { helpCsv } from "../utils/help_text.js";
3
3
  const AGENT_SKILLS_SOURCE = "neondatabase/agent-skills";
4
4
  const PLATFORMS_SKILLS_SOURCE = "neondatabase/neon-for-agent-platforms";
5
5
  const NEON_SKILL_CATALOG = [
6
- {
7
- skill: "claimable-postgres",
8
- source: AGENT_SKILLS_SOURCE,
9
- defaultSelected: true
10
- },
11
6
  {
12
7
  skill: "neon",
13
8
  source: AGENT_SKILLS_SOURCE,
@@ -1,8 +1,10 @@
1
1
  import { tryResolveAddMcpAgentId } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
+ import { detectSkillsAgents, skillsInstallableAgents } from "./targets.js";
4
+ import { collectYesAgents, noDetectedAgentsMessage } from "../init/plan.js";
3
5
  import { agentChoicesFrom, pickAgentsInteractively, resolveAgentSelection } from "../utils/agent_picker.js";
6
+ import { detectAgent } from "../init/detect_host.js";
4
7
  import { invocationsForSelection, resolveSkillId, uniqueSkillEntries, yesInstallInvocations } from "./catalog.js";
5
- import { detectSkillsAgents, skillsInstallableAgents } from "./targets.js";
6
8
  import { pickSkillsInteractively } from "./wizard.js";
7
9
  //#region src/skills/plan.ts
8
10
  const assertSkillsCanRun = (options) => {
@@ -11,7 +13,7 @@ const assertSkillsCanRun = (options) => {
11
13
  throw new Error("No interactive terminal. Pass -y to update installed skills.");
12
14
  }
13
15
  if (options.yes || options.interactive || options.hasSkills === true) return;
14
- throw new Error("No interactive terminal. Pass -y to install the default skills, or --skill <name>.");
16
+ throw new Error("No interactive terminal. Pass -y to install the default skills into detected agents, or --skill <name>.");
15
17
  };
16
18
  async function resolveSkillsPlan(options) {
17
19
  assertSkillsCanRun({
@@ -24,16 +26,28 @@ async function resolveSkillsPlan(options) {
24
26
  const prompt = options.interactive && !options.yes;
25
27
  const scope = options.global ? "global" : "project";
26
28
  const available = skillsInstallableAgents();
27
- const detected = await detectSkillsAgents({
29
+ const availableSet = new Set(available);
30
+ const scoped = await detectSkillsAgents({
28
31
  scope,
29
- cwd: options.cwd
32
+ cwd: options.cwd,
33
+ ...options.detectInstalledAgents ? { detectInstalledAgents: options.detectInstalledAgents } : {}
30
34
  });
35
+ const detected = options.yes && options.agents.length === 0 ? await collectYesAgents({
36
+ detected: () => scoped,
37
+ detectAgent: options.detectAgent ?? detectAgent,
38
+ acceptHost: (id) => availableSet.has(id)
39
+ }) : scoped;
31
40
  const selected = await resolveAgentSelection({
32
41
  specified: options.agents,
33
42
  choices: agentChoicesFrom(available, detected),
34
43
  detected,
35
44
  message: "Which coding agents should get Neon agent skills? (space to toggle, enter to confirm)",
36
- nonInteractiveMessage: scope === "project" ? `No coding agents detected in this project. Pass --agent <name>. Supported agents: ${available.join(", ")}` : `No coding agents detected. Pass --agent <name>. Supported agents: ${available.join(", ")}`,
45
+ nonInteractiveMessage: noDetectedAgentsMessage({
46
+ scope,
47
+ supported: available,
48
+ fix: options.yes ? "run-without-yes" : "pass-yes",
49
+ nameAgent: true
50
+ }),
37
51
  resolveSpecified: (raw) => {
38
52
  if (raw === "*") throw new Error("neon skills does not accept --agent *. Pass --agent <name> for each coding agent, or omit --agent to use detected agents.");
39
53
  const id = tryResolveAddMcpAgentId(raw);
@@ -4,7 +4,7 @@ import { detectProjectAgents } from "add-mcp";
4
4
  //#region src/skills/targets.ts
5
5
  const skillsInstallableAgents = () => listMcpAgentIds().filter((id) => supportsSkills(id));
6
6
  const detectSkillsAgents = async (options) => {
7
- const detected = options.scope === "project" ? detectProjectAgents(options.cwd) : await detectInstalledAgents();
7
+ const detected = options.scope === "project" ? detectProjectAgents(options.cwd) : await (options.detectInstalledAgents ?? detectInstalledAgents)();
8
8
  return uniqueAgentIds(detected).filter((id) => supportsSkills(id));
9
9
  };
10
10
  const mappedSkillsAgentNames = (agents) => {
@@ -12,7 +12,7 @@ const restoreCursorOnAbort = (state) => {
12
12
  }
13
13
  };
14
14
  const pickSkillsInteractively = async () => {
15
- if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass -y to install the default skills, or --skill <name>.");
15
+ if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass -y to install the default skills into detected agents.");
16
16
  const { skills } = await prompts({
17
17
  onState: restoreCursorOnAbort,
18
18
  type: "multiselect",
@@ -0,0 +1,13 @@
1
+ //#region src/utils/agent_flag.ts
2
+ const coerceAgentFlag = (value) => {
3
+ if (value === void 0) return [];
4
+ const list = Array.isArray(value) ? value : [value];
5
+ if (list.length === 0) throw new Error("--agent needs a value. Pass one, or omit the flag entirely.");
6
+ return list.map((item) => {
7
+ if (typeof item !== "string" || item.trim() === "") throw new Error("--agent needs a value. Pass one, or omit the flag entirely.");
8
+ return item;
9
+ });
10
+ };
11
+ const agentArgv = (agents) => agents.flatMap((id) => ["--agent", id]);
12
+ //#endregion
13
+ export { agentArgv, coerceAgentFlag };
@@ -4,7 +4,7 @@ import prompts from "prompts";
4
4
  //#region src/utils/agent_picker.ts
5
5
  const canPickAgentsInteractively = () => !isCi() && Boolean(process.stdout.isTTY) && Boolean(process.stdin.isTTY);
6
6
  const pickAgentsInteractively = async (options) => {
7
- if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass --agent <name>, or run this command in a terminal to pick agents.");
7
+ if (!canPickAgentsInteractively()) throw new Error("No interactive terminal. Pass -y, or run this command in a terminal to pick agents.");
8
8
  if (options.choices.length === 0) throw new Error("No coding agents are available to pick.");
9
9
  const selected = new Set(options.selected ?? []);
10
10
  const { agents } = await prompts({
@@ -48,7 +48,7 @@ const resolveAgentSelection = async (options) => {
48
48
  choices: options.choices,
49
49
  selected: options.detected.filter((id) => options.choices.some((choice) => choice.id === id))
50
50
  });
51
- if (selected.length === 0) throw new Error("No agents selected. Pass --agent <name>, or pick at least one agent.");
51
+ if (selected.length === 0) throw new Error("No agents selected. Pick at least one agent, or pass -y to use detected agents.");
52
52
  return uniqueAgentIds(selected);
53
53
  }
54
54
  if (options.detected.length > 0) return uniqueAgentIds(options.detected);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.10.1",
3
+ "version": "4.11.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.0.5",
66
+ "@neon/config-runtime": "1.0.5",
67
67
  "@neon/sdk": "3.0.0",
68
- "@neon/config-runtime": "1.0.5"
68
+ "@neon/config": "1.0.5"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",