neon 7.0.1 → 7.0.3

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
@@ -47,10 +47,17 @@ brew upgrade neonctl
47
47
  To upgrade a binary version, download the latest binary file, as described above, and replace your old binary with the new one.
48
48
 
49
49
  Global npm, pnpm, Bun, and Homebrew installs check for updates once per day.
50
- When an update is available, terminal commands periodically print the matching
51
- upgrade command. Set `NO_UPDATE_NOTIFIER=1` to disable these checks. The notifier
50
+ When an update is available, terminal commands print the matching upgrade command
51
+ every three days. Set `NO_UPDATE_NOTIFIER=1` to disable these checks. The notifier
52
52
  does not run in CI, for machine-readable output, or from standalone binaries.
53
53
 
54
+ On macOS and Linux, the same terminal commands warn once a day when more than one
55
+ global `neon` install is on PATH, listing each one in PATH order. A project's
56
+ `node_modules/.bin`, which `npx` and package scripts add, doesn't count. When one
57
+ of them is Homebrew, the warning recommends `brew uninstall neonctl`, which
58
+ leaves `neon` running the other install, such as npm. While duplicates remain, the update notice is held back;
59
+ `NO_UPDATE_NOTIFIER=1` silences both.
60
+
54
61
  ## Connect
55
62
 
56
63
  Run the following command to authenticate a connection to Neon:
@@ -1,6 +1,6 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
- import { describeOperation, getEndpoints, loadSpec } from "../utils/openapi.js";
3
2
  import { writer } from "../writer.js";
3
+ import { describeOperation, getEndpoints, loadSpec } from "../utils/openapi.js";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import YAML from "yaml";
@@ -4,13 +4,13 @@ import { log } from "../log.js";
4
4
  import { recordCommandSuccessExtras, recordScaffoldedTemplate } from "../analytics.js";
5
5
  import { getCliName } from "../utils/cli_name.js";
6
6
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
7
+ import { coerceAgentFlag } from "../utils/agent_flag.js";
8
+ import { assertNamedAgentTooling, chooseYesAgentTooling, initPluginAgents, initSkillsMcpAgents, postScaffoldActions, projectContextFile, resolveNamedAgents } from "../init/plan.js";
9
+ import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
7
10
  import { writer } from "../writer.js";
8
11
  import { FALLBACK_TEMPLATES, ensureTargetUsable, fetchTemplates, findTemplate, scaffoldTemplate, templateIds } from "../init/bootstrap.js";
9
12
  import { agentSetupDoneLabel, formatInitDone, printInitBanner, printInitDone, shouldPrintInitBanner } from "../init/chrome.js";
10
- import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
11
13
  import { runAuthenticatedLink } from "../init/link.js";
12
- import { coerceAgentFlag } from "../utils/agent_flag.js";
13
- import { assertNamedAgentTooling, chooseYesAgentTooling, initPluginAgents, initSkillsMcpAgents, postScaffoldActions, projectContextFile, resolveNamedAgents } from "../init/plan.js";
14
14
  import { formatTemplateTitle } from "../init/template_title.js";
15
15
  import { pickAgentSetupInteractively, pickInitLinkInteractively } from "../init/wizard.js";
16
16
  import { runAgentTooling } from "../init/tooling.js";
@@ -2,11 +2,11 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { retryOnLock } from "../api.js";
4
4
  import { contextBranch, readContextFile } from "../context.js";
5
+ import { branchCreateRequest } from "../parameters.gen.js";
5
6
  import { writer } from "../writer.js";
6
7
  import { EndpointType } from "../utils/api_enums.js";
7
8
  import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp } from "../utils/formats.js";
8
9
  import { branchIdFromProps, branchIdResolve, fillSingleProject } from "../utils/enrichers.js";
9
- import { branchCreateRequest } from "../parameters.gen.js";
10
10
  import { getComputeUnits } from "../utils/compute_units.js";
11
11
  import { psql } from "../utils/psql.js";
12
12
  import { parsePointInTime } from "../utils/point_in_time.js";
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { isNeonApiError, retryOnLock } from "../api.js";
4
+ import { createProjectBranchBucket, deleteProjectBranchBucket, deleteProjectBranchBucketObject, deleteProjectBranchBucketObjectsByPrefix, getProjectBranchBucketObject, listProjectBranchBucketObjects, listProjectBranchBuckets, presignUpload } from "../storage_api.js";
4
5
  import { writer } from "../writer.js";
5
6
  import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
6
- import { createProjectBranchBucket, deleteProjectBranchBucket, deleteProjectBranchBucketObject, deleteProjectBranchBucketObjectsByPrefix, getProjectBranchBucketObject, listProjectBranchBucketObjects, listProjectBranchBuckets, presignUpload } from "../storage_api.js";
7
7
  import { createReadStream, createWriteStream } from "node:fs";
8
8
  import { basename } from "node:path";
9
9
  import { stat, unlink } from "node:fs/promises";
@@ -10,12 +10,12 @@ import { editNeonConfig } from "../config_edit.js";
10
10
  import { toNeonConfigView } from "../config_format.js";
11
11
  import { declaredNeonServices } from "../config_services.js";
12
12
  import { loadEnvFileIntoProcess } from "../env_file.js";
13
+ import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
13
14
  import { writer } from "../writer.js";
14
15
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
15
16
  import { assertAiGatewayProvisionable, warnAiGateway } from "../utils/ai_gateway_notice.js";
16
17
  import { announceTargetBranch } from "../utils/branch_notice.js";
17
18
  import { renderAppliedChanges, renderBranchSettingConflicts, renderCustomDomainFollowup } from "../utils/config_diff.js";
18
- import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
19
19
  import { bundleEntry } from "../utils/esbuild.js";
20
20
  import { readGitContext } from "../utils/git.js";
21
21
  import { resolveHookEnv, runDeployAfterHook, runDeployBeforeHook } from "../utils/hooks.js";
@@ -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 { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry } from "@neon/config-runtime";
17
16
  import { spawn, spawnSync } from "node:child_process";
17
+ import { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry } from "@neon/config-runtime";
18
18
  import { once } from "node:events";
19
19
  import { createServer } from "node:net";
20
20
  //#region src/commands/dev.ts
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { getCliName } from "../utils/cli_name.js";
4
+ import { projectUpdateRequest } from "../parameters.gen.js";
4
5
  import { writer } from "../writer.js";
5
6
  import { fillSingleProject } from "../utils/enrichers.js";
6
- import { projectUpdateRequest } from "../parameters.gen.js";
7
7
  //#region src/commands/ip_allow.ts
8
8
  var ip_allow_exports = /* @__PURE__ */ __exportAll({
9
9
  builder: () => builder,
@@ -0,0 +1,316 @@
1
+ import { getCliName } from "../utils/cli_name.js";
2
+ import { Parser } from "yargs/helpers";
3
+ //#region src/commands/manifest.ts
4
+ /**
5
+ * Every top-level command, in help order, with the metadata yargs needs to list and route it.
6
+ * A command's module (and everything it imports) loads only when yargs selects that command,
7
+ * so `neon --help` and `neon projects list` don't pay for `init`, `dev`, or `psql`.
8
+ *
9
+ * `command`, `aliases`, and `describe` must equal the module's own exports; `manifest.test.ts`
10
+ * enforces that. `load` uses a literal specifier so bundlers can see and package each chunk.
11
+ */
12
+ const commandManifest = [
13
+ {
14
+ command: "auth",
15
+ aliases: ["login"],
16
+ describe: "Sign in with a browser. See --help for API keys and profiles",
17
+ load: () => import("./auth.js")
18
+ },
19
+ {
20
+ command: "profile",
21
+ aliases: ["profiles"],
22
+ describe: "Manage named sets of Neon credentials",
23
+ load: () => import("./profile.js")
24
+ },
25
+ {
26
+ command: "api-keys",
27
+ aliases: ["api-key"],
28
+ describe: "Manage API keys",
29
+ load: () => import("./api_keys.js")
30
+ },
31
+ {
32
+ command: "api [path]",
33
+ describe: "Call any Neon API route directly (authenticated passthrough)",
34
+ load: () => import("./api.js")
35
+ },
36
+ {
37
+ command: "me",
38
+ describe: "Show current user",
39
+ load: () => import("./user.js")
40
+ },
41
+ {
42
+ command: "orgs",
43
+ aliases: ["org"],
44
+ describe: "Manage organizations",
45
+ load: () => import("./orgs.js")
46
+ },
47
+ {
48
+ command: "projects",
49
+ aliases: ["project"],
50
+ describe: "Manage projects",
51
+ load: () => import("./projects.js")
52
+ },
53
+ {
54
+ command: "ip-allow",
55
+ describe: "Manage IP Allow",
56
+ load: () => import("./ip_allow.js")
57
+ },
58
+ {
59
+ command: "vpc",
60
+ describe: "Manage VPC endpoints and project VPC restrictions",
61
+ load: () => import("./vpc_endpoints.js")
62
+ },
63
+ {
64
+ command: "neon-auth",
65
+ describe: "Manage Neon Auth",
66
+ load: () => import("./neon_auth.js")
67
+ },
68
+ {
69
+ command: "branches",
70
+ aliases: ["branch"],
71
+ describe: "Manage branches",
72
+ load: () => import("./branches.js")
73
+ },
74
+ {
75
+ command: "databases",
76
+ aliases: ["database", "db"],
77
+ describe: "Manage databases",
78
+ load: () => import("./databases.js")
79
+ },
80
+ {
81
+ command: "roles",
82
+ aliases: ["role"],
83
+ describe: "Manage roles",
84
+ load: () => import("./roles.js")
85
+ },
86
+ {
87
+ command: "operations",
88
+ aliases: ["operation"],
89
+ describe: "Manage operations",
90
+ load: () => import("./operations.js")
91
+ },
92
+ {
93
+ command: "logs",
94
+ describe: "Query branch logs",
95
+ load: () => import("./logs.js")
96
+ },
97
+ {
98
+ command: "snapshots",
99
+ aliases: ["snapshot"],
100
+ describe: "Manage snapshots",
101
+ load: () => import("./snapshots.js")
102
+ },
103
+ {
104
+ command: "inspect",
105
+ aliases: ["inspection"],
106
+ describe: "Inspect a branch's Postgres health and configuration",
107
+ load: () => import("./inspect.js")
108
+ },
109
+ {
110
+ command: "connection-string [branch]",
111
+ aliases: ["cs"],
112
+ describe: "Get connection string",
113
+ load: () => import("./connection_string.js")
114
+ },
115
+ {
116
+ command: "psql [branch]",
117
+ describe: "Connect to a database via psql",
118
+ load: () => import("./psql.js")
119
+ },
120
+ {
121
+ command: "set-context",
122
+ describe: `Deprecated: use \`${getCliName()} link\`. Set the .neon context (raw write).`,
123
+ load: () => import("./set_context.js")
124
+ },
125
+ {
126
+ command: "checkout [id|name]",
127
+ describe: "Pin a branch in the local context (.neon) so subsequent commands target it",
128
+ load: () => import("./checkout.js")
129
+ },
130
+ {
131
+ command: "git",
132
+ aliases: [],
133
+ describe: "Sync the checked-out Neon branch to your git branch (Preview)",
134
+ load: () => import("./git.js")
135
+ },
136
+ {
137
+ command: "link",
138
+ describe: "Link the current directory to a Neon project",
139
+ load: () => import("./link.js")
140
+ },
141
+ {
142
+ command: "open",
143
+ describe: "Open the linked project in the Neon Console",
144
+ load: () => import("./open.js")
145
+ },
146
+ {
147
+ command: "claim",
148
+ aliases: ["claimable"],
149
+ describe: "Create and claim temporary Neon projects",
150
+ load: () => import("./claim.js")
151
+ },
152
+ {
153
+ command: "init",
154
+ describe: "Set up coding agents and this directory for Neon. -y is Recommended. -y with Custom flags applies those answers without prompts.",
155
+ load: () => import("./init.js")
156
+ },
157
+ {
158
+ command: "mcp",
159
+ describe: "Install the Neon MCP server into coding agents",
160
+ load: () => import("./mcp.js")
161
+ },
162
+ {
163
+ command: "plugins",
164
+ aliases: ["plugin"],
165
+ describe: "Install the Neon plugin into coding agents",
166
+ load: () => import("./plugins.js")
167
+ },
168
+ {
169
+ command: "skills",
170
+ aliases: ["skill"],
171
+ describe: "Install Neon agent skills into coding agents",
172
+ load: () => import("./skills.js")
173
+ },
174
+ {
175
+ command: "ask",
176
+ describe: "Ask a question about Neon",
177
+ load: () => import("./ask.js")
178
+ },
179
+ {
180
+ command: "data-api",
181
+ describe: "Manage the Neon Data API for a database",
182
+ load: () => import("./data_api.js")
183
+ },
184
+ {
185
+ command: "functions",
186
+ aliases: ["function"],
187
+ describe: "Manage Neon Functions",
188
+ load: () => import("./functions.js")
189
+ },
190
+ {
191
+ command: "triggers",
192
+ aliases: ["trigger"],
193
+ describe: "Manage function triggers",
194
+ load: () => import("./triggers.js")
195
+ },
196
+ {
197
+ command: "credentials",
198
+ aliases: ["credential"],
199
+ describe: "Manage branch credentials",
200
+ load: () => import("./credentials.js")
201
+ },
202
+ {
203
+ command: "dev",
204
+ describe: "Run Neon Functions locally with a dev server",
205
+ load: () => import("./dev.js")
206
+ },
207
+ {
208
+ command: "diff [compare-branch]",
209
+ describe: "Show a git-style schema diff between the current branch and another branch",
210
+ load: () => import("./diff.js")
211
+ },
212
+ {
213
+ command: "config",
214
+ describe: "Manage a branch with a neon.ts policy",
215
+ load: () => import("./config.js")
216
+ },
217
+ {
218
+ command: "status",
219
+ describe: "Show the branch's live Neon state (alias of `config status`)",
220
+ load: () => import("./status.js")
221
+ },
222
+ {
223
+ command: "deploy",
224
+ describe: "Apply a neon.ts policy to a branch (alias for `config apply`)",
225
+ load: () => import("./deploy.js")
226
+ },
227
+ {
228
+ command: "env",
229
+ describe: "Manage a branch's Neon env variables locally",
230
+ load: () => import("./env.js")
231
+ },
232
+ {
233
+ command: "buckets",
234
+ aliases: ["bucket"],
235
+ describe: "Manage branch object-storage buckets and their objects",
236
+ load: () => import("./bucket.js")
237
+ },
238
+ {
239
+ command: "bootstrap [directory]",
240
+ describe: "Scaffold a new project from a Neon starter template, then install agent tooling and link a Neon project",
241
+ load: () => import("./bootstrap.js")
242
+ }
243
+ ];
244
+ const COMPLETION_COMMAND = "completion";
245
+ const COMPLETION_FLAG = "--get-yargs-completions";
246
+ const verbOf = (entry) => entry.command.split(" ")[0];
247
+ const entryByName = new Map(commandManifest.flatMap((entry) => [verbOf(entry), ...entry.aliases ?? []].map((name) => [name, entry])));
248
+ /**
249
+ * The command yargs 17 will run for these top-level positionals: it walks them in order,
250
+ * skips `completion`, runs the first one that names a command, and stops at the first one
251
+ * that doesn't (`kRunYargsParserAndExecuteCommands` in yargs-factory.js).
252
+ */
253
+ const selectCommand = (positionals) => {
254
+ const first = positionals.find((p) => String(p) !== COMPLETION_COMMAND);
255
+ return first === void 0 ? void 0 : entryByName.get(String(first));
256
+ };
257
+ /**
258
+ * The command whose builder yargs 17 runs to complete `--get-yargs-completions <words>`: the
259
+ * first word that is a command's canonical name, at any position; aliases don't count there
260
+ * (`defaultCompletion` in completion.js looks words up in the handler map, keyed by name).
261
+ */
262
+ const selectCompletionCommand = (words) => words.map((word) => commandManifest.find((entry) => verbOf(entry) === word)).find((entry) => entry);
263
+ function assertHasGetOptions(argv) {
264
+ if (!("getOptions" in argv) || typeof argv.getOptions !== "function") throw new Error("yargs no longer exposes getOptions(); update preloadCommand for the installed yargs");
265
+ }
266
+ const loaded = /* @__PURE__ */ new Map();
267
+ /**
268
+ * Import the module of the command yargs is about to run, before it runs, so its builder can
269
+ * stay synchronous. Parses argv with yargs' own parser and the top-level options yargs will
270
+ * use, so an option value that happens to be a command name (`--api-key projects`) can't
271
+ * change the answer.
272
+ */
273
+ const preloadCommand = async (cli, args) => {
274
+ const doubleDash = args.indexOf("--");
275
+ const completionAt = (doubleDash === -1 ? args : args.slice(0, doubleDash)).indexOf(COMPLETION_FLAG);
276
+ let entry;
277
+ if (completionAt >= 0) entry = selectCompletionCommand(args.slice(completionAt + 1));
278
+ else {
279
+ assertHasGetOptions(cli);
280
+ const options = cli.getOptions();
281
+ const { argv } = Parser.detailed([...args], {
282
+ ...options,
283
+ configuration: {
284
+ ...options?.configuration,
285
+ "populate--": true,
286
+ "parse-positional-numbers": false
287
+ }
288
+ });
289
+ entry = selectCommand(argv._);
290
+ }
291
+ if (entry && !loaded.has(entry)) loaded.set(entry, await entry.load());
292
+ return entry;
293
+ };
294
+ /**
295
+ * The manifest as yargs command modules. The builder is synchronous when `preloadCommand`
296
+ * already imported the module, which is every invocation it predicts correctly. A mispredicted
297
+ * command still runs, because yargs 17 awaits a builder's promise when it runs a command; shell
298
+ * completion does not await it, so a mispredicted completion lists no subcommands.
299
+ */
300
+ const lazyCommands = commandManifest.map((entry) => ({
301
+ command: entry.command,
302
+ aliases: entry.aliases,
303
+ describe: entry.describe,
304
+ builder: (argv) => {
305
+ const mod = loaded.get(entry);
306
+ return mod ? mod.builder(argv) : entry.load().then((m) => {
307
+ loaded.set(entry, m);
308
+ return m.builder(argv);
309
+ });
310
+ },
311
+ handler: async (args) => {
312
+ return (loaded.get(entry) ?? await entry.load()).handler?.(args);
313
+ }
314
+ }));
315
+ //#endregion
316
+ export { commandManifest, lazyCommands, preloadCommand, selectCommand, selectCompletionCommand };
@@ -4,11 +4,11 @@ import { readContextFile } from "../context.js";
4
4
  import { recordCommandSuccessExtras } from "../analytics.js";
5
5
  import { getCliName } from "../utils/cli_name.js";
6
6
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
7
+ import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
8
+ import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
7
9
  import { writer } from "../writer.js";
8
10
  import { noPassthrough, single } from "../utils/flags.js";
9
11
  import { AGENT_MCP_SKIP_MESSAGE } from "../init/copy.js";
10
- import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
11
- import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
12
12
  import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
13
13
  import { canPickAgentsInteractively, skippableAgentPicker } from "../utils/agent_picker.js";
14
14
  import { resolveMcpPlan } from "../mcp/plan.js";
@@ -2,13 +2,13 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { recordCommandSuccessExtras } from "../analytics.js";
4
4
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
5
- import { writer } from "../writer.js";
6
- import { noPassthrough } from "../utils/flags.js";
7
- import { AGENT_PLUGIN_SKIP_MESSAGE } from "../init/copy.js";
8
5
  import { getAgentDisplayName } from "../mcp/agents.js";
9
6
  import { NEON_MCP_URL } from "../mcp/install.js";
10
7
  import "../init/agents.js";
11
8
  import { pluginsInstallableAgents } from "../plugins/targets.js";
9
+ import { writer } from "../writer.js";
10
+ import { noPassthrough } from "../utils/flags.js";
11
+ import { AGENT_PLUGIN_SKIP_MESSAGE } from "../init/copy.js";
12
12
  import { canPickAgentsInteractively, skippableAgentPicker } from "../utils/agent_picker.js";
13
13
  import { resolvePluginsPlan } from "../plugins/plan.js";
14
14
  import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
@@ -11,10 +11,10 @@ import { auth, revokeToken } from "../auth.js";
11
11
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
12
12
  import { readOutgoingCredential, retirePreviousCredential, revokeTokenSet, withdrawKey } from "../retire_credential.js";
13
13
  import { authFlow, usableCredential } from "./auth.js";
14
+ import { identityFromAuthDetails, isApiKeyMethod, mintedKeyName, notAnApiKeyMessage } from "../profile_keys.js";
14
15
  import { writer } from "../writer.js";
15
16
  import { noPassthrough, single } from "../utils/flags.js";
16
17
  import { orgIdForProject } from "./api_keys.js";
17
- import { identityFromAuthDetails, isApiKeyMethod, mintedKeyName, notAnApiKeyMessage } from "../profile_keys.js";
18
18
  import { existsSync, readFileSync, rmSync } from "node:fs";
19
19
  import { isAbsolute, relative } from "node:path";
20
20
  import prompts from "prompts";
@@ -4,8 +4,8 @@ import { log } from "../log.js";
4
4
  import { isNeonApiError, messageFromBody } from "../api.js";
5
5
  import { updateContextFile } from "../context.js";
6
6
  import { getCliName } from "../utils/cli_name.js";
7
- import { writer } from "../writer.js";
8
7
  import { projectCreateRequest, projectUpdateRequest } from "../parameters.gen.js";
8
+ import { writer } from "../writer.js";
9
9
  import { getComputeUnits } from "../utils/compute_units.js";
10
10
  import { psql } from "../utils/psql.js";
11
11
  import prompts from "prompts";
@@ -2,12 +2,12 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { recordCommandSuccessExtras } from "../analytics.js";
4
4
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
5
- import { writer } from "../writer.js";
6
- import { noPassthrough } from "../utils/flags.js";
7
- import { AGENT_SKILLS_SKIP_MESSAGE } from "../init/copy.js";
8
5
  import { getAgentDisplayName } from "../mcp/agents.js";
9
6
  import "../init/agents.js";
10
7
  import { mappedSkillsAgentNames, skillsInstallableAgents } from "../skills/targets.js";
8
+ import { writer } from "../writer.js";
9
+ import { noPassthrough } from "../utils/flags.js";
10
+ import { AGENT_SKILLS_SKIP_MESSAGE } from "../init/copy.js";
11
11
  import { canPickAgentsInteractively, skippableAgentPicker } from "../utils/agent_picker.js";
12
12
  import { skillsHelpValues, skillsYesHelp } from "../skills/catalog.js";
13
13
  import { confirmSkillsUpdate } from "../skills/wizard.js";
package/dist/index.js CHANGED
@@ -12,8 +12,8 @@ import { getCliName } from "./utils/cli_name.js";
12
12
  import { ensureAuth } from "./commands/auth.js";
13
13
  import { recoverFrom401 } from "./auth_recovery.js";
14
14
  import { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageErrorHelp } from "./help.js";
15
+ import { lazyCommands, preloadCommand } from "./commands/manifest.js";
15
16
  import { rewriteUnknownAgentArg } from "./init/plan.js";
16
- import commands_default from "./commands/index.js";
17
17
  import { recoverFromSSO } from "./sso_recovery.js";
18
18
  import { notifyIfUpdateAvailable } from "./update_notifier.js";
19
19
  import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
@@ -128,7 +128,7 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
128
128
  if (args.help) await showHelp(builder);
129
129
  }, true).middleware(async (args) => {
130
130
  if (args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0]) || args._.length > 1 && isBareParentCommand(builder, args._)) await showHelp(builder);
131
- }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(commands_default).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
131
+ }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(lazyCommands).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
132
132
  async function handleError(msg, err, canRetry) {
133
133
  if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
134
134
  await showHelp(builder);
@@ -194,6 +194,7 @@ async function handleError(msg, err, canRetry) {
194
194
  (async () => {
195
195
  let attempts = 0;
196
196
  const MAX_ATTEMPTS = 2;
197
+ await preloadCommand(builder, hideBin(process.argv));
197
198
  while (attempts < MAX_ATTEMPTS) try {
198
199
  const args = await builder.argv;
199
200
  trackEvent("cli_command_success", commandSuccessProperties(args));
package/dist/init/mode.js CHANGED
@@ -1,5 +1,5 @@
1
- import { NON_TTY_AGENT_SETUP, NO_AGENT_SETUP_CONFLICT } from "./copy.js";
2
1
  import { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
2
+ import { NON_TTY_AGENT_SETUP, NO_AGENT_SETUP_CONFLICT } from "./copy.js";
3
3
  //#region src/init/mode.ts
4
4
  const hasInitMcpFlags = (input) => input.mcpConfigLocation !== void 0 || input.mcpAuth !== void 0 || input.mcpProjectScoped === true;
5
5
  const hasSkillsFlag = (skills) => skills !== void 0 && skills.length > 0;
package/dist/init/run.js CHANGED
@@ -8,16 +8,16 @@ import { readLinkedClaimableCredentials } from "../claimable/state.js";
8
8
  import { getCliName } from "../utils/cli_name.js";
9
9
  import { deprecatedServiceMessage, parseServices, servicesFlagValue } from "../neon_services.js";
10
10
  import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE } from "../config_template.js";
11
- import { CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, MCP_SCOPED_NEEDS_PROJECT, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, PROGRESS, YES_LINK_NEEDS_AUTH, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, installFailedNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, namedAgentsUnavailable, skippedLinkNext, unattendedUnauthedNext } from "./copy.js";
12
- import { agentsRowValue, configRowValue, formatInitDone, headingForKind, printInitBanner, printInitDone, printInitProgress, projectRowValue, shouldPrintInitBanner } from "./chrome.js";
13
- import { inferPackageManager, resolvePackageManager } from "../utils/package_manager.js";
14
- import { ConfigInstallFailed, hasNeonConfigFile, initCmd, neonConfigFilename } from "../commands/config.js";
15
- import { runAuthenticatedLink } from "./link.js";
16
11
  import { getAgentDisplayName } from "../mcp/agents.js";
17
12
  import { mcpInstallableAgents } from "../mcp/targets.js";
18
13
  import { missingPluginsCommands, pluginsInstallableAgents } from "../plugins/targets.js";
19
14
  import { skillsInstallableAgents } from "../skills/targets.js";
20
15
  import { FALLBACK_SKILLS_AGENTS, assertNamedAgentTooling, funnelAgentSetup, planInitToolingSteps, recommendedTooling, resolveNamedAgents, splitInitTooling } from "./plan.js";
16
+ import { inferPackageManager, resolvePackageManager } from "../utils/package_manager.js";
17
+ import { CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, MCP_SCOPED_NEEDS_PROJECT, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, PROGRESS, YES_LINK_NEEDS_AUTH, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, installFailedNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, namedAgentsUnavailable, skippedLinkNext, unattendedUnauthedNext } from "./copy.js";
18
+ import { agentsRowValue, configRowValue, formatInitDone, headingForKind, printInitBanner, printInitDone, printInitProgress, projectRowValue, shouldPrintInitBanner } from "./chrome.js";
19
+ import { ConfigInstallFailed, hasNeonConfigFile, initCmd, neonConfigFilename } from "../commands/config.js";
20
+ import { runAuthenticatedLink } from "./link.js";
21
21
  import { pullInitEnv } from "./auth.js";
22
22
  import { InitCancelled, raceSigint, restoreCursor } from "./cancelled.js";
23
23
  import { pickAgentSetupInteractively, pickInitAgentsInteractively, pickInitConfigInteractively, pickInitMcpAuthInteractively, pickInitMcpConfigLocationInteractively, pickInitModeInteractively, pickInitPackageManagerInteractively, pickInitProjectSetupInteractively, pickInitServicesInteractively, pickInitSkillsInteractively } from "./wizard.js";
@@ -1,7 +1,7 @@
1
1
  import { log } from "../log.js";
2
2
  import { getCliName } from "../utils/cli_name.js";
3
- import { PROGRESS } from "./copy.js";
4
3
  import { assertNamedAgentTooling, chooseYesAgentTooling, collectYesAgents, initYesSupportedAgents, noDetectedAgentsMessage, planAgentSteps, planToolingSteps, planYesAgentSteps, resolveInitAgentSetup } from "./plan.js";
4
+ import { PROGRESS } from "./copy.js";
5
5
  import { detectAgent } from "./detect_host.js";
6
6
  import { AgentSelectionSkipped, canPickAgentsInteractively } from "../utils/agent_picker.js";
7
7
  import { mcpInstallError, reportMcpInstall } from "../commands/mcp.js";
@@ -1,9 +1,9 @@
1
1
  import { CONFIG_INIT_SERVICES } from "../config_template.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
- import { installedPackageManagers } from "../utils/package_manager.js";
4
- import { POSTGRES_SERVICE_CHOICE, keepPostgresSelected } from "../utils/service_picker.js";
5
2
  import { getAgentDisplayName, listMcpAgentIds } from "../mcp/agents.js";
6
3
  import { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
4
+ import { installedPackageManagers } from "../utils/package_manager.js";
5
+ import { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, PACKAGE_MANAGER_MESSAGE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, agentMissingCommandDescription } from "./copy.js";
6
+ import { POSTGRES_SERVICE_CHOICE, keepPostgresSelected } from "../utils/service_picker.js";
7
7
  import { canPickAgentsInteractively, pickAgentsInteractively } from "../utils/agent_picker.js";
8
8
  import { NEON_SKILL_CATALOG, listSkillIds } from "../skills/catalog.js";
9
9
  import { InitCancelled, restoreCursorVisibilityOnAbort } from "./cancelled.js";
@@ -1,7 +1,7 @@
1
1
  import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { basename, dirname, join } from "node:path";
3
- import { execFileSync } from "node:child_process";
4
3
  import { agents, upsertServer } from "add-mcp";
4
+ import { execFileSync } from "node:child_process";
5
5
  //#region src/mcp/install.ts
6
6
  const NEON_MCP_URL = "https://mcp.neon.tech/mcp";
7
7
  const NEON_MCP_NAME = "Neon";
package/dist/mcp/mint.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { log } from "../log.js";
2
2
  import { isNeonApiError } from "../api.js";
3
3
  import { getCliName } from "../utils/cli_name.js";
4
- import { orgIdForProject } from "../commands/api_keys.js";
5
4
  import { mintedKeyName } from "../profile_keys.js";
5
+ import { orgIdForProject } from "../commands/api_keys.js";
6
6
  //#region src/mcp/mint.ts
7
7
  const cannotMintMessage = "This CLI credential cannot mint API keys. Organization and project-scoped keys cannot create other keys. Sign in with `neon auth` or pass a personal API key.";
8
8
  function mintedKeyRevokeCommand(key) {
@@ -0,0 +1,47 @@
1
+ import { log } from "./log.js";
2
+ import { writeJsonFile } from "./utils/json_file.js";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ //#region src/notices.ts
6
+ /**
7
+ * When each terminal notice was last shown, so a notice repeats on its own interval rather
8
+ * than on every command. Kept apart from the update cache: that cache is tied to one install
9
+ * source and rewritten by a background worker, while a notice can come from any copy.
10
+ *
11
+ * Throttling is best effort. Two commands started together can both show a due notice.
12
+ */
13
+ const NOTICES_FILE = "notices.json";
14
+ /**
15
+ * Keeps ids this version doesn't know, so an older and a newer CLI sharing one config
16
+ * directory don't erase each other's timestamps.
17
+ */
18
+ const parseNoticeLog = (raw) => {
19
+ let value;
20
+ try {
21
+ value = JSON.parse(raw);
22
+ } catch {
23
+ return {};
24
+ }
25
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
26
+ return Object.fromEntries(Object.entries(value).filter((entry) => typeof entry[1] === "number" && Number.isFinite(entry[1])));
27
+ };
28
+ const isNoticeDue = (shownAt, now, intervalMs) => shownAt === void 0 || now - shownAt >= intervalMs;
29
+ const readNoticeLog = (path) => {
30
+ try {
31
+ return parseNoticeLog(readFileSync(path, "utf8"));
32
+ } catch {
33
+ return {};
34
+ }
35
+ };
36
+ const showNotice = ({ configDir, id, intervalMs, message, now = Date.now() }) => {
37
+ const path = join(configDir, NOTICES_FILE);
38
+ const shown = readNoticeLog(path);
39
+ if (!isNoticeDue(shown[id], now, intervalMs)) return;
40
+ log.warning("%s", message);
41
+ writeJsonFile(path, {
42
+ ...shown,
43
+ [id]: now
44
+ }, "CLI notice log");
45
+ };
46
+ //#endregion
47
+ export { isNoticeDue, parseNoticeLog, showNotice };
@@ -1,8 +1,8 @@
1
1
  import { detectInstalledAgents, listMcpAgentIds, tryResolveAddMcpAgentId, uniqueAgentIds } from "../mcp/agents.js";
2
2
  import "../init/agents.js";
3
3
  import { delimiter, dirname, join, resolve } from "node:path";
4
- import which from "which";
5
4
  import { detectProjectAgents } from "add-mcp";
5
+ import which from "which";
6
6
  //#region src/plugins/targets.ts
7
7
  const CLAUDE_CODE = { target: "claude-code" };
8
8
  const PLUGINS_TARGET_BY_TYPE = {
@@ -26,8 +26,15 @@ const reserveClosedPort = () => new Promise((resolve, reject) => {
26
26
  const test = test$1.extend({
27
27
  runMockServer: async ({}, use) => {
28
28
  let startedServer;
29
- await use(async (mockDir) => {
29
+ await use(async (mockDir, onRequest) => {
30
30
  const app = express();
31
+ if (onRequest) app.use((req, _res, next) => {
32
+ onRequest({
33
+ method: req.method,
34
+ path: req.path
35
+ });
36
+ next();
37
+ });
31
38
  app.use(express.json());
32
39
  app.use("/", emocks(join(process.cwd(), "mocks", mockDir), { "404": (_req, res) => res.status(404).send({ message: "Not Found" }) }));
33
40
  const server = await new Promise((resolve) => {
@@ -1,25 +1,29 @@
1
1
  import { isCi } from "./env.js";
2
2
  import { log } from "./log.js";
3
- import { recommendedCliUpgradeCommand } from "./utils/package_manager.js";
4
- import { existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
5
- import { join } from "node:path";
3
+ import { writeJsonFile } from "./utils/json_file.js";
4
+ import { showNotice } from "./notices.js";
5
+ import { detectCliInstallMethod, recommendedCliUpgradeCommand } from "./utils/package_manager.js";
6
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
7
+ import { basename, dirname, join } from "node:path";
6
8
  import { fileURLToPath } from "node:url";
7
9
  import { spawn } from "node:child_process";
10
+ import which from "which";
8
11
  import semver from "semver";
9
12
  //#region src/update_notifier.ts
10
- const CHECK_INTERVAL_MS = 864e5;
11
- const NOTIFY_INTERVAL_MS = 3 * CHECK_INTERVAL_MS;
13
+ const DAY_MS = 864e5;
14
+ const CHECK_INTERVAL_MS = DAY_MS;
15
+ const UPDATE_NOTICE_INTERVAL_MS = 3 * DAY_MS;
16
+ const DUPLICATE_NOTICE_INTERVAL_MS = DAY_MS;
12
17
  const CACHE_FILE = "update-check.json";
13
18
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
19
  const parseUpdateCheckCache = (raw) => {
15
20
  try {
16
21
  const value = JSON.parse(raw);
17
- if (!isRecord(value) || typeof value.checkedAt !== "number" || !Number.isFinite(value.checkedAt) || value.source !== "homebrew" && value.source !== "npm" || value.latestVersion !== void 0 && typeof value.latestVersion !== "string" || value.notifiedAt !== void 0 && (typeof value.notifiedAt !== "number" || !Number.isFinite(value.notifiedAt))) return;
22
+ if (!isRecord(value) || typeof value.checkedAt !== "number" || !Number.isFinite(value.checkedAt) || value.source !== "homebrew" && value.source !== "npm" || value.latestVersion !== void 0 && typeof value.latestVersion !== "string") return;
18
23
  return {
19
24
  checkedAt: value.checkedAt,
20
25
  source: value.source,
21
- ...value.latestVersion === void 0 ? {} : { latestVersion: value.latestVersion },
22
- ...value.notifiedAt === void 0 ? {} : { notifiedAt: value.notifiedAt }
26
+ ...value.latestVersion === void 0 ? {} : { latestVersion: value.latestVersion }
23
27
  };
24
28
  } catch {
25
29
  return;
@@ -32,20 +36,7 @@ const readUpdateCheckCache = (cachePath) => {
32
36
  return;
33
37
  }
34
38
  };
35
- const writeUpdateCheckCache = (cachePath, cache) => {
36
- const temporaryPath = `${cachePath}.${process.pid}.tmp`;
37
- try {
38
- writeFileSync(temporaryPath, `${JSON.stringify(cache)}\n`, { mode: 384 });
39
- renameSync(temporaryPath, cachePath);
40
- return true;
41
- } catch (error) {
42
- try {
43
- rmSync(temporaryPath, { force: true });
44
- } catch {}
45
- log.debug("Could not write the CLI update cache: %s", error instanceof Error ? error.message : String(error));
46
- return false;
47
- }
48
- };
39
+ const writeUpdateCheckCache = (cachePath, cache) => writeJsonFile(cachePath, cache, "CLI update cache");
49
40
  const formatUpdateNotice = ({ currentVersion, latestVersion, upgradeCommand }) => {
50
41
  if (semver.valid(currentVersion) === null || semver.valid(latestVersion) === null || !semver.gt(latestVersion, currentVersion)) return;
51
42
  return `Neon CLI update available: ${currentVersion} → ${latestVersion}\n${upgradeCommand}`;
@@ -84,6 +75,71 @@ const spawnUpdateWorker = (cachePath, source) => {
84
75
  log.debug("Could not start the CLI update check: %s", error instanceof Error ? error.message : String(error));
85
76
  }
86
77
  };
78
+ const INSTALL_LABELS = {
79
+ homebrew: "Homebrew",
80
+ bun: "Bun",
81
+ pnpm: "pnpm",
82
+ npm: "npm"
83
+ };
84
+ /**
85
+ * `npm exec`, `npx`, package scripts, and `dlx` prepend a project's or a cache's
86
+ * `node_modules/.bin`, which is gone from PATH once the command ends.
87
+ */
88
+ const isPackageBin = (executable) => {
89
+ const bin = dirname(executable);
90
+ return basename(bin) === ".bin" && basename(dirname(bin)) === "node_modules";
91
+ };
92
+ /**
93
+ * Every distinct `command` executable on `path`, in PATH order, leaving out
94
+ * package-local `node_modules/.bin` entries. Entries that resolve to the same
95
+ * file (a repeated PATH directory, Homebrew's `bin` and `opt` links) count once.
96
+ */
97
+ const findCliInstalls = (command, path) => {
98
+ const seen = /* @__PURE__ */ new Set();
99
+ const installs = [];
100
+ for (const executable of which.sync(command, {
101
+ all: true,
102
+ nothrow: true,
103
+ path
104
+ }) ?? []) {
105
+ if (isPackageBin(executable)) continue;
106
+ const target = realpathSync(executable);
107
+ if (seen.has(target)) continue;
108
+ seen.add(target);
109
+ installs.push({
110
+ method: detectCliInstallMethod(target),
111
+ path: executable
112
+ });
113
+ }
114
+ return installs;
115
+ };
116
+ const formatDuplicateInstallNotice = ({ installs, otherNeonctl }) => {
117
+ if (installs.length < 2) return void 0;
118
+ const lines = ["Multiple Neon CLI installs found on PATH:", ...installs.map(({ method, path }, index) => {
119
+ const labels = [...method === void 0 ? [] : [INSTALL_LABELS[method]], ...index === 0 ? ["first on PATH"] : []];
120
+ return labels.length === 0 ? ` ${path}` : ` ${path} (${labels.join(", ")})`;
121
+ })];
122
+ if (!installs.some(({ method }) => method === "homebrew")) {
123
+ lines.push("Keep one and uninstall the others.");
124
+ return lines.join("\n");
125
+ }
126
+ lines.push(installs.some(({ method }) => method === "npm") ? "Remove the Homebrew install to use npm:" : "Remove the Homebrew install:", " brew uninstall neonctl");
127
+ if (!otherNeonctl) lines.push("This also removes the `neonctl` command; run `neon` instead.");
128
+ lines.push("Then open a new shell.");
129
+ return lines.join("\n");
130
+ };
131
+ const duplicateInstallNotice = () => {
132
+ try {
133
+ const installs = findCliInstalls("neon", process.env.PATH);
134
+ return formatDuplicateInstallNotice({
135
+ installs,
136
+ otherNeonctl: findCliInstalls("neonctl", process.env.PATH).some(({ method }) => method !== "homebrew")
137
+ });
138
+ } catch (error) {
139
+ log.debug("Could not check PATH for other Neon CLI installs: %s", error instanceof Error ? error.message : String(error));
140
+ return;
141
+ }
142
+ };
87
143
  const isPackagedExecutable = () => "pkg" in process;
88
144
  const shouldRunUpdateNotifier = ({ commandPath, currentBranch, disabled, isCi: runningInCi, isPackaged, output, stderrIsTty, stdoutIsTty }) => !runningInCi && !disabled && !isPackaged && output === "table" && stdoutIsTty && stderrIsTty && commandPath.length > 0 && commandPath[0] !== "completion" && !isCurrentBranchProbe({
89
145
  commandPath,
@@ -100,37 +156,43 @@ const notifyIfUpdateAvailable = ({ commandPath, configDir, currentBranch, curren
100
156
  stderrIsTty: process.stderr.isTTY === true,
101
157
  stdoutIsTty: process.stdout.isTTY === true
102
158
  })) return;
159
+ const now = Date.now();
160
+ if (process.platform !== "win32") {
161
+ const duplicateNotice = duplicateInstallNotice();
162
+ if (duplicateNotice !== void 0) {
163
+ showNotice({
164
+ configDir,
165
+ id: "duplicate-installs",
166
+ intervalMs: DUPLICATE_NOTICE_INTERVAL_MS,
167
+ message: duplicateNotice,
168
+ now
169
+ });
170
+ return;
171
+ }
172
+ }
103
173
  const command = recommendedCliUpgradeCommand(fileURLToPath(import.meta.url));
104
174
  if (command === void 0) return;
105
175
  const cachePath = join(configDir, CACHE_FILE);
106
- const now = Date.now();
107
176
  const source = updateSource(command);
108
177
  const cache = cacheForUpdateSource(readUpdateCheckCache(cachePath), source);
109
- let nextCache = cache;
110
- if (cache?.latestVersion !== void 0 && (cache.notifiedAt === void 0 || now - cache.notifiedAt >= NOTIFY_INTERVAL_MS)) {
111
- const notice = formatUpdateNotice({
112
- currentVersion,
113
- latestVersion: cache.latestVersion,
114
- upgradeCommand: command
115
- });
116
- if (notice !== void 0) {
117
- log.warning("%s", notice);
118
- nextCache = {
119
- ...cache,
120
- notifiedAt: now
121
- };
122
- }
123
- }
124
- if (!shouldRefreshUpdateCheck(cache, now)) {
125
- if (nextCache !== cache && nextCache !== void 0) writeUpdateCheckCache(cachePath, nextCache);
126
- return;
127
- }
128
- nextCache = {
129
- ...nextCache,
178
+ const notice = cache?.latestVersion === void 0 ? void 0 : formatUpdateNotice({
179
+ currentVersion,
180
+ latestVersion: cache.latestVersion,
181
+ upgradeCommand: command
182
+ });
183
+ if (notice !== void 0) showNotice({
184
+ configDir,
185
+ id: "update",
186
+ intervalMs: UPDATE_NOTICE_INTERVAL_MS,
187
+ message: notice,
188
+ now
189
+ });
190
+ if (!shouldRefreshUpdateCheck(cache, now)) return;
191
+ if (writeUpdateCheckCache(cachePath, {
192
+ ...cache,
130
193
  checkedAt: now,
131
194
  source
132
- };
133
- if (writeUpdateCheckCache(cachePath, nextCache)) spawnUpdateWorker(cachePath, source);
195
+ })) spawnUpdateWorker(cachePath, source);
134
196
  };
135
197
  const recordLatestVersion = (cachePath, source, latestVersion) => {
136
198
  if (semver.valid(latestVersion) === null) return;
@@ -142,4 +204,4 @@ const recordLatestVersion = (cachePath, source, latestVersion) => {
142
204
  });
143
205
  };
144
206
  //#endregion
145
- export { cacheForUpdateSource, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
207
+ export { cacheForUpdateSource, findCliInstalls, formatDuplicateInstallNotice, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
@@ -0,0 +1,20 @@
1
+ import { log } from "../log.js";
2
+ import { renameSync, rmSync, writeFileSync } from "node:fs";
3
+ //#region src/utils/json_file.ts
4
+ /** Renames a temporary file into place so a concurrent reader never sees a partial write. */
5
+ const writeJsonFile = (path, value, description) => {
6
+ const temporaryPath = `${path}.${process.pid}.tmp`;
7
+ try {
8
+ writeFileSync(temporaryPath, `${JSON.stringify(value)}\n`, { mode: 384 });
9
+ renameSync(temporaryPath, path);
10
+ return true;
11
+ } catch (error) {
12
+ try {
13
+ rmSync(temporaryPath, { force: true });
14
+ } catch {}
15
+ log.debug("Could not write the %s: %s", description, error instanceof Error ? error.message : String(error));
16
+ return false;
17
+ }
18
+ };
19
+ //#endregion
20
+ export { writeJsonFile };
@@ -159,12 +159,23 @@ const resolvePackageManager = (cwd) => inferPackageManager(cwd) ?? installedPack
159
159
  * as {@link resolvePackageManager} minus the lockfile walk.
160
160
  */
161
161
  const resolveInvokingPackageManager = () => detectInvokingPackageManager() ?? installedPackageManagers()[0] ?? "npm";
162
+ /** How the CLI file at `path` was installed globally, or undefined when it can't be told. */
163
+ const detectCliInstallMethod = (path) => {
164
+ const normalized = path.replace(/\\/g, "/").toLowerCase();
165
+ if (normalized.includes("/cellar/neonctl/")) return "homebrew";
166
+ if (normalized.includes("/.bun/install/global/node_modules/neon/")) return "bun";
167
+ if (normalized.includes("/pnpm/global/") && normalized.includes("/.pnpm/neon@") && normalized.includes("/node_modules/neon/")) return "pnpm";
168
+ if (normalized.includes("/lib/node_modules/neon/") || normalized.includes("/npm/node_modules/neon/")) return "npm";
169
+ };
170
+ const CLI_UPGRADE_COMMANDS = {
171
+ homebrew: "brew upgrade neonctl",
172
+ bun: "bun i -g neon@latest",
173
+ pnpm: "pnpm i -g neon@latest",
174
+ npm: "npm i -g neon@latest"
175
+ };
162
176
  const recommendedCliUpgradeCommand = (modulePath) => {
163
- const path = modulePath.replace(/\\/g, "/").toLowerCase();
164
- if (path.includes("/cellar/neonctl/")) return "brew upgrade neonctl";
165
- if (path.includes("/.bun/install/global/node_modules/neon/")) return "bun i -g neon@latest";
166
- if (path.includes("/pnpm/global/") && path.includes("/.pnpm/neon@") && path.includes("/node_modules/neon/")) return "pnpm i -g neon@latest";
167
- if (path.includes("/lib/node_modules/neon/") || path.includes("/npm/node_modules/neon/")) return "npm i -g neon@latest";
177
+ const method = detectCliInstallMethod(modulePath);
178
+ return method === void 0 ? void 0 : CLI_UPGRADE_COMMANDS[method];
168
179
  };
169
180
  /**
170
181
  * The argv for an install with `pm`: every dependency in the manifest when
@@ -333,4 +344,4 @@ const runCommand = (cmd, args, cwd, env) => new Promise((resolvePromise) => {
333
344
  });
334
345
  });
335
346
  //#endregion
336
- export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, commandEnv, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, recommendedCliUpgradeCommand, resolveInvokingPackageManager, resolvePackageManager, runCommand };
347
+ export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, commandEnv, detectCliInstallMethod, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, recommendedCliUpgradeCommand, resolveInvokingPackageManager, resolvePackageManager, runCommand };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -99,9 +99,9 @@
99
99
  "tsx": "4.22.3",
100
100
  "typescript": "^5.9.0",
101
101
  "vitest": "^3.0.9",
102
+ "@neon-internals/cli-core": "0.0.0",
102
103
  "@neon-internals/env-core": "0.0.26",
103
104
  "@neon/e2e-harness": "0.0.0",
104
- "@neon-internals/cli-core": "0.0.0",
105
105
  "@neon/functions": "0.11.0"
106
106
  },
107
107
  "publishConfig": {
@@ -134,6 +134,9 @@
134
134
  "test": "pnpm --filter neonctl... build && vitest run",
135
135
  "test:ci": "pnpm build && vitest run",
136
136
  "test:e2e": "pnpm build && vitest run --config vitest.e2e.config.ts",
137
+ "test:perf": "pnpm --filter neon... build && vitest run src/perf_budget.test.ts",
138
+ "perf": "tsx scripts/perf/perf.ts",
139
+ "perf:ci-report": "tsx scripts/perf/ci-report.ts",
137
140
  "test:conformance": "vitest run --config tests/psql-conformance/vitest.config.ts"
138
141
  }
139
142
  }