neon 6.0.0 → 6.1.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/README.md CHANGED
@@ -936,12 +936,15 @@ The plugins CLI installs every plugin it finds in the Neon plugin package. Today
936
936
 
937
937
  `neon snapshots` (alias `neon snapshot`) manages **snapshots** — point-in-time backups of a branch that you can list, rename, expire, restore into a branch, or schedule automatically. Snapshots are a Beta Neon feature and were previously only available in the Console and REST API; this command group brings them to the CLI.
938
938
 
939
- Every sub-command resolves the project through the standard chain (`--project-id`, then the `.neon` context file, then a single-project auto-detect). Branch-scoped sub-commands (`create`, `schedule`) default to the branch pinned in `.neon`, falling back to the project's default branch, and accept `--branch <id|name>`. The `get`, `update`, `delete`, and `restore` sub-commands take a snapshot **id or name** as their positional argument (an id wins; an ambiguous name errors and asks you to use the id).
939
+ Every sub-command resolves the project through the standard chain (`--project-id`, then the `.neon` context file, then a single-project auto-detect). Branch-scoped sub-commands (`create`, `schedule`) default to the branch pinned in `.neon`, falling back to the project's default branch, and accept `--branch <id|name>`. The `get`, `update`, `delete`, and `restore` sub-commands take a snapshot **id, unique name, or slug** as their positional argument. An id wins. A unique name still wins over another snapshot's matching slug, so a name you already use keeps targeting that snapshot. Slug is used when no name matches. An ambiguous name errors and asks you to use the id.
940
940
 
941
941
  ```bash
942
942
  # Snapshot the head of the current/default branch
943
943
  neon snapshots create --name pre-migration
944
944
 
945
+ # Give the snapshot a slug (unique in the project; omit to let the API generate one)
946
+ neon snapshots create --branch main --name "Before migration" --slug before-migration
947
+
945
948
  # Snapshot a specific branch at a point in time (RFC 3339 timestamp OR LSN — mutually exclusive)
946
949
  neon snapshots create --branch main --timestamp 2025-01-01T00:00:00Z
947
950
  neon snapshots create --branch main --lsn 0/1F3C8A0 --expires-at 2025-12-31T23:59:59Z
@@ -949,6 +952,7 @@ neon snapshots create --branch main --lsn 0/1F3C8A0 --expires-at 2025-12-31T23:5
949
952
  # List / inspect
950
953
  neon snapshots list
951
954
  neon snapshots get pre-migration
955
+ neon snapshots get before-migration
952
956
 
953
957
  # Rename or change expiration (omit both to error; --expires-at and --clear-expiration conflict)
954
958
  neon snapshots update snap-1234 --name nightly
@@ -2,12 +2,10 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
4
  import { recordCommandSuccessExtras, recordScaffoldedTemplate } from "../analytics.js";
5
- import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
6
5
  import { getCliName } from "../utils/cli_name.js";
7
6
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
8
7
  import { writer } from "../writer.js";
9
8
  import { FALLBACK_TEMPLATES, ensureTargetUsable, fetchTemplates, findTemplate, scaffoldTemplate, templateIds } from "../init/bootstrap.js";
10
- import { spawnCliChild } from "../init/child.js";
11
9
  import { agentSetupDoneLabel, formatInitDone, printInitBanner, printInitDone, shouldPrintInitBanner } from "../init/chrome.js";
12
10
  import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
13
11
  import { runAuthenticatedLink } from "../init/link.js";
@@ -271,7 +269,9 @@ const runDefaultSteps = async (props, targetDir, pm, neonConfig, template, named
271
269
  };
272
270
  const executePostScaffold = async (props, targetDir, choices) => {
273
271
  const kids = {
274
- ...bootstrapChildren(props, targetDir),
272
+ output: "table",
273
+ auth: bootstrapAuth(props, targetDir),
274
+ operations: props.operations,
275
275
  ...props.narrate ? { narrate: props.narrate } : {}
276
276
  };
277
277
  let installed = false;
@@ -370,20 +370,22 @@ const hasNeonConfig = (dir) => NEON_CONFIG_FILENAMES.some((name) => existsSync(j
370
370
  const logSkippedLink = (pm) => {
371
371
  log.info(`Skipping the Neon link step: \`${getCliName()} link\` reads this project's neon.ts to pull env vars, which needs its dependencies. Run \`${formatInstallCommand(pm)}\`, then \`${getCliName()} link\`.`);
372
372
  };
373
- const bootstrapChildren = (props, targetDir) => {
374
- const explicitKey = props.profile ? "" : credentialInputs().apiKeyFlag;
375
- return {
376
- run: props.run ?? spawnCliChild,
377
- forward: {
378
- ...props.configDir ? { configDir: props.configDir } : {},
379
- ...props.profile ? { profile: props.profile } : {},
380
- apiHost: props.apiHost,
381
- contextFile: projectContextFile(targetDir, props.contextFile),
382
- ...props.analytics === false ? { analytics: false } : {}
383
- },
384
- ...explicitKey ? { authEnv: { NEON_API_KEY: explicitKey } } : {}
385
- };
386
- };
373
+ /**
374
+ * plugins/skills need no Neon auth at all; mcp resolves it itself, in-process, exactly as
375
+ * the standalone `neon mcp` does (see `init/auth.ts`) — this is that shared context.
376
+ */
377
+ const bootstrapAuth = (props, targetDir) => ({
378
+ apiClient: props.apiClient,
379
+ apiKey: props.apiKey,
380
+ apiHost: props.apiHost,
381
+ contextFile: projectContextFile(targetDir, props.contextFile),
382
+ ...props.configDir ? { configDir: props.configDir } : {},
383
+ ...props.profile ? { profile: props.profile } : {},
384
+ ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
385
+ ...props.clientId ? { clientId: props.clientId } : {},
386
+ ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
387
+ ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {}
388
+ });
387
389
  /**
388
390
  * Initialize a git repository in the scaffolded directory. Just `git init` — we
389
391
  * deliberately don't auto-commit, both to avoid failing on a machine with no
@@ -13,8 +13,8 @@ import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs"
13
13
  import { basename, dirname, join, relative, resolve } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
15
  import chalk from "chalk";
16
- import { spawn, spawnSync } from "node:child_process";
17
16
  import { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry } from "@neon/config-runtime";
17
+ import { spawn, spawnSync } from "node:child_process";
18
18
  import { once } from "node:events";
19
19
  import { createServer } from "node:net";
20
20
  //#region src/commands/dev.ts
@@ -6,6 +6,9 @@ import { t as env_exports } from "./env.js";
6
6
  import { t as config_exports } from "./config.js";
7
7
  import { t as projects_exports } from "./projects.js";
8
8
  import { t as link_exports } from "./link.js";
9
+ import { t as mcp_exports } from "./mcp.js";
10
+ import { t as plugins_exports } from "./plugins.js";
11
+ import { t as skills_exports } from "./skills.js";
9
12
  import { t as bootstrap_exports } from "./bootstrap.js";
10
13
  import { t as branches_exports } from "./branches.js";
11
14
  import { t as bucket_exports } from "./bucket.js";
@@ -23,17 +26,14 @@ import { t as init_exports } from "./init.js";
23
26
  import { t as inspect_exports } from "./inspect.js";
24
27
  import { t as ip_allow_exports } from "./ip_allow.js";
25
28
  import { t as logs_exports } from "./logs.js";
26
- import { t as mcp_exports } from "./mcp.js";
27
29
  import { t as neon_auth_exports } from "./neon_auth.js";
28
30
  import { t as open_exports } from "./open.js";
29
31
  import { t as operations_exports } from "./operations.js";
30
32
  import { t as orgs_exports } from "./orgs.js";
31
- import { t as plugins_exports } from "./plugins.js";
32
33
  import { t as profile_exports } from "./profile.js";
33
34
  import { t as psql_exports } from "./psql.js";
34
35
  import { t as roles_exports } from "./roles.js";
35
36
  import { t as set_context_exports } from "./set_context.js";
36
- import { t as skills_exports } from "./skills.js";
37
37
  import { t as snapshots_exports } from "./snapshots.js";
38
38
  import { t as status_exports } from "./status.js";
39
39
  import { t as triggers_exports } from "./triggers.js";
@@ -3,7 +3,6 @@ import { getCliName } from "../utils/cli_name.js";
3
3
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
4
4
  import { servicesOption } from "../neon_services.js";
5
5
  import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES } from "../config_template.js";
6
- import { initChildEnv } from "../init/child.js";
7
6
  import { coerceAgentFlag } from "../utils/agent_flag.js";
8
7
  import { initPluginAgents, initSkillsMcpAgents } from "../init/plan.js";
9
8
  import { listSkillIds } from "../skills/catalog.js";
@@ -13,8 +12,7 @@ var init_exports = /* @__PURE__ */ __exportAll({
13
12
  builder: () => builder,
14
13
  command: () => command,
15
14
  describe: () => describe,
16
- handler: () => handler,
17
- initChildEnv: () => initChildEnv
15
+ handler: () => handler
18
16
  });
19
17
  const command = "init";
20
18
  const describe = "Set up coding agents and this directory for Neon. -y is Recommended. -y with Custom flags applies those answers without prompts.";
@@ -108,4 +106,4 @@ const handler = async (props) => {
108
106
  await runInit(props);
109
107
  };
110
108
  //#endregion
111
- export { builder, command, describe, handler, initChildEnv, init_exports as t };
109
+ export { builder, command, describe, handler, init_exports as t };
@@ -8,15 +8,18 @@ import { writer } from "../writer.js";
8
8
  import { noPassthrough, single } from "../utils/flags.js";
9
9
  import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
10
10
  import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
11
- import { canPickAgentsInteractively } from "../utils/agent_picker.js";
12
11
  import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
12
+ import { canPickAgentsInteractively } from "../utils/agent_picker.js";
13
13
  import { resolveMcpPlan } from "../mcp/plan.js";
14
14
  //#region src/commands/mcp.ts
15
15
  var mcp_exports = /* @__PURE__ */ __exportAll({
16
16
  builder: () => builder,
17
17
  command: () => "mcp",
18
18
  describe: () => describe,
19
- handler: () => handler
19
+ handler: () => handler,
20
+ mcpInstallError: () => mcpInstallError,
21
+ reportMcpInstall: () => reportMcpInstall,
22
+ setupNeonMcp: () => setupNeonMcp
20
23
  });
21
24
  const command = "mcp";
22
25
  const describe = "Install the Neon MCP server into coding agents";
@@ -87,20 +90,29 @@ const builder = (argv) => argv.usage("$0 mcp [options]").options({
87
90
  }
88
91
  }
89
92
  }).example("$0 mcp", "Interactive: config location, agents, then auth").example("$0 mcp -y", "Global config, installed apps else the host CLI agent, reuse or mint an API key").example("$0 mcp --oauth", "Install with OAuth; the agent signs in on first use").example("$0 mcp --agent cursor --agent claude-code", "Install into specific agents").example("$0 mcp --project", "Write project-level config").example("$0 mcp --read-only", "Hide write tools via ?readonly=true").example("$0 mcp --project-id <id>", "Pin tools to one project via ?projectId=").example("$0 mcp --category querying --category schema", "Limit tools to those categories").epilogue(helpEpilogue(`Installs ${NEON_MCP_URL}`, helpCsv("Supported agents at global scope", mcpGlobalAgents), helpCsv("--project does not support", mcpProjectDroppedAgents), helpCsv("Supported categories", NEON_MCP_CATEGORIES), "neon mcp -y:", " global config", " globally installed apps, else the host CLI agent", " reuse an existing Neon MCP API key, else mint an account-wide key", " write tools on, all categories", " no project pin (including from .neon)")).strict().check(noPassthrough("mcp"));
90
- const handler = async (props) => {
91
- const cwd = process.cwd();
92
- const interactive = canPickAgentsInteractively() && props.yes !== true;
93
- const linkedProjectId = readContextFile(props.contextFile).projectId;
93
+ /**
94
+ * Resolves auth, mints or reuses an API key, and writes the Neon MCP server into every
95
+ * resolved agent. No CLI-only concerns here (no `writer` table, no
96
+ * `recordCommandSuccessExtras`): a caller that only wants the outcome — `neon init` /
97
+ * `neon bootstrap` — gets it without rendering a table.
98
+ *
99
+ * Throws directly when zero agents succeed, since that path never reaches a results table
100
+ * worth showing (same as the CLI handler always did).
101
+ */
102
+ const setupNeonMcp = async (options) => {
103
+ const cwd = options.cwd ?? process.cwd();
104
+ const interactive = canPickAgentsInteractively() && options.yes !== true;
105
+ const linkedProjectId = readContextFile(options.contextFile).projectId;
94
106
  const plan = await resolveMcpPlan({
95
- project: props.project === true,
96
- oauth: props.oauth === true,
97
- agents: props.agent ?? [],
98
- yes: props.yes === true,
107
+ project: options.project === true,
108
+ oauth: options.oauth === true,
109
+ agents: options.agent ?? [],
110
+ yes: options.yes === true,
99
111
  cwd,
100
112
  interactive,
101
- readOnly: props.readOnly === true,
102
- projectId: props.projectId,
103
- categories: props.category ?? [],
113
+ readOnly: options.readOnly === true,
114
+ projectId: options.projectId,
115
+ categories: options.category ?? [],
104
116
  linkedProjectId
105
117
  });
106
118
  const { install, skipped } = resolveInstallTargets({
@@ -125,8 +137,8 @@ const handler = async (props) => {
125
137
  cwd
126
138
  }) : void 0;
127
139
  if (plan.auth === "api-key" && !existing) {
128
- if (!props.apiClient || !props.apiKey) throw new Error(`Authentication required. Run \`${getCliName()} auth\`, pass --api-key or use --oauth to install without a Neon credential.`);
129
- if (!canPickAgentsInteractively() && props.yes !== true && (props.agent ?? []).length === 0) throw new Error("No interactive terminal. Pass -y to mint into every detected agent, --agent <name> to name them or --oauth to install without minting.");
140
+ if (!options.apiClient || !options.apiKey) throw new Error(`Authentication required. Run \`${getCliName()} auth\`, pass --api-key or use --oauth to install without a Neon credential.`);
141
+ if (!canPickAgentsInteractively() && options.yes !== true && (options.agent ?? []).length === 0) throw new Error("No interactive terminal. Pass -y to mint into every detected agent, --agent <name> to name them or --oauth to install without minting.");
130
142
  }
131
143
  let auth;
132
144
  let minted;
@@ -140,7 +152,7 @@ const handler = async (props) => {
140
152
  if (plan.urlProjectId) log.warning("That key keeps its existing scope. Remove the Neon MCP entry to mint a project-scoped key, or pass --oauth to pin tools without a key.");
141
153
  } else {
142
154
  minted = await mintMcpApiKey({
143
- apiClient: props.apiClient,
155
+ apiClient: options.apiClient,
144
156
  projectId: plan.urlProjectId
145
157
  });
146
158
  auth = {
@@ -183,15 +195,28 @@ const handler = async (props) => {
183
195
  error: row.error
184
196
  });
185
197
  if (successes === 0) {
186
- if (minted && props.apiClient) {
187
- const withdrawn = await withdrawMintedKey(props.apiClient, minted);
198
+ if (minted && options.apiClient) {
199
+ const withdrawn = await withdrawMintedKey(options.apiClient, minted);
188
200
  throw new Error(withdrawn ? "Failed to write Neon MCP config to any agent. The minted API key has been revoked." : `Failed to write Neon MCP config to any agent. The minted API key could NOT be revoked. Remove it with \`${mintedKeyRevokeCommand(minted)}\`.`);
189
201
  }
190
202
  throw new Error("Failed to write Neon MCP config to any agent.");
191
203
  }
192
- log.info("URL: %s", url);
204
+ return {
205
+ scope: plan.scope,
206
+ rows,
207
+ failedAgents,
208
+ url,
209
+ auth: auth.kind,
210
+ ...minted ? { minted } : {}
211
+ };
212
+ };
213
+ /** The exact error `neon mcp` throws when some (not all) agents failed, or `undefined` on full success. */
214
+ const mcpInstallError = (outcome) => outcome.failedAgents.length > 0 ? /* @__PURE__ */ new Error(`Failed to write Neon MCP config for: ${outcome.failedAgents.join(", ")}.`) : void 0;
215
+ /** Writes the results table plus the URL/minted-key/OAuth notices. Shared by `neon mcp` and any in-process caller that wants the same human output. */
216
+ const reportMcpInstall = (props, outcome) => {
217
+ log.info("URL: %s", outcome.url);
193
218
  const out = writer(props);
194
- out.write(rows, {
219
+ out.write(outcome.rows, {
195
220
  fields: [
196
221
  "agent",
197
222
  "status",
@@ -200,10 +225,27 @@ const handler = async (props) => {
200
225
  title: "MCP"
201
226
  });
202
227
  out.end();
203
- if (minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", minted.name, minted.id, minted.projectId ? "project" : "account", mintedKeyRevokeCommand(minted));
204
- if (plan.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
205
- if (failedAgents.length > 0) throw new Error(`Failed to write Neon MCP config for: ${failedAgents.join(", ")}.`);
206
- recordCommandSuccessExtras({ scope: plan.scope });
228
+ if (outcome.minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", outcome.minted.name, outcome.minted.id, outcome.minted.projectId ? "project" : "account", mintedKeyRevokeCommand(outcome.minted));
229
+ if (outcome.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
230
+ };
231
+ const handler = async (props) => {
232
+ const outcome = await setupNeonMcp({
233
+ apiClient: props.apiClient,
234
+ apiKey: props.apiKey,
235
+ contextFile: props.contextFile,
236
+ cwd: props.cwd,
237
+ yes: props.yes,
238
+ oauth: props.oauth,
239
+ project: props.project,
240
+ agent: props.agent,
241
+ readOnly: props.readOnly,
242
+ projectId: props.projectId,
243
+ category: props.category
244
+ });
245
+ reportMcpInstall(props, outcome);
246
+ const error = mcpInstallError(outcome);
247
+ if (error) throw error;
248
+ recordCommandSuccessExtras({ scope: outcome.scope });
207
249
  };
208
250
  //#endregion
209
- export { builder, command, describe, handler, mcp_exports as t };
251
+ export { builder, command, describe, handler, mcpInstallError, reportMcpInstall, setupNeonMcp, mcp_exports as t };
@@ -17,7 +17,10 @@ var plugins_exports = /* @__PURE__ */ __exportAll({
17
17
  builder: () => builder,
18
18
  command: () => command,
19
19
  describe: () => describe,
20
- handler: () => handler
20
+ handler: () => handler,
21
+ installPlugins: () => installPlugins,
22
+ pluginsInstallError: () => pluginsInstallError,
23
+ reportPluginsInstall: () => reportPluginsInstall
21
24
  });
22
25
  const coerceAgents = (value) => {
23
26
  if (value === void 0) return [];
@@ -54,13 +57,18 @@ const builder = (argv) => argv.usage("$0 plugins [options]").options({
54
57
  coerce: coerceAgents
55
58
  }
56
59
  }).example("$0 plugins", "Interactive: pick agents, then install").example("$0 plugins -y", "Detected agents (project folders, else the host CLI agent), skip prompts").example("$0 plugins --agent cursor --agent claude-code", "Install into specific agents").example("$0 plugins --global", "Install user-level").epilogue(helpEpilogue(helpCsv("Supported agents at project scope", pluginProjectAgents), helpCsv("Also with --global", pluginGlobalOnlyAgents), `Currently one plugin: ${NEON_PLUGIN_NAME} from ${PLUGIN_SOURCE}.`, `It includes the Neon MCP server (${NEON_MCP_URL})`, helpCsv("and these skills", PLUGIN_SKILLS))).strict().check(noPassthrough("plugins"));
57
- const handler = async (props) => {
58
- const cwd = process.cwd();
59
- const yes = props.yes === true;
60
+ /**
61
+ * Plans and runs the plugin install for every resolved target. No CLI-only concerns here
62
+ * (no `writer` table, no `recordCommandSuccessExtras`): a caller that only wants the
63
+ * outcome — `neon init` / `neon bootstrap` — gets it without rendering a result table.
64
+ */
65
+ const installPlugins = async (options) => {
66
+ const cwd = options.cwd ?? process.cwd();
67
+ const yes = options.yes === true;
60
68
  const interactive = canPickAgentsInteractively() && !yes;
61
69
  const plan = await resolvePluginsPlan({
62
- global: props.global === true,
63
- agents: props.agent ?? [],
70
+ global: options.global === true,
71
+ agents: options.agents ?? [],
64
72
  yes,
65
73
  cwd,
66
74
  interactive
@@ -70,12 +78,14 @@ const handler = async (props) => {
70
78
  const rows = [];
71
79
  const failed = [];
72
80
  const scope = scopeLabel(plan.scope);
73
- for (const mapped of plan.targets) {
81
+ for (const [index, mapped] of plan.targets.entries()) {
74
82
  const args = pluginsAddArgs({
75
83
  target: mapped.target,
76
84
  global: plan.scope === "global"
77
85
  });
78
86
  const agent = mapped.agents.join(", ");
87
+ const displayNames = mapped.agents.map((id) => getAgentDisplayName(id)).join(", ");
88
+ log.info("Installing the Neon plugin for %s (%d/%d)...", displayNames, index + 1, plan.targets.length);
79
89
  try {
80
90
  await runPluginsCli({
81
91
  args,
@@ -102,8 +112,31 @@ const handler = async (props) => {
102
112
  });
103
113
  }
104
114
  }
115
+ return {
116
+ scope: plan.scope,
117
+ rows,
118
+ failed
119
+ };
120
+ };
121
+ /** The exact error `neon plugins` throws for a failed outcome, or `undefined` on success. */
122
+ const pluginsInstallError = (outcome) => {
123
+ const { failed, rows, scope } = outcome;
124
+ if (failed.length === 0) return;
125
+ const first = failed[0];
126
+ if (first === void 0) return /* @__PURE__ */ new Error("Failed to install the Neon plugin.");
127
+ const retry = neonPluginsRetryCommand({
128
+ agents: failed.flatMap((row) => row.agents),
129
+ global: scope === "global"
130
+ });
131
+ if (first.message.includes("needs npx (Node.js)")) return new Error(first.message);
132
+ const detail = failed.map((row) => row.message).join("\n");
133
+ if (failed.length === rows.length) return /* @__PURE__ */ new Error(`${detail}\nRetry with: ${retry}`);
134
+ return /* @__PURE__ */ new Error(`Failed to install the Neon plugin for: ${failed.flatMap((row) => row.agents).join(", ")}.\n${detail}\nRetry with: ${retry}`);
135
+ };
136
+ /** Writes the results table and, on success, the "Installed the Neon plugin" line. Shared by `neon plugins` and any in-process caller that wants the same human output. */
137
+ const reportPluginsInstall = (props, outcome) => {
105
138
  const out = writer(props);
106
- out.write(rows, {
139
+ out.write(outcome.rows, {
107
140
  fields: [
108
141
  "scope",
109
142
  "plugin",
@@ -114,21 +147,19 @@ const handler = async (props) => {
114
147
  title: "Plugins"
115
148
  });
116
149
  out.end();
117
- if (failed.length === 0) {
118
- recordCommandSuccessExtras({ scope: plan.scope });
119
- log.info(plan.scope === "project" ? "Installed the Neon plugin (project)." : "Installed the Neon plugin (user).");
120
- return;
121
- }
122
- const first = failed[0];
123
- if (first === void 0) throw new Error("Failed to install the Neon plugin.");
124
- const retry = neonPluginsRetryCommand({
125
- agents: failed.flatMap((row) => row.agents),
126
- global: plan.scope === "global"
150
+ if (outcome.failed.length === 0) log.info(outcome.scope === "project" ? "Installed the Neon plugin (project)." : "Installed the Neon plugin (user).");
151
+ };
152
+ const handler = async (props) => {
153
+ const outcome = await installPlugins({
154
+ cwd: props.cwd,
155
+ yes: props.yes,
156
+ global: props.global,
157
+ agents: props.agent
127
158
  });
128
- if (first.message.includes("needs npx (Node.js)")) throw new Error(first.message);
129
- const detail = failed.map((row) => row.message).join("\n");
130
- if (failed.length === rows.length) throw new Error(`${detail}\nRetry with: ${retry}`);
131
- throw new Error(`Failed to install the Neon plugin for: ${failed.flatMap((row) => row.agents).join(", ")}.\n${detail}\nRetry with: ${retry}`);
159
+ reportPluginsInstall(props, outcome);
160
+ const error = pluginsInstallError(outcome);
161
+ if (error) throw error;
162
+ recordCommandSuccessExtras({ scope: outcome.scope });
132
163
  };
133
164
  //#endregion
134
- export { aliases, builder, command, describe, handler, plugins_exports as t };
165
+ export { aliases, builder, command, describe, handler, installPlugins, pluginsInstallError, reportPluginsInstall, plugins_exports as t };
@@ -18,7 +18,10 @@ var skills_exports = /* @__PURE__ */ __exportAll({
18
18
  builder: () => builder,
19
19
  command: () => command,
20
20
  describe: () => describe,
21
- handler: () => handler
21
+ handler: () => handler,
22
+ installSkills: () => installSkills,
23
+ reportSkillsInstall: () => reportSkillsInstall,
24
+ skillsInstallError: () => skillsInstallError
22
25
  });
23
26
  const scopeLabel = (scope) => scope === "project" ? "this directory" : "user-level";
24
27
  const coerceAgents = (value) => {
@@ -83,15 +86,20 @@ const builder = (argv) => argv.usage("$0 skills [command] [options]").command("u
83
86
  coerce: coerceSkills
84
87
  }
85
88
  }).example("$0 skills", "Interactive: this directory, then agents, then skills").example("$0 skills -y", "This directory, detected agents (project folders, else the host CLI agent), the default skills").example("$0 skills -y -s neon -s neon-ai-gateway", "Named skills into detected agents").example("$0 skills -s neon -s neon-ai-gateway --agent cursor", "Named skills into a named agent").example("$0 skills --global", "Install user-level skills").epilogue(helpEpilogue(helpCsv("Supported agents", skillsInstallableAgents()), "Supported skills, by source repo:", skillsHelpValues(), skillsYesHelp())).strict().check(noPassthrough("skills"));
86
- const handler = async (props) => {
89
+ /**
90
+ * Plans and runs the skills install for every resolved source/invocation. No CLI-only
91
+ * concerns here (no `writer` table, no `recordCommandSuccessExtras`): a caller that only
92
+ * wants the outcome — `neon init` / `neon bootstrap` — gets it without rendering a table.
93
+ */
94
+ const installSkills = async (options) => {
87
95
  assertSkillsNode();
88
- const cwd = process.cwd();
89
- const yes = props.yes === true;
96
+ const cwd = options.cwd ?? process.cwd();
97
+ const yes = options.yes === true;
90
98
  const interactive = canPickAgentsInteractively() && !yes;
91
99
  const plan = await resolveSkillsPlan({
92
- global: props.global === true,
93
- agents: props.agent ?? [],
94
- skills: props.skill ?? [],
100
+ global: options.global === true,
101
+ agents: options.agents ?? [],
102
+ skills: options.skills ?? [],
95
103
  yes,
96
104
  cwd,
97
105
  interactive
@@ -138,8 +146,32 @@ const handler = async (props) => {
138
146
  });
139
147
  }
140
148
  }
149
+ return {
150
+ scope: plan.scope,
151
+ agents: plan.agents,
152
+ rows,
153
+ failed
154
+ };
155
+ };
156
+ /** The exact error `neon skills` throws for a failed outcome, or `undefined` on success. */
157
+ const skillsInstallError = (outcome) => {
158
+ const { failed, rows, scope, agents } = outcome;
159
+ if (failed.length === 0) return;
160
+ const first = failed[0];
161
+ if (first === void 0) return /* @__PURE__ */ new Error("Failed to install Neon agent skills.");
162
+ const retry = neonSkillsRetryCommand({
163
+ skills: failed.flatMap((row) => row.skills),
164
+ agents,
165
+ global: scope === "global"
166
+ });
167
+ if (first.message.includes("needs npx (Node.js)") || first.message.includes("needs Node.js")) return new Error(first.message);
168
+ if (failed.length === rows.length) return /* @__PURE__ */ new Error(`${first.message}\nRetry with: ${retry}`);
169
+ return /* @__PURE__ */ new Error(`Failed to install Neon agent skills for: ${failed.map((row) => row.label).join(", ")}.\n${first.message}\nRetry with: ${retry}`);
170
+ };
171
+ /** Writes the results table and, on success, the "Wrote ... skills" line. Shared by `neon skills` and any in-process caller that wants the same human output. */
172
+ const reportSkillsInstall = (props, outcome) => {
141
173
  const out = writer(props);
142
- out.write(rows, {
174
+ out.write(outcome.rows, {
143
175
  fields: [
144
176
  "scope",
145
177
  "skills",
@@ -150,21 +182,20 @@ const handler = async (props) => {
150
182
  title: "Skills"
151
183
  });
152
184
  out.end();
153
- if (failed.length === 0) {
154
- recordCommandSuccessExtras({ scope: plan.scope });
155
- log.info(plan.scope === "project" ? "Wrote skills in this directory." : "Wrote user-level skills.");
156
- return;
157
- }
158
- const first = failed[0];
159
- if (first === void 0) throw new Error("Failed to install Neon agent skills.");
160
- const retry = neonSkillsRetryCommand({
161
- skills: failed.flatMap((row) => row.skills),
162
- agents: plan.agents,
163
- global: plan.scope === "global"
185
+ if (outcome.failed.length === 0) log.info(outcome.scope === "project" ? "Wrote skills in this directory." : "Wrote user-level skills.");
186
+ };
187
+ const handler = async (props) => {
188
+ const outcome = await installSkills({
189
+ cwd: props.cwd,
190
+ yes: props.yes,
191
+ global: props.global,
192
+ agents: props.agent,
193
+ skills: props.skill
164
194
  });
165
- if (first.message.includes("needs npx (Node.js)") || first.message.includes("needs Node.js")) throw new Error(first.message);
166
- if (failed.length === rows.length) throw new Error(`${first.message}\nRetry with: ${retry}`);
167
- throw new Error(`Failed to install Neon agent skills for: ${failed.map((row) => row.label).join(", ")}.\n${first.message}\nRetry with: ${retry}`);
195
+ reportSkillsInstall(props, outcome);
196
+ const error = skillsInstallError(outcome);
197
+ if (error) throw error;
198
+ recordCommandSuccessExtras({ scope: outcome.scope });
168
199
  };
169
200
  const updateHandler = async (props) => {
170
201
  assertSkillsNode();
@@ -219,4 +250,4 @@ const updateHandler = async (props) => {
219
250
  if (failure !== void 0) throw new Error(failure.includes("needs npx (Node.js)") || failure.includes("needs Node.js") ? failure : `${failure}\nRetry with: ${neonSkillsUpdateRetryCommand(scope === "global")}`);
220
251
  };
221
252
  //#endregion
222
- export { aliases, builder, command, describe, handler, skills_exports as t };
253
+ export { aliases, builder, command, describe, handler, installSkills, reportSkillsInstall, skillsInstallError, skills_exports as t };
@@ -17,6 +17,7 @@ var snapshots_exports = /* @__PURE__ */ __exportAll({
17
17
  const SNAPSHOT_FIELDS = [
18
18
  "id",
19
19
  "name",
20
+ "slug",
20
21
  "source_branch_id",
21
22
  "expires_at",
22
23
  "created_at"
@@ -38,6 +39,12 @@ const SNAPSHOT_FREQUENCIES = [
38
39
  "weekly",
39
40
  "monthly"
40
41
  ];
42
+ /** Matches createSnapshot.query.slug in the Management API spec. */
43
+ const SNAPSHOT_SLUG = /^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$/;
44
+ const SNAPSHOT_REF = {
45
+ describe: "Snapshot id, unique name, or slug. Lookup order: id, unique name, slug.",
46
+ type: "string"
47
+ };
41
48
  /** Narrow an arbitrary string to a supported {@link SnapshotFrequency}. */
42
49
  const isSnapshotFrequency = (value) => SNAPSHOT_FREQUENCIES.some((frequency) => frequency === value);
43
50
  const command = "snapshots";
@@ -46,7 +53,7 @@ const aliases = ["snapshot"];
46
53
  const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").options({ "project-id": {
47
54
  describe: "Project ID",
48
55
  type: "string"
49
- } }).middleware(fillSingleProject).command("list", "List snapshots in the project", (yargs) => yargs, (args) => list(args)).command("get <id>", "Get a snapshot by id or name", (yargs) => yargs, (args) => get(args)).command("create", "Create a snapshot from a branch", (yargs) => yargs.options({
56
+ } }).middleware(fillSingleProject).command("list", "List snapshots in the project", (yargs) => yargs, (args) => list(args)).command("get <id>", "Get a snapshot by id, unique name, or slug", (yargs) => yargs.positional("id", SNAPSHOT_REF), (args) => get(args)).command("create", "Create a snapshot from a branch", (yargs) => yargs.options({
50
57
  branch: {
51
58
  alias: "b",
52
59
  describe: "Branch id or name to snapshot. Defaults to the branch in your context, or the project's default branch.",
@@ -56,6 +63,10 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
56
63
  describe: "A name for the snapshot",
57
64
  type: "string"
58
65
  },
66
+ slug: {
67
+ describe: "User-defined resource ID, unique in the project (1-63 characters: start with a lowercase letter, then lowercase letters, digits, or hyphens, ending with a letter or digit). Omit to let the API generate one. It cannot be changed later.",
68
+ type: "string"
69
+ },
59
70
  timestamp: {
60
71
  describe: "Take the snapshot at this point in time (RFC 3339, e.g. 2025-01-01T00:00:00Z). Must fall within the branch's restore window. Mutually exclusive with --lsn.",
61
72
  type: "string"
@@ -71,9 +82,10 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
71
82
  }).conflicts("timestamp", "lsn").example([
72
83
  ["$0 snapshots create", "Snapshot the head of the context/default branch"],
73
84
  ["$0 snapshots create --branch main --name pre-migration", "Snapshot the head of main with a name"],
85
+ ["$0 snapshots create --branch main --name \"Before migration\" --slug before-migration", "Snapshot main with a display name and a slug"],
74
86
  ["$0 snapshots create --branch main --timestamp 2025-01-01T00:00:00Z", "Snapshot main at a point in time"],
75
87
  ["$0 snapshots create --branch main --lsn 0/1F3C8A0 --expires-at 2025-12-31T23:59:59Z", "Snapshot main at an LSN, auto-deleting at the given time"]
76
- ]), (args) => create(args)).command("update <id>", "Update a snapshot's name or expiration", (yargs) => yargs.options({
88
+ ]), (args) => create(args)).command("update <id>", "Update a snapshot's name or expiration by id, unique name, or slug", (yargs) => yargs.positional("id", SNAPSHOT_REF).options({
77
89
  name: {
78
90
  describe: "Rename the snapshot",
79
91
  type: "string"
@@ -86,7 +98,7 @@ const builder = (argv) => argv.usage("$0 snapshots <sub-command> [options]").opt
86
98
  describe: "Clear the expiration so the snapshot is kept indefinitely.",
87
99
  type: "boolean"
88
100
  }
89
- }).conflicts("expires-at", "clear-expiration"), (args) => update(args)).command("delete <id>", "Delete a snapshot by id or name", (yargs) => yargs, (args) => deleteSnapshot(args)).command("restore <id>", "Restore a snapshot into a branch", (yargs) => yargs.options({
101
+ }).conflicts("expires-at", "clear-expiration"), (args) => update(args)).command("delete <id>", "Delete a snapshot by id, unique name, or slug", (yargs) => yargs.positional("id", SNAPSHOT_REF), (args) => deleteSnapshot(args)).command("restore <id>", "Restore a snapshot (id, unique name, or slug) into a branch", (yargs) => yargs.positional("id", SNAPSHOT_REF).options({
90
102
  name: {
91
103
  describe: "Name for the newly restored branch. Auto-generated when omitted.",
92
104
  type: "string"
@@ -155,9 +167,8 @@ const toIso = (value, flag) => {
155
167
  return new Date(ms).toISOString();
156
168
  };
157
169
  /**
158
- * Resolve a snapshot from an id **or** a name. Snapshot names are not guaranteed
159
- * unique, so an id match wins; a name that resolves to more than one snapshot is a
160
- * hard error asking the user to disambiguate by id.
170
+ * Unique names precede slugs so existing name-based commands keep targeting
171
+ * the named snapshot when another snapshot's slug matches that name.
161
172
  */
162
173
  const resolveSnapshot = async (props) => {
163
174
  const { data: { snapshots } } = await props.apiClient.listSnapshots(props.projectId);
@@ -166,8 +177,11 @@ const resolveSnapshot = async (props) => {
166
177
  const byName = snapshots.filter((s) => s.name === props.id);
167
178
  if (byName.length === 1) return byName[0];
168
179
  if (byName.length > 1) throw new Error(`Multiple snapshots are named "${props.id}". Re-run with the snapshot id:\n${byName.map((s) => ` ${s.id}`).join("\n")}`);
169
- throw new Error(`Snapshot "${props.id}" not found.\nAvailable snapshots: ${snapshots.map((s) => `${s.name} (${s.id})`).join(", ") || "none"}`);
180
+ const bySlug = snapshots.find((s) => s.slug === props.id);
181
+ if (bySlug) return bySlug;
182
+ throw new Error(`Snapshot "${props.id}" not found.\nAvailable snapshots: ${snapshots.map((s) => formatSnapshotRef(s)).join(", ") || "none"}`);
170
183
  };
184
+ const formatSnapshotRef = (snapshot) => snapshot.slug ? `${snapshot.name} (${snapshot.id}, slug: ${snapshot.slug})` : `${snapshot.name} (${snapshot.id})`;
171
185
  const list = async (props) => {
172
186
  const { data: { snapshots } } = await props.apiClient.listSnapshots(props.projectId);
173
187
  writer(props).end(snapshots, {
@@ -187,12 +201,14 @@ const get = async (props) => {
187
201
  const create = async (props) => {
188
202
  if (props.lsn !== void 0 && !looksLikeLSN(props.lsn)) throw new Error(`Invalid --lsn value: "${props.lsn}". Expected an LSN like 0/1F3C8A0.`);
189
203
  if (props.timestamp !== void 0 && !looksLikeTimestamp(props.timestamp)) throw new Error(`Invalid --timestamp value: "${props.timestamp}". Use an RFC 3339 timestamp, e.g. 2025-01-01T00:00:00Z.`);
204
+ if (props.slug !== void 0 && !SNAPSHOT_SLUG.test(props.slug)) throw new Error(`Invalid --slug value: "${props.slug}". Use 1-63 characters: start with a lowercase letter, then lowercase letters, digits, or hyphens, and end with a letter or digit.`);
190
205
  const { branchId } = await resolveBranchRef({
191
206
  ...props,
192
207
  branch: props.branch
193
208
  });
194
209
  const { data } = await retryOnLock(() => props.apiClient.createSnapshot(props.projectId, branchId, {
195
210
  name: props.name,
211
+ slug: props.slug,
196
212
  timestamp: props.timestamp,
197
213
  lsn: props.lsn,
198
214
  expires_at: props.expiresAt ? toIso(props.expiresAt, "--expires-at") : void 0