neon 5.0.1 → 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.
@@ -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 };
@@ -2,9 +2,30 @@ import { CONFIG_INIT_SERVICES } from "../config_template.js";
2
2
  import prompts from "prompts";
3
3
  //#region src/utils/service_picker.ts
4
4
  /**
5
- * The picker's rows, in {@link CONFIG_INIT_SERVICES} order. Titles use the product names from
6
- * the CLI's README ("Managed Better Auth", "Object Storage") rather than the `neon.ts` field
7
- * names, since this is the list a user reads before they've seen a policy.
5
+ * Re-select Postgres after each render. `prompts` lets space unselect a selected
6
+ * row even when `disabled` is set, and `disabled` also strikethroughs the title.
7
+ */
8
+ const keepPostgresSelected = (prompt) => {
9
+ if (typeof prompt !== "object" || prompt === null || !("value" in prompt)) return;
10
+ const rows = prompt.value;
11
+ if (!Array.isArray(rows)) return;
12
+ for (const row of rows) {
13
+ if (!isChoiceRow(row) || row.value !== "postgres") continue;
14
+ row.selected = true;
15
+ }
16
+ };
17
+ const isChoiceRow = (row) => typeof row === "object" && row !== null && "value" in row && "selected" in row;
18
+ const POSTGRES_SERVICE_CHOICE = {
19
+ value: "postgres",
20
+ title: "Postgres (always included)",
21
+ description: "Every Neon project includes a Postgres database.",
22
+ selected: true
23
+ };
24
+ /**
25
+ * The picker's rows, in {@link CONFIG_INIT_SERVICES} order after the locked Postgres
26
+ * row. Titles use the product names from the CLI's README ("Managed Better Auth",
27
+ * "Object Storage") rather than the `neon.ts` field names, since this is the list a
28
+ * user reads before they've seen a policy.
8
29
  */
9
30
  const CHOICES = [
10
31
  {
@@ -41,7 +62,7 @@ const CHOICES = [
41
62
  * Callers guard the TTY themselves (see `initCmd`); this function assumes it may prompt.
42
63
  */
43
64
  const pickServicesInteractively = async () => {
44
- const { services } = await prompts({
65
+ const question = {
45
66
  onState: (state) => {
46
67
  if (state.aborted) {
47
68
  process.stdout.write("\x1B[?25h");
@@ -49,18 +70,23 @@ const pickServicesInteractively = async () => {
49
70
  process.exit(1);
50
71
  }
51
72
  },
73
+ onRender() {
74
+ keepPostgresSelected(this);
75
+ },
52
76
  type: "multiselect",
53
77
  name: "services",
54
78
  message: "Which Neon services should neon.ts declare? (space to toggle, enter to confirm)",
55
79
  instructions: false,
56
- choices: CHOICES.map((choice) => ({
80
+ cursor: 1,
81
+ choices: [POSTGRES_SERVICE_CHOICE, ...CHOICES.map((choice) => ({
57
82
  value: choice.value,
58
83
  title: choice.title,
59
84
  description: choice.description
60
- }))
61
- });
85
+ }))]
86
+ };
87
+ const { services } = await prompts(question);
62
88
  if (!Array.isArray(services)) throw new Error("Aborted: no services selected.");
63
89
  return CONFIG_INIT_SERVICES.filter((service) => services.includes(service));
64
90
  };
65
91
  //#endregion
66
- export { pickServicesInteractively };
92
+ export { POSTGRES_SERVICE_CHOICE, keepPostgresSelected, pickServicesInteractively };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "5.0.1",
3
+ "version": "6.1.0",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -58,14 +58,15 @@
58
58
  "pg-protocol": "^1.14.0",
59
59
  "picocolors": "^1.1.1",
60
60
  "prompts": "2.4.2",
61
+ "semver": "7.7.2",
61
62
  "undici": "^7.28.0",
62
63
  "which": "3.0.1",
63
64
  "yaml": "^2.9.0",
64
65
  "yargs": "17.7.2",
65
66
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.7.2",
67
- "@neon/sdk": "6.0.0",
68
- "@neon/config-runtime": "1.6.2"
67
+ "@neon/config-runtime": "1.6.3",
68
+ "@neon/config": "1.7.3",
69
+ "@neon/sdk": "6.1.0"
69
70
  },
70
71
  "optionalDependencies": {
71
72
  "@napi-rs/keyring": "1.3.0",
@@ -82,6 +83,7 @@
82
83
  "@types/express": "4.17.21",
83
84
  "@types/node": "^20.19.0",
84
85
  "@types/prompts": "2.4.9",
86
+ "@types/semver": "7.7.1",
85
87
  "@types/which": "3.0.4",
86
88
  "@types/yargs": "^17.0.35",
87
89
  "@vitest/coverage-v8": "^3.0.9",
@@ -98,7 +100,7 @@
98
100
  "typescript": "^5.9.0",
99
101
  "vitest": "^3.0.9",
100
102
  "@neon-internals/cli-core": "0.0.0",
101
- "@neon-internals/env-core": "0.0.21",
103
+ "@neon-internals/env-core": "0.0.22",
102
104
  "@neon/e2e-harness": "0.0.0",
103
105
  "@neon/functions": "0.11.0"
104
106
  },
@@ -1,37 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- //#region src/init/child.ts
3
- const AUTH_CHILD = /* @__PURE__ */ new Set([
4
- "bootstrap",
5
- "link",
6
- "mcp",
7
- "env"
8
- ]);
9
- const initChildEnv = (command, overlay, base = process.env) => {
10
- const child = { ...base };
11
- if (command === void 0 || !AUTH_CHILD.has(command)) {
12
- for (const key of Object.keys(child)) if (key.toUpperCase() === "NEON_API_KEY") delete child[key];
13
- }
14
- return overlay ? {
15
- ...child,
16
- ...overlay
17
- } : child;
18
- };
19
- const spawnCliChild = async (argv, cwd, overlay) => {
20
- const cli = process.argv[1];
21
- if (!cli) throw new Error("Cannot re-exec the Neon CLI: process.argv[1] is missing.");
22
- return new Promise((resolve) => {
23
- const child = spawn(process.execPath, [cli, ...argv], {
24
- cwd,
25
- stdio: "inherit",
26
- env: initChildEnv(argv[0], overlay)
27
- });
28
- child.on("error", () => {
29
- resolve(false);
30
- });
31
- child.on("close", (code) => {
32
- resolve(code === 0);
33
- });
34
- });
35
- };
36
- //#endregion
37
- export { AUTH_CHILD, initChildEnv, spawnCliChild };
@@ -1,36 +0,0 @@
1
- import { getAgentDisplayName } from "../mcp/agents.js";
2
- import "../init/agents.js";
3
- import { canPickAgentsInteractively } from "../utils/agent_picker.js";
4
- import { NEON_PLUGIN_NAME } from "./run.js";
5
- import prompts from "prompts";
6
- //#region src/plugins/wizard.ts
7
- const restoreCursorOnAbort = (state) => {
8
- if (state.aborted) {
9
- process.stdout.write("\x1B[?25h");
10
- process.stdout.write("\n");
11
- process.exit(1);
12
- }
13
- };
14
- const pluginsInstallSummary = (options) => {
15
- const rows = [
16
- ["Scope", options.scope === "project" ? "project" : "user"],
17
- ["Agents", options.agents.map(getAgentDisplayName).join(", ")],
18
- ["Plugin", NEON_PLUGIN_NAME]
19
- ];
20
- const labelWidth = Math.max(...rows.map(([label]) => label.length));
21
- return rows.map(([label, value]) => `${label.padEnd(labelWidth)} ${value}`).join("\n");
22
- };
23
- const confirmPluginsInstall = async (options) => {
24
- if (!canPickAgentsInteractively()) return true;
25
- process.stdout.write(`\n${pluginsInstallSummary(options)}\n\n`);
26
- const { ok } = await prompts({
27
- onState: restoreCursorOnAbort,
28
- type: "confirm",
29
- name: "ok",
30
- message: "Install the Neon plugin into these agents?",
31
- initial: true
32
- });
33
- return ok === true;
34
- };
35
- //#endregion
36
- export { confirmPluginsInstall, pluginsInstallSummary };