neon 5.0.0 → 6.0.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.
@@ -1,96 +1,254 @@
1
+ import { CONFIG_INIT_SERVICES } from "../config_template.js";
2
+ import { InitCancelled, restoreCursorVisibilityOnAbort } from "./cancelled.js";
3
+ import { AGENT_DETECTED_DESCRIPTION, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, PACKAGE_MANAGER_MESSAGE, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE } from "./copy.js";
4
+ import { installedPackageManagers } from "../utils/package_manager.js";
5
+ import { POSTGRES_SERVICE_CHOICE, keepPostgresSelected } from "../utils/service_picker.js";
6
+ import { getAgentDisplayName, listMcpAgentIds } from "../mcp/agents.js";
1
7
  import { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
2
- import { SKIP_TEMPLATE_VALUE, formatTemplateTitle } from "./template_title.js";
3
8
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
4
- import { NEON_PLUGIN_NAME } from "../plugins/run.js";
9
+ import { NEON_SKILL_CATALOG, listSkillIds } from "../skills/catalog.js";
5
10
  import chalk from "chalk";
6
11
  import prompts from "prompts";
7
12
  //#region src/init/wizard.ts
8
13
  const restoreCursorOnAbort = (state) => {
9
- if (state.aborted) {
10
- process.stdout.write("\x1B[?25h");
11
- process.stdout.write("\n");
12
- process.exit(1);
13
- }
14
+ restoreCursorVisibilityOnAbort(state);
14
15
  };
15
16
  const requireInteractive = () => {
16
17
  if (!canPickAgentsInteractively()) throw new Error(INIT_NEEDS_YES_OR_TERMINAL);
17
18
  };
19
+ const aborted = () => {
20
+ throw new InitCancelled();
21
+ };
22
+ const pickInitModeInteractively = async () => {
23
+ requireInteractive();
24
+ const { mode } = await prompts({
25
+ onState: restoreCursorOnAbort,
26
+ type: "select",
27
+ name: "mode",
28
+ message: MODE_MESSAGE,
29
+ initial: 0,
30
+ choices: [{
31
+ title: MODE_RECOMMENDED_TITLE,
32
+ value: "recommended",
33
+ description: MODE_RECOMMENDED_DESCRIPTION
34
+ }, {
35
+ title: MODE_CUSTOM_TITLE,
36
+ value: "custom",
37
+ description: MODE_CUSTOM_DESCRIPTION
38
+ }]
39
+ });
40
+ if (mode !== "recommended" && mode !== "custom") return aborted();
41
+ return mode;
42
+ };
18
43
  const pickAgentSetupInteractively = async () => {
19
44
  requireInteractive();
20
45
  const { setup } = await prompts({
21
46
  onState: restoreCursorOnAbort,
22
47
  type: "select",
23
48
  name: "setup",
24
- message: "How would you like to set up your coding agents?",
49
+ message: AGENT_SETUP_MESSAGE,
25
50
  initial: 0,
26
51
  choices: [
27
52
  {
28
- title: "Neon plugin (recommended)",
53
+ title: AGENT_SETUP_PLUGIN_TITLE,
29
54
  value: "plugin",
30
- description: `Install ${NEON_PLUGIN_NAME} (skills and MCP together)`
55
+ description: AGENT_SETUP_PLUGIN_DESCRIPTION
31
56
  },
32
57
  {
33
- title: "Skills and MCP separately",
58
+ title: AGENT_SETUP_SKILLS_TITLE,
34
59
  value: "skills-mcp",
35
- description: "Install skills, then the MCP server"
60
+ description: AGENT_SETUP_SKILLS_DESCRIPTION
36
61
  },
37
62
  {
38
- title: "Skip agent setup",
63
+ title: AGENT_SETUP_SKIP_TITLE,
39
64
  value: "skip",
40
- description: "Continue to project setup"
65
+ description: AGENT_SETUP_SKIP_DESCRIPTION
41
66
  }
42
67
  ]
43
68
  });
44
- if (setup !== "plugin" && setup !== "skills-mcp" && setup !== "skip") throw new Error("Aborted.");
69
+ if (setup !== "plugin" && setup !== "skills-mcp" && setup !== "skip") return aborted();
45
70
  return setup;
46
71
  };
47
- /** First catalog template; skip is index 0 so it stays on the first screen. */
48
- const INIT_TEMPLATE_PICKER_INITIAL = 1;
49
- const initTemplatePickerChoices = (templates) => {
50
- if (templates.length === 0) throw new Error("No templates available to scaffold from.");
51
- return [{
52
- title: "Skip the template",
53
- value: SKIP_TEMPLATE_VALUE,
54
- description: "Set up agents and link a Neon project without copying template files."
55
- }, ...templates.map((template) => ({
56
- title: formatTemplateTitle(template),
57
- value: template.id,
58
- description: template.description ?? ""
59
- }))];
72
+ const pickInitAgentsInteractively = async (input) => {
73
+ requireInteractive();
74
+ if (input.available.length === 0) throw new Error("No coding agents are available to pick.");
75
+ const detected = new Set(input.detected);
76
+ const { agents } = await prompts({
77
+ onState: restoreCursorOnAbort,
78
+ type: "multiselect",
79
+ name: "agents",
80
+ message: AGENT_PICKER_MESSAGE,
81
+ instructions: false,
82
+ min: 1,
83
+ choices: input.available.map((id) => ({
84
+ value: id,
85
+ title: `${getAgentDisplayName(id)} (${id})`,
86
+ description: detected.has(id) ? AGENT_DETECTED_DESCRIPTION : AGENT_OTHER_DESCRIPTION,
87
+ selected: detected.has(id)
88
+ }))
89
+ });
90
+ if (!Array.isArray(agents)) return aborted();
91
+ return agents.filter((id) => typeof id === "string");
92
+ };
93
+ const pickInitSkillsInteractively = async () => {
94
+ requireInteractive();
95
+ const { skills } = await prompts({
96
+ onState: restoreCursorOnAbort,
97
+ type: "multiselect",
98
+ name: "skills",
99
+ message: SKILLS_PICKER_MESSAGE,
100
+ instructions: false,
101
+ min: 1,
102
+ choices: NEON_SKILL_CATALOG.map((entry) => ({
103
+ value: entry.skill,
104
+ title: entry.skill,
105
+ selected: entry.defaultSelected
106
+ }))
107
+ });
108
+ if (!Array.isArray(skills)) return aborted();
109
+ const allowed = new Set(listSkillIds());
110
+ return skills.filter((skill) => typeof skill === "string" && allowed.has(skill));
111
+ };
112
+ const pickInitMcpConfigLocationInteractively = async () => {
113
+ requireInteractive();
114
+ const { scope } = await prompts({
115
+ onState: restoreCursorOnAbort,
116
+ type: "select",
117
+ name: "scope",
118
+ message: MCP_SCOPE_MESSAGE,
119
+ initial: 0,
120
+ choices: [{
121
+ title: MCP_SCOPE_GLOBAL_TITLE,
122
+ value: "global",
123
+ description: MCP_SCOPE_GLOBAL_DESCRIPTION
124
+ }, {
125
+ title: MCP_SCOPE_PROJECT_TITLE,
126
+ value: "project",
127
+ description: MCP_SCOPE_PROJECT_DESCRIPTION
128
+ }]
129
+ });
130
+ if (scope !== "global" && scope !== "project") return aborted();
131
+ return scope;
132
+ };
133
+ const pickInitMcpAuthInteractively = async (input) => {
134
+ requireInteractive();
135
+ const { auth } = await prompts({
136
+ onState: restoreCursorOnAbort,
137
+ type: "select",
138
+ name: "auth",
139
+ message: MCP_AUTH_MESSAGE,
140
+ initial: input.authenticated ? 1 : 0,
141
+ choices: [{
142
+ title: MCP_AUTH_OAUTH_TITLE,
143
+ value: "oauth",
144
+ description: MCP_AUTH_OAUTH_DESCRIPTION
145
+ }, {
146
+ title: MCP_AUTH_API_KEY_TITLE,
147
+ value: "api-key",
148
+ description: MCP_AUTH_API_KEY_DESCRIPTION
149
+ }]
150
+ });
151
+ if (auth !== "oauth" && auth !== "api-key") return aborted();
152
+ return auth;
60
153
  };
61
- const pickInitTemplateInteractively = async (templates) => {
154
+ const pickInitProjectSetupInteractively = async () => {
62
155
  requireInteractive();
63
- const choices = initTemplatePickerChoices(templates);
64
- const { id } = await prompts({
156
+ const { setup } = await prompts({
65
157
  onState: restoreCursorOnAbort,
66
158
  type: "select",
67
- name: "id",
68
- message: "How would you like to set up this directory?",
69
- initial: 1,
70
- choices
159
+ name: "setup",
160
+ message: PROJECT_SETUP_MESSAGE,
161
+ initial: 0,
162
+ choices: [{
163
+ title: PROJECT_SETUP_LINK_TITLE,
164
+ value: "link",
165
+ description: PROJECT_SETUP_LINK_DESCRIPTION
166
+ }, {
167
+ title: PROJECT_SETUP_CLAIMABLE_TITLE,
168
+ value: "claimable",
169
+ description: PROJECT_SETUP_CLAIMABLE_DESCRIPTION
170
+ }]
71
171
  });
72
- if (id === "skip") return { kind: "skip" };
73
- if (typeof id !== "string" || id.length === 0) throw new Error("Aborted.");
74
- const selected = templates.find((template) => template.id === id);
75
- if (selected === void 0) throw new Error("Aborted.");
76
- return {
77
- kind: "template",
78
- template: selected
79
- };
172
+ if (setup !== "link" && setup !== "claimable") return aborted();
173
+ return setup;
80
174
  };
81
175
  const pickInitConfigInteractively = async () => {
82
176
  requireInteractive();
83
- process.stdout.write(`${chalk.dim("Choose services, then edit neon.ts and apply changes with neon config apply.")}\n`);
177
+ process.stdout.write(`${chalk.dim(CONFIG_CONFIRM_HINT)}\n`);
84
178
  const { value } = await prompts({
85
179
  onState: restoreCursorOnAbort,
86
180
  type: "confirm",
87
181
  name: "value",
88
- message: "Create neon.ts to manage this project's Neon setup as code?",
182
+ message: CONFIG_CONFIRM_MESSAGE,
89
183
  initial: true
90
184
  });
91
- if (value === void 0) throw new Error("Aborted.");
185
+ if (value === void 0) return aborted();
92
186
  return value === true;
93
187
  };
188
+ const pickInitServicesInteractively = async () => {
189
+ requireInteractive();
190
+ process.stdout.write(`${chalk.dim(SERVICES_HINT)}\n`);
191
+ const { services } = await prompts({
192
+ onState: restoreCursorOnAbort,
193
+ onRender() {
194
+ keepPostgresSelected(this);
195
+ },
196
+ type: "multiselect",
197
+ name: "services",
198
+ message: SERVICES_MESSAGE,
199
+ instructions: false,
200
+ cursor: 1,
201
+ choices: [
202
+ POSTGRES_SERVICE_CHOICE,
203
+ {
204
+ value: "auth",
205
+ title: "Managed Better Auth",
206
+ description: "Declare managed authentication for users and sessions."
207
+ },
208
+ {
209
+ value: "data-api",
210
+ title: "Data API",
211
+ description: "Declare an HTTP API for Postgres. Also declares Managed Better Auth."
212
+ },
213
+ {
214
+ value: "functions",
215
+ title: "Functions",
216
+ description: "Declare a hello function and add its source file."
217
+ },
218
+ {
219
+ value: "object-storage",
220
+ title: "Object Storage",
221
+ description: "Declare a private S3-compatible bucket named assets."
222
+ },
223
+ {
224
+ value: "ai-gateway",
225
+ title: "AI Gateway",
226
+ description: "Declare AI Gateway access. Requires an eligible Neon plan."
227
+ }
228
+ ]
229
+ });
230
+ if (!Array.isArray(services)) return aborted();
231
+ return CONFIG_INIT_SERVICES.filter((service) => services.includes(service));
232
+ };
233
+ const pickInitPackageManagerInteractively = async () => {
234
+ requireInteractive();
235
+ const available = installedPackageManagers();
236
+ if (available.length <= 1) return available[0];
237
+ const initial = Math.max(0, available.indexOf("npm"));
238
+ const { pm } = await prompts({
239
+ onState: restoreCursorOnAbort,
240
+ type: "select",
241
+ name: "pm",
242
+ message: PACKAGE_MANAGER_MESSAGE,
243
+ initial,
244
+ choices: available.map((id) => ({
245
+ title: id,
246
+ value: id
247
+ }))
248
+ });
249
+ if (pm !== "npm" && pm !== "pnpm" && pm !== "yarn" && pm !== "bun") return aborted();
250
+ return pm;
251
+ };
94
252
  const pickInitLinkInteractively = async () => {
95
253
  requireInteractive();
96
254
  const { value } = await prompts({
@@ -100,8 +258,9 @@ const pickInitLinkInteractively = async () => {
100
258
  message: "Link this project to a Neon project now?",
101
259
  initial: true
102
260
  });
103
- if (value === void 0) throw new Error("Aborted.");
261
+ if (value === void 0) return aborted();
104
262
  return value === true;
105
263
  };
264
+ const initAgentIds = () => listMcpAgentIds();
106
265
  //#endregion
107
- export { INIT_TEMPLATE_PICKER_INITIAL, initTemplatePickerChoices, pickAgentSetupInteractively, pickInitConfigInteractively, pickInitLinkInteractively, pickInitTemplateInteractively };
266
+ export { initAgentIds, pickAgentSetupInteractively, pickInitAgentsInteractively, pickInitConfigInteractively, pickInitLinkInteractively, pickInitMcpAuthInteractively, pickInitMcpConfigLocationInteractively, pickInitModeInteractively, pickInitPackageManagerInteractively, pickInitProjectSetupInteractively, pickInitServicesInteractively, pickInitSkillsInteractively };
@@ -1,4 +1,3 @@
1
- import { getAgentDisplayName } from "./agents.js";
2
1
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
3
2
  import prompts from "prompts";
4
3
  //#region src/mcp/wizard.ts
@@ -62,30 +61,5 @@ const pickMcpProjectPin = async (linkedProjectId, willMintKey = false) => {
62
61
  });
63
62
  return pin === true;
64
63
  };
65
- const mcpInstallSummary = (options) => {
66
- const agents = options.install.map(getAgentDisplayName).join(", ");
67
- const auth = options.auth === "oauth" ? "OAuth, agent signs in on first use" : options.reuse ? "reuse the API key already in agent config" : options.mintProjectId ? `mint an API key limited to ${options.mintProjectId}` : "mint an account-wide API key that reaches every organization";
68
- const rows = [
69
- ["Config", options.scope === "project" ? "this directory" : "user-level"],
70
- ["Agents", agents],
71
- ["Auth", auth],
72
- ["URL", options.url]
73
- ];
74
- if (options.skipped.length > 0) rows.push(["Skipped", options.skipped.map((row) => `${getAgentDisplayName(row.agent)} (${row.error})`).join("; ")]);
75
- const labelWidth = Math.max(...rows.map(([label]) => label.length));
76
- return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join("\n");
77
- };
78
- const confirmMcpInstall = async (options) => {
79
- if (!canPickAgentsInteractively()) return true;
80
- process.stdout.write(`\n${mcpInstallSummary(options)}\n\n`);
81
- const { ok } = await prompts({
82
- onState: restoreCursorOnAbort,
83
- type: "confirm",
84
- name: "ok",
85
- message: "Write the Neon MCP server into these agents?",
86
- initial: true
87
- });
88
- return ok === true;
89
- };
90
64
  //#endregion
91
- export { confirmMcpInstall, mcpInstallSummary, pickMcpAuth, pickMcpProjectPin, pickMcpScope };
65
+ export { pickMcpAuth, pickMcpProjectPin, pickMcpScope };
@@ -1,5 +1,3 @@
1
- import { getAgentDisplayName } from "../mcp/agents.js";
2
- import "../init/agents.js";
3
1
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
4
2
  import { NEON_SKILL_CATALOG } from "./catalog.js";
5
3
  import prompts from "prompts";
@@ -34,28 +32,6 @@ const isSkillEntry = (value) => {
34
32
  if (!("source" in value) || !("skill" in value)) return false;
35
33
  return typeof value.source === "string" && typeof value.skill === "string" && value.source.length > 0 && value.skill.length > 0;
36
34
  };
37
- const skillsInstallSummary = (options) => {
38
- const skills = options.invocations.flatMap((invocation) => invocation.skills).join(", ");
39
- const rows = [
40
- ["Config", options.scope === "project" ? "this directory" : "user-level"],
41
- ["Agents", options.agents.map(getAgentDisplayName).join(", ")],
42
- ["Skills", skills]
43
- ];
44
- const labelWidth = Math.max(...rows.map(([label]) => label.length));
45
- return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join("\n");
46
- };
47
- const confirmSkillsInstall = async (options) => {
48
- if (!canPickAgentsInteractively()) return true;
49
- process.stdout.write(`\n${skillsInstallSummary(options)}\n\n`);
50
- const { ok } = await prompts({
51
- onState: restoreCursorOnAbort,
52
- type: "confirm",
53
- name: "ok",
54
- message: "Write Neon agent skills into these agents?",
55
- initial: true
56
- });
57
- return ok === true;
58
- };
59
35
  const confirmSkillsUpdate = async (options) => {
60
36
  if (!canPickAgentsInteractively()) return true;
61
37
  const config = options.scope === "project" ? "this directory" : "user-level";
@@ -70,4 +46,4 @@ const confirmSkillsUpdate = async (options) => {
70
46
  return ok === true;
71
47
  };
72
48
  //#endregion
73
- export { confirmSkillsInstall, confirmSkillsUpdate, pickSkillsInteractively, skillsInstallSummary };
49
+ export { confirmSkillsUpdate, pickSkillsInteractively };
@@ -0,0 +1,26 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { homedir } from "node:os";
4
+ //#region src/test_utils/npm_env.ts
5
+ const isNpmEnvKey = (key) => {
6
+ const lower = key.toLowerCase();
7
+ return lower.startsWith("npm_config_") || lower === "node_auth_token" || lower === "npm_token";
8
+ };
9
+ /**
10
+ * Keep registry auth when a test replaces HOME. Isolated agent detection
11
+ * otherwise drops ~/.npmrc and `npx`/skills fail with E401 or hang.
12
+ */
13
+ const npmEnvForIsolatedHome = () => {
14
+ const env = {};
15
+ for (const [key, value] of Object.entries(process.env)) {
16
+ if (value === void 0 || !isNpmEnvKey(key)) continue;
17
+ env[key] = value;
18
+ }
19
+ if (env.npm_config_userconfig === void 0 && env.NPM_CONFIG_USERCONFIG === void 0) {
20
+ const src = join(homedir(), ".npmrc");
21
+ if (existsSync(src)) env.npm_config_userconfig = src;
22
+ }
23
+ return env;
24
+ };
25
+ //#endregion
26
+ export { npmEnvForIsolatedHome };
@@ -0,0 +1,47 @@
1
+ import { recordLatestVersion } from "./update_notifier.js";
2
+ import { promisify } from "node:util";
3
+ import { pathToFileURL } from "node:url";
4
+ import { execFile } from "node:child_process";
5
+ //#region src/update_check_worker.ts
6
+ const execFileAsync = promisify(execFile);
7
+ const LOOKUP_TIMEOUT_MS = 5e3;
8
+ const npmViewInvocation = (platform) => ({
9
+ args: [
10
+ "view",
11
+ "neon",
12
+ "version",
13
+ "--json"
14
+ ],
15
+ command: platform === "win32" ? "npm.cmd" : "npm",
16
+ shell: platform === "win32"
17
+ });
18
+ const npmLatestVersion = async () => {
19
+ const invocation = npmViewInvocation(process.platform);
20
+ const { stdout } = await execFileAsync(invocation.command, invocation.args, {
21
+ encoding: "utf8",
22
+ shell: invocation.shell,
23
+ timeout: LOOKUP_TIMEOUT_MS,
24
+ windowsHide: true
25
+ });
26
+ const value = JSON.parse(stdout);
27
+ return typeof value === "string" ? value : void 0;
28
+ };
29
+ const homebrewLatestVersion = async () => {
30
+ const response = await fetch("https://formulae.brew.sh/api/formula/neonctl.json", { signal: AbortSignal.timeout(LOOKUP_TIMEOUT_MS) });
31
+ if (!response.ok) return void 0;
32
+ const value = await response.json();
33
+ if (typeof value !== "object" || value === null || !("versions" in value) || typeof value.versions !== "object" || value.versions === null || !("stable" in value.versions) || typeof value.versions.stable !== "string") return;
34
+ return value.versions.stable;
35
+ };
36
+ const run = async () => {
37
+ const [cachePath, source] = process.argv.slice(2);
38
+ if (cachePath === void 0 || source !== "homebrew" && source !== "npm") return;
39
+ try {
40
+ const latestVersion = source === "homebrew" ? await homebrewLatestVersion() : await npmLatestVersion();
41
+ if (latestVersion !== void 0) recordLatestVersion(cachePath, source, latestVersion);
42
+ } catch {}
43
+ };
44
+ const entry = process.argv[1];
45
+ if (entry !== void 0 && import.meta.url === pathToFileURL(entry).href) run();
46
+ //#endregion
47
+ export { npmViewInvocation };
@@ -0,0 +1,145 @@
1
+ import { isCi } from "./env.js";
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";
6
+ import { fileURLToPath } from "node:url";
7
+ import { spawn } from "node:child_process";
8
+ import semver from "semver";
9
+ //#region src/update_notifier.ts
10
+ const CHECK_INTERVAL_MS = 864e5;
11
+ const NOTIFY_INTERVAL_MS = 3 * CHECK_INTERVAL_MS;
12
+ const CACHE_FILE = "update-check.json";
13
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
14
+ const parseUpdateCheckCache = (raw) => {
15
+ try {
16
+ 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;
18
+ return {
19
+ checkedAt: value.checkedAt,
20
+ source: value.source,
21
+ ...value.latestVersion === void 0 ? {} : { latestVersion: value.latestVersion },
22
+ ...value.notifiedAt === void 0 ? {} : { notifiedAt: value.notifiedAt }
23
+ };
24
+ } catch {
25
+ return;
26
+ }
27
+ };
28
+ const readUpdateCheckCache = (cachePath) => {
29
+ try {
30
+ return parseUpdateCheckCache(readFileSync(cachePath, "utf8"));
31
+ } catch {
32
+ return;
33
+ }
34
+ };
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
+ };
49
+ const formatUpdateNotice = ({ currentVersion, latestVersion, upgradeCommand }) => {
50
+ if (semver.valid(currentVersion) === null || semver.valid(latestVersion) === null || !semver.gt(latestVersion, currentVersion)) return;
51
+ return `Neon CLI update available: ${currentVersion} → ${latestVersion}\n${upgradeCommand}`;
52
+ };
53
+ const shouldRefreshUpdateCheck = (cache, now) => cache === void 0 || now - cache.checkedAt >= CHECK_INTERVAL_MS;
54
+ const cacheForUpdateSource = (cache, source) => cache?.source === source ? cache : void 0;
55
+ const isCurrentBranchProbe = ({ commandPath, currentBranch }) => currentBranch && (commandPath[0] === "status" || commandPath[0] === "config" && commandPath[1] === "status");
56
+ const updateSource = (command) => command.startsWith("brew ") ? "homebrew" : "npm";
57
+ const spawnUpdateWorkerProcess = ({ cachePath, executablePath, source, workerPath }) => {
58
+ const child = spawn(executablePath, [
59
+ workerPath,
60
+ cachePath,
61
+ source
62
+ ], {
63
+ detached: true,
64
+ stdio: "ignore",
65
+ windowsHide: true
66
+ });
67
+ child.on("error", (error) => {
68
+ log.debug("CLI update check failed to start: %s", error.message);
69
+ });
70
+ child.unref();
71
+ return child;
72
+ };
73
+ const spawnUpdateWorker = (cachePath, source) => {
74
+ const workerPath = fileURLToPath(new URL("./update_check_worker.js", import.meta.url));
75
+ if (!existsSync(workerPath)) return;
76
+ try {
77
+ spawnUpdateWorkerProcess({
78
+ cachePath,
79
+ executablePath: process.execPath,
80
+ source,
81
+ workerPath
82
+ });
83
+ } catch (error) {
84
+ log.debug("Could not start the CLI update check: %s", error instanceof Error ? error.message : String(error));
85
+ }
86
+ };
87
+ const isPackagedExecutable = () => "pkg" in process;
88
+ 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
+ commandPath,
90
+ currentBranch
91
+ });
92
+ const notifyIfUpdateAvailable = ({ commandPath, configDir, currentBranch, currentVersion, output }) => {
93
+ if (!shouldRunUpdateNotifier({
94
+ commandPath,
95
+ currentBranch,
96
+ disabled: "NO_UPDATE_NOTIFIER" in process.env,
97
+ isCi: isCi(),
98
+ isPackaged: isPackagedExecutable(),
99
+ output,
100
+ stderrIsTty: process.stderr.isTTY === true,
101
+ stdoutIsTty: process.stdout.isTTY === true
102
+ })) return;
103
+ const command = recommendedCliUpgradeCommand(fileURLToPath(import.meta.url));
104
+ if (command === void 0) return;
105
+ const cachePath = join(configDir, CACHE_FILE);
106
+ const now = Date.now();
107
+ const source = updateSource(command);
108
+ 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,
130
+ checkedAt: now,
131
+ source
132
+ };
133
+ if (writeUpdateCheckCache(cachePath, nextCache)) spawnUpdateWorker(cachePath, source);
134
+ };
135
+ const recordLatestVersion = (cachePath, source, latestVersion) => {
136
+ if (semver.valid(latestVersion) === null) return;
137
+ const cache = cacheForUpdateSource(readUpdateCheckCache(cachePath), source);
138
+ if (cache === void 0) return;
139
+ writeUpdateCheckCache(cachePath, {
140
+ ...cache,
141
+ latestVersion
142
+ });
143
+ };
144
+ //#endregion
145
+ export { cacheForUpdateSource, formatUpdateNotice, notifyIfUpdateAvailable, parseUpdateCheckCache, recordLatestVersion, shouldRefreshUpdateCheck, shouldRunUpdateNotifier, spawnUpdateWorkerProcess, writeUpdateCheckCache };
@@ -159,6 +159,13 @@ 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
+ 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";
168
+ };
162
169
  /**
163
170
  * The argv for an install with `pm`: every dependency in the manifest when
164
171
  * `packages` is empty, otherwise those packages added to it. npm spells the add
@@ -326,4 +333,4 @@ const runCommand = (cmd, args, cwd, env) => new Promise((resolvePromise) => {
326
333
  });
327
334
  });
328
335
  //#endregion
329
- export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, commandEnv, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolveInvokingPackageManager, resolvePackageManager, runCommand };
336
+ export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, commandEnv, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, recommendedCliUpgradeCommand, resolveInvokingPackageManager, resolvePackageManager, runCommand };