neon 3.0.0 → 3.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.
Files changed (198) hide show
  1. package/README.md +54 -0
  2. package/dist/_shared/auth_selection.js +76 -79
  3. package/dist/_shared/credentials.js +166 -187
  4. package/dist/_shared/env-core/env.js +354 -517
  5. package/dist/_shared/env-core/reuse-secrets.js +159 -203
  6. package/dist/_shared/paths.js +129 -126
  7. package/dist/_shared/profiles.js +192 -242
  8. package/dist/_shared/secure_file.js +36 -38
  9. package/dist/_virtual/_rolldown/runtime.js +13 -0
  10. package/dist/analytics.js +163 -207
  11. package/dist/api.js +815 -758
  12. package/dist/auth.js +121 -141
  13. package/dist/auth_context.js +39 -53
  14. package/dist/cli.js +4 -7
  15. package/dist/commands/api.js +220 -250
  16. package/dist/commands/api_keys.js +251 -314
  17. package/dist/commands/auth.js +283 -328
  18. package/dist/commands/bootstrap.js +372 -437
  19. package/dist/commands/branches.js +304 -455
  20. package/dist/commands/bucket.js +374 -514
  21. package/dist/commands/checkout.js +213 -298
  22. package/dist/commands/config.js +573 -690
  23. package/dist/commands/connection_string.js +137 -165
  24. package/dist/commands/data_api.js +238 -260
  25. package/dist/commands/databases.js +67 -76
  26. package/dist/commands/deploy.js +31 -25
  27. package/dist/commands/dev.js +639 -719
  28. package/dist/commands/diff.js +156 -200
  29. package/dist/commands/env.js +243 -303
  30. package/dist/commands/functions.js +275 -355
  31. package/dist/commands/index.js +70 -65
  32. package/dist/commands/init.js +84 -119
  33. package/dist/commands/inspect.js +55 -55
  34. package/dist/commands/ip_allow.js +88 -120
  35. package/dist/commands/link.js +874 -1019
  36. package/dist/commands/logs.js +291 -0
  37. package/dist/commands/neon_auth.js +725 -933
  38. package/dist/commands/operations.js +34 -25
  39. package/dist/commands/orgs.js +28 -18
  40. package/dist/commands/profile.js +614 -845
  41. package/dist/commands/projects.js +313 -373
  42. package/dist/commands/psql.js +60 -58
  43. package/dist/commands/roles.js +55 -58
  44. package/dist/commands/schema_diff.js +87 -131
  45. package/dist/commands/set_context.js +34 -26
  46. package/dist/commands/snapshots.js +288 -413
  47. package/dist/commands/status.js +41 -37
  48. package/dist/commands/user.js +21 -10
  49. package/dist/commands/vpc_endpoints.js +85 -113
  50. package/dist/config.js +7 -6
  51. package/dist/config_format.js +50 -66
  52. package/dist/config_template.js +128 -157
  53. package/dist/context.js +183 -235
  54. package/dist/current_branch_fast_path.js +40 -49
  55. package/dist/dev/env.js +313 -394
  56. package/dist/dev/functions.js +54 -68
  57. package/dist/dev/inputs.js +46 -58
  58. package/dist/dev/runtime.js +135 -164
  59. package/dist/dev/websocket.js +766 -959
  60. package/dist/env.js +27 -33
  61. package/dist/env_file.js +118 -132
  62. package/dist/env_services.js +36 -38
  63. package/dist/errors.js +57 -68
  64. package/dist/functions_api.js +45 -43
  65. package/dist/help.js +189 -140
  66. package/dist/index.js +182 -257
  67. package/dist/init/agents.js +137 -118
  68. package/dist/init/auth.js +58 -68
  69. package/dist/init/bootstrap.js +325 -396
  70. package/dist/init/build_config.js +4 -2
  71. package/dist/init/detect_agent.js +56 -101
  72. package/dist/init/editors.js +35 -52
  73. package/dist/init/enrich_output.js +51 -66
  74. package/dist/init/extension.js +134 -171
  75. package/dist/init/inspect.js +179 -266
  76. package/dist/init/interactive.js +510 -622
  77. package/dist/init/neonctl.js +117 -168
  78. package/dist/init/orchestrate.js +157 -173
  79. package/dist/init/phases/auth.js +188 -202
  80. package/dist/init/phases/cleanup.js +23 -23
  81. package/dist/init/phases/db.js +251 -277
  82. package/dist/init/phases/getting_started.js +213 -223
  83. package/dist/init/phases/mcp.js +174 -224
  84. package/dist/init/phases/migrations.js +247 -248
  85. package/dist/init/phases/neon_auth.js +114 -133
  86. package/dist/init/phases/setup.js +546 -703
  87. package/dist/init/phases/skills.js +75 -86
  88. package/dist/init/phases/status.js +72 -67
  89. package/dist/init/resolve_context.js +102 -99
  90. package/dist/init/route_command.js +91 -98
  91. package/dist/init/skills.js +174 -218
  92. package/dist/init/vsix.js +77 -99
  93. package/dist/log.js +17 -16
  94. package/dist/neon_services.js +104 -129
  95. package/dist/parameters.gen.js +481 -471
  96. package/dist/pkg.js +17 -19
  97. package/dist/profile_keys.js +44 -47
  98. package/dist/psql/cli.js +44 -47
  99. package/dist/psql/command/cmd_cond.js +231 -406
  100. package/dist/psql/command/cmd_connect.js +557 -764
  101. package/dist/psql/command/cmd_copy.js +727 -983
  102. package/dist/psql/command/cmd_describe.js +1499 -1688
  103. package/dist/psql/command/cmd_format.js +733 -905
  104. package/dist/psql/command/cmd_io.js +1293 -2082
  105. package/dist/psql/command/cmd_lo.js +297 -359
  106. package/dist/psql/command/cmd_meta.js +727 -878
  107. package/dist/psql/command/cmd_misc.js +138 -172
  108. package/dist/psql/command/cmd_pipeline.js +547 -1099
  109. package/dist/psql/command/cmd_restrict.js +119 -155
  110. package/dist/psql/command/cmd_show.js +529 -688
  111. package/dist/psql/command/dispatch.js +261 -325
  112. package/dist/psql/command/inputQueue.js +35 -33
  113. package/dist/psql/command/shared.js +49 -63
  114. package/dist/psql/complete/filenames.js +90 -133
  115. package/dist/psql/complete/index.js +59 -97
  116. package/dist/psql/complete/matcher.js +236 -300
  117. package/dist/psql/complete/psqlVars.js +218 -223
  118. package/dist/psql/complete/queries.js +159 -177
  119. package/dist/psql/complete/rules.js +1493 -2299
  120. package/dist/psql/core/common.js +762 -1180
  121. package/dist/psql/core/help.js +456 -546
  122. package/dist/psql/core/mainloop.js +692 -1302
  123. package/dist/psql/core/prompt.js +391 -408
  124. package/dist/psql/core/settings.js +429 -644
  125. package/dist/psql/core/sqlHelp.js +480 -554
  126. package/dist/psql/core/startup.js +626 -815
  127. package/dist/psql/core/syncVars.js +67 -110
  128. package/dist/psql/core/variables.js +156 -278
  129. package/dist/psql/describe/formatters.js +884 -1285
  130. package/dist/psql/describe/processNamePattern.js +173 -260
  131. package/dist/psql/describe/queries.js +1368 -2403
  132. package/dist/psql/describe/versionGate.js +32 -41
  133. package/dist/psql/index.js +1414 -1927
  134. package/dist/psql/io/history.js +232 -271
  135. package/dist/psql/io/input.js +103 -108
  136. package/dist/psql/io/lineEditor/buffer.js +238 -319
  137. package/dist/psql/io/lineEditor/complete.js +135 -213
  138. package/dist/psql/io/lineEditor/filename.js +139 -148
  139. package/dist/psql/io/lineEditor/index.js +653 -870
  140. package/dist/psql/io/lineEditor/keymap.js +544 -702
  141. package/dist/psql/io/lineEditor/vt100.js +294 -341
  142. package/dist/psql/io/pgpass.js +158 -187
  143. package/dist/psql/io/pgservice.js +146 -183
  144. package/dist/psql/io/psqlrc.js +328 -403
  145. package/dist/psql/print/aligned.js +1020 -1683
  146. package/dist/psql/print/asciidoc.js +180 -214
  147. package/dist/psql/print/crosstab.js +281 -442
  148. package/dist/psql/print/csv.js +48 -70
  149. package/dist/psql/print/html.js +195 -226
  150. package/dist/psql/print/json.js +75 -88
  151. package/dist/psql/print/latex.js +291 -364
  152. package/dist/psql/print/pager.js +171 -242
  153. package/dist/psql/print/troff.js +194 -226
  154. package/dist/psql/print/unaligned.js +69 -95
  155. package/dist/psql/print/units.js +167 -169
  156. package/dist/psql/scanner/slash.js +428 -483
  157. package/dist/psql/scanner/sql.js +445 -889
  158. package/dist/psql/scanner/stringutils.js +309 -379
  159. package/dist/psql/types/index.js +2 -7
  160. package/dist/psql/types/scanner.js +25 -22
  161. package/dist/psql/wire/connection.js +2042 -2803
  162. package/dist/psql/wire/copy.js +84 -100
  163. package/dist/psql/wire/notify.js +39 -59
  164. package/dist/psql/wire/pipeline.js +305 -518
  165. package/dist/psql/wire/protocol.js +349 -417
  166. package/dist/psql/wire/sasl.js +180 -265
  167. package/dist/psql/wire/tls.js +400 -561
  168. package/dist/storage_api.js +115 -129
  169. package/dist/test_utils/fixtures.js +94 -113
  170. package/dist/test_utils/oauth_server.js +10 -7
  171. package/dist/test_utils/project_dir.js +33 -0
  172. package/dist/utils/ai_gateway_notice.js +131 -162
  173. package/dist/utils/api_enums.js +21 -28
  174. package/dist/utils/auth.js +10 -4
  175. package/dist/utils/branch_notice.js +20 -19
  176. package/dist/utils/branch_picker.js +83 -89
  177. package/dist/utils/cli_name.js +15 -12
  178. package/dist/utils/compute_units.js +20 -27
  179. package/dist/utils/config_diff.js +127 -158
  180. package/dist/utils/enrichers.js +95 -148
  181. package/dist/utils/esbuild.js +130 -189
  182. package/dist/utils/flags.js +35 -47
  183. package/dist/utils/formats.js +8 -15
  184. package/dist/utils/git_diff.js +69 -80
  185. package/dist/utils/inspect_db.js +101 -143
  186. package/dist/utils/inspect_queries.js +179 -142
  187. package/dist/utils/middlewares.js +37 -44
  188. package/dist/utils/openapi.js +87 -99
  189. package/dist/utils/package_manager.js +312 -110
  190. package/dist/utils/point_in_time.js +49 -53
  191. package/dist/utils/psql.js +89 -106
  192. package/dist/utils/service_picker.js +55 -58
  193. package/dist/utils/string.js +5 -5
  194. package/dist/utils/ui.js +38 -55
  195. package/dist/utils/write_sync.js +26 -35
  196. package/dist/utils/zip.js +4 -3
  197. package/dist/writer.js +67 -87
  198. package/package.json +7 -5
@@ -1,114 +1,102 @@
1
- // Lightweight loader for the Neon OpenAPI spec, used by `neon api --list` to
2
- // enumerate the available routes. The `neon api <path>` request path does NOT
3
- // depend on this module — it is a pure passthrough — so a stale or unreachable
4
- // spec never blocks a real API call. Listing degrades gracefully: fresh cache →
5
- // live fetch → stale cache → clear error.
1
+ import { log } from "../log.js";
6
2
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
3
  import { dirname, join } from "node:path";
8
- import { log } from "../log.js";
4
+ //#region src/utils/openapi.ts
9
5
  /** Public URL of the released Neon OpenAPI v2 spec. */
10
- export const DEFAULT_SPEC_URL = "https://neon.com/api_spec/release/v2.json";
6
+ const DEFAULT_SPEC_URL = "https://neon.com/api_spec/release/v2.json";
11
7
  const CACHE_FILE = "openapi-spec.json";
12
- const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
13
- const FETCH_TIMEOUT_MS = 10000;
14
- const HTTP_METHODS = new Set([
15
- "get",
16
- "post",
17
- "put",
18
- "patch",
19
- "delete",
20
- "head",
21
- "options",
8
+ const CACHE_TTL_MS = 864e5;
9
+ const FETCH_TIMEOUT_MS = 1e4;
10
+ const HTTP_METHODS = /* @__PURE__ */ new Set([
11
+ "get",
12
+ "post",
13
+ "put",
14
+ "patch",
15
+ "delete",
16
+ "head",
17
+ "options"
22
18
  ]);
23
19
  async function fetchSpec(url) {
24
- const controller = new AbortController();
25
- const timer = setTimeout(() => {
26
- controller.abort();
27
- }, FETCH_TIMEOUT_MS);
28
- try {
29
- const res = await fetch(url, {
30
- signal: controller.signal,
31
- headers: { Accept: "application/json" },
32
- });
33
- if (!res.ok) {
34
- throw new Error(`Failed to fetch OpenAPI spec (${res.status} ${res.statusText})`);
35
- }
36
- return (await res.json());
37
- }
38
- finally {
39
- clearTimeout(timer);
40
- }
20
+ const controller = new AbortController();
21
+ const timer = setTimeout(() => {
22
+ controller.abort();
23
+ }, FETCH_TIMEOUT_MS);
24
+ try {
25
+ const res = await fetch(url, {
26
+ signal: controller.signal,
27
+ headers: { Accept: "application/json" }
28
+ });
29
+ if (!res.ok) throw new Error(`Failed to fetch OpenAPI spec (${res.status} ${res.statusText})`);
30
+ return await res.json();
31
+ } finally {
32
+ clearTimeout(timer);
33
+ }
41
34
  }
42
35
  function readCache(cachePath) {
43
- try {
44
- return JSON.parse(readFileSync(cachePath, "utf8"));
45
- }
46
- catch {
47
- return null;
48
- }
36
+ try {
37
+ return JSON.parse(readFileSync(cachePath, "utf8"));
38
+ } catch {
39
+ return null;
40
+ }
49
41
  }
50
42
  function writeCache(cachePath, data) {
51
- try {
52
- mkdirSync(dirname(cachePath), { recursive: true });
53
- writeFileSync(cachePath, JSON.stringify(data));
54
- }
55
- catch (err) {
56
- log.debug("Failed to cache OpenAPI spec: %s", err);
57
- }
43
+ try {
44
+ mkdirSync(dirname(cachePath), { recursive: true });
45
+ writeFileSync(cachePath, JSON.stringify(data));
46
+ } catch (err) {
47
+ log.debug("Failed to cache OpenAPI spec: %s", err);
48
+ }
58
49
  }
59
50
  /**
60
- * Load the Neon OpenAPI spec, preferring a fresh on-disk cache, then a live
61
- * fetch (which refreshes the cache), then a stale cache as a last resort.
62
- * Returns `null` when no spec can be obtained.
63
- */
64
- export async function loadSpec(opts) {
65
- const { configDir, specUrl, refresh } = opts;
66
- const cachePath = join(configDir, CACHE_FILE);
67
- if (!refresh) {
68
- const cached = readCache(cachePath);
69
- if (cached &&
70
- cached.specUrl === specUrl &&
71
- Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
72
- log.debug("Using cached OpenAPI spec from %s", cachePath);
73
- return cached.spec;
74
- }
75
- }
76
- try {
77
- log.debug("Fetching OpenAPI spec from %s", specUrl);
78
- const spec = await fetchSpec(specUrl);
79
- writeCache(cachePath, { fetchedAt: Date.now(), specUrl, spec });
80
- return spec;
81
- }
82
- catch (err) {
83
- log.debug("Failed to fetch OpenAPI spec: %s", err);
84
- const stale = readCache(cachePath);
85
- if (stale && stale.specUrl === specUrl) {
86
- log.debug("Falling back to stale cached OpenAPI spec");
87
- return stale.spec;
88
- }
89
- return null;
90
- }
51
+ * Load the Neon OpenAPI spec, preferring a fresh on-disk cache, then a live
52
+ * fetch (which refreshes the cache), then a stale cache as a last resort.
53
+ * Returns `null` when no spec can be obtained.
54
+ */
55
+ async function loadSpec(opts) {
56
+ const { configDir, specUrl, refresh } = opts;
57
+ const cachePath = join(configDir, CACHE_FILE);
58
+ if (!refresh) {
59
+ const cached = readCache(cachePath);
60
+ if (cached && cached.specUrl === specUrl && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
61
+ log.debug("Using cached OpenAPI spec from %s", cachePath);
62
+ return cached.spec;
63
+ }
64
+ }
65
+ try {
66
+ log.debug("Fetching OpenAPI spec from %s", specUrl);
67
+ const spec = await fetchSpec(specUrl);
68
+ writeCache(cachePath, {
69
+ fetchedAt: Date.now(),
70
+ specUrl,
71
+ spec
72
+ });
73
+ return spec;
74
+ } catch (err) {
75
+ log.debug("Failed to fetch OpenAPI spec: %s", err);
76
+ const stale = readCache(cachePath);
77
+ if (stale && stale.specUrl === specUrl) {
78
+ log.debug("Falling back to stale cached OpenAPI spec");
79
+ return stale.spec;
80
+ }
81
+ return null;
82
+ }
91
83
  }
92
84
  /** Flatten a spec into a sorted list of routes (by path, then method). */
93
- export function getEndpoints(spec) {
94
- const endpoints = [];
95
- for (const [path, item] of Object.entries(spec.paths ?? {})) {
96
- for (const [method, op] of Object.entries(item)) {
97
- if (!HTTP_METHODS.has(method.toLowerCase())) {
98
- continue;
99
- }
100
- const operation = (op ?? {});
101
- endpoints.push({
102
- method: method.toUpperCase(),
103
- path,
104
- summary: operation.summary,
105
- operationId: operation.operationId,
106
- tags: operation.tags ?? [],
107
- });
108
- }
109
- }
110
- endpoints.sort((a, b) => a.path === b.path
111
- ? a.method.localeCompare(b.method)
112
- : a.path.localeCompare(b.path));
113
- return endpoints;
85
+ function getEndpoints(spec) {
86
+ const endpoints = [];
87
+ for (const [path, item] of Object.entries(spec.paths ?? {})) for (const [method, op] of Object.entries(item)) {
88
+ if (!HTTP_METHODS.has(method.toLowerCase())) continue;
89
+ const operation = op ?? {};
90
+ endpoints.push({
91
+ method: method.toUpperCase(),
92
+ path,
93
+ summary: operation.summary,
94
+ operationId: operation.operationId,
95
+ tags: operation.tags ?? []
96
+ });
97
+ }
98
+ endpoints.sort((a, b) => a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path));
99
+ return endpoints;
114
100
  }
101
+ //#endregion
102
+ export { DEFAULT_SPEC_URL, getEndpoints, loadSpec };
@@ -1,122 +1,324 @@
1
- import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
1
+ import { log } from "../log.js";
2
+ import { existsSync, realpathSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
+ import { spawn, spawnSync } from "node:child_process";
4
5
  import which from "which";
5
- import { log } from "../log.js";
6
- // npm first so it's the default/preselected choice; the rest follow in rough
7
- // popularity order.
8
- export const PACKAGE_MANAGERS = [
9
- "npm",
10
- "pnpm",
11
- "yarn",
12
- "bun",
6
+ //#region src/utils/package_manager.ts
7
+ /**
8
+ * Package manager detection and install helpers for the Neon CLI.
9
+ *
10
+ * Every command that shells out to install dependencies should go through here
11
+ * rather than reading `npm_config_user_agent` or lockfiles on its own.
12
+ *
13
+ * **`detect*` may return undefined, `resolve*` never does**, and `inferPackageManager` is the
14
+ * composed detector for callers that can prompt rather than guess.
15
+ *
16
+ * - {@link detectProjectPackageManager} — lockfile walk from `cwd` up to the repo root
17
+ * - {@link detectInvokingPackageManager} — the tool that launched us (`npx`, `pnpm dlx`, …)
18
+ * - {@link inferPackageManager} — project, then invocation, or nothing (when there is
19
+ * something to ask the user, an unanswerable guess is worse than a prompt)
20
+ * - {@link resolvePackageManager} — project lockfile, then invocation, then PATH, then npm
21
+ * (installing into an existing project directory)
22
+ * - {@link resolveInvokingPackageManager} — invocation, then PATH, then npm (global installs,
23
+ * or when the target directory has no lockfile yet, e.g. a fresh scaffold)
24
+ * - {@link installArgs} — argv to install into a project; {@link globalInstallCommand} —
25
+ * command and argv to install a CLI globally, or undefined when nothing here can
26
+ * - {@link formatInstallCommand} — shell-ready `pnpm install` / `npm add …` for agents and hints
27
+ * - {@link formatExecCommand} — shell-ready `pnpm exec drizzle-kit …` for a binary the project
28
+ * already depends on, plus {@link MISSING_BINARY_HINT} for the step that carries it
29
+ *
30
+ * Never re-derive any of this at a call site: a second detector is a second answer, and the
31
+ * two disagree exactly where it hurts — an agent told to run `npm install` in a pnpm project.
32
+ * That includes spelling an install command by hand. Every install string the CLI prints,
33
+ * emits as agent JSON, or hands to {@link runCommand} comes from the formatters here.
34
+ */
35
+ const PACKAGE_MANAGERS = [
36
+ "npm",
37
+ "pnpm",
38
+ "yarn",
39
+ "bun"
13
40
  ];
14
41
  /**
15
- * Lockfiles, and the package manager each one belongs to. npm is last on
16
- * purpose: a repo with both a pnpm lockfile and a leftover `package-lock.json`
17
- * (which a failed run like the one this fixes can leave behind) is a pnpm repo.
18
- */
42
+ * Lockfiles, and the package manager each one belongs to. npm is last on
43
+ * purpose: a repo with both a pnpm lockfile and a leftover `package-lock.json`
44
+ * (which a failed run like the one this fixes can leave behind) is a pnpm repo.
45
+ */
19
46
  const LOCKFILES = [
20
- ["pnpm-lock.yaml", "pnpm"],
21
- ["yarn.lock", "yarn"],
22
- // bun 1.2+ writes the text `bun.lock`; older versions the binary `bun.lockb`.
23
- ["bun.lock", "bun"],
24
- ["bun.lockb", "bun"],
25
- ["package-lock.json", "npm"],
47
+ ["pnpm-lock.yaml", "pnpm"],
48
+ ["yarn.lock", "yarn"],
49
+ ["bun.lock", "bun"],
50
+ ["bun.lockb", "bun"],
51
+ ["package-lock.json", "npm"],
52
+ ["npm-shrinkwrap.json", "npm"]
26
53
  ];
54
+ const lockfileIn = (dir) => {
55
+ for (const [file, pm] of LOCKFILES) if (existsSync(join(dir, file))) return pm;
56
+ };
57
+ /** `.git` is a file rather than a directory in a worktree or a submodule. */
58
+ const isRepoRoot = (dir) => existsSync(join(dir, ".git"));
59
+ /**
60
+ * The repository `dir` belongs to, or undefined when it belongs to none.
61
+ */
62
+ const repoRoot = (dir) => {
63
+ let current = dir;
64
+ for (;;) {
65
+ if (isRepoRoot(current)) return current;
66
+ const parent = dirname(current);
67
+ if (parent === current) return void 0;
68
+ current = parent;
69
+ }
70
+ };
27
71
  /**
28
- * The package manager the project at `cwd` uses, from its lockfile. Searches
29
- * `cwd` and then each parent up to the repo root: in a monorepo the lockfile
30
- * sits at the root while we scaffold into a package. Stopping at the root keeps
31
- * a stray lockfile above the repository from deciding how we install into it.
32
- */
33
- export const detectProjectPackageManager = (cwd) => {
34
- let dir = cwd;
35
- for (;;) {
36
- for (const [file, pm] of LOCKFILES) {
37
- if (existsSync(join(dir, file)))
38
- return pm;
39
- }
40
- // After the lockfiles, not before: the repo root's own lockfile counts.
41
- // `.git` is a file rather than a directory in a worktree or submodule.
42
- if (existsSync(join(dir, ".git")))
43
- return undefined;
44
- const parent = dirname(dir);
45
- if (parent === dir)
46
- return undefined;
47
- dir = parent;
48
- }
72
+ * The physical path, so the walk climbs real parents.
73
+ *
74
+ * `dirname` is lexical: given a symlink to `repo/packages/app`, its parent is
75
+ * the symlink's directory, not `repo/packages` — so the walk would leave the
76
+ * repository immediately and never see the root lockfile.
77
+ *
78
+ * A path that doesn't exist yet is fine and keeps its lexical form: `bootstrap`
79
+ * resolves a target directory before creating it. Anything else — a permission
80
+ * error, a symlink loop, a path component that is not a directory — is a broken
81
+ * input rather than a missing one, and silently falling back to the lexical path
82
+ * would pick a package manager off the wrong tree.
83
+ */
84
+ const physicalPath = (dir) => {
85
+ try {
86
+ return realpathSync(dir);
87
+ } catch (err) {
88
+ if (err.code === "ENOENT") return dir;
89
+ throw new Error(`Could not resolve ${dir} while detecting the package manager: ${err instanceof Error ? err.message : String(err)}`);
90
+ }
49
91
  };
50
92
  /**
51
- * The package manager the CLI was invoked through, read from the
52
- * `npm_config_user_agent` npm sets for `npm exec`/`npx`, `pnpm dlx`, `yarn
53
- * dlx`, and `bunx` (so `pnpm dlx neonctl …` installs with pnpm). Returns
54
- * undefined when there's nothing to infer from — e.g. a globally-installed
55
- * `neon`/`neonctl` — so the caller can ask (or fall back) instead of silently
56
- * assuming npm.
57
- */
58
- export const detectPackageManager = () => {
59
- const ua = process.env.npm_config_user_agent ?? "";
60
- if (ua.startsWith("pnpm"))
61
- return "pnpm";
62
- if (ua.startsWith("yarn"))
63
- return "yarn";
64
- if (ua.startsWith("bun"))
65
- return "bun";
66
- if (ua.startsWith("npm"))
67
- return "npm";
68
- return undefined;
93
+ * The package manager the project at `cwd` uses, from its lockfile.
94
+ *
95
+ * The repository is the search boundary, and it is located *first*. Inside one,
96
+ * the walk climbs from `cwd` to the repo root, because in a monorepo the
97
+ * lockfile sits at the root while we install into a package. Outside any
98
+ * repository — a directory `bootstrap` is about to scaffold into, which has no
99
+ * `.git` until a later step — only `cwd` itself is read.
100
+ *
101
+ * Locating the boundary first is what makes that second case safe. Climbing and
102
+ * checking as it went, a fresh scaffold under `~` would inherit a stray
103
+ * `~/package-lock.json` and install with npm on the strength of a lockfile
104
+ * belonging to nothing.
105
+ */
106
+ const detectProjectPackageManager = (cwd) => {
107
+ const start = physicalPath(cwd);
108
+ const root = repoRoot(start);
109
+ if (root === void 0) return lockfileIn(start);
110
+ let dir = start;
111
+ for (;;) {
112
+ const found = lockfileIn(dir);
113
+ if (found) return found;
114
+ if (dir === root) return void 0;
115
+ dir = dirname(dir);
116
+ }
117
+ };
118
+ /**
119
+ * The package manager the CLI was invoked through, read from the
120
+ * `npm_config_user_agent` npm sets for `npm exec`/`npx`, `pnpm dlx`, `yarn
121
+ * dlx`, and `bunx` (so `pnpm dlx neonctl …` installs with pnpm). Returns
122
+ * undefined when there's nothing to infer from — e.g. a globally-installed
123
+ * `neon`/`neonctl` — so the caller can ask (or fall back) instead of silently
124
+ * assuming npm.
125
+ */
126
+ const detectInvokingPackageManager = () => {
127
+ const ua = process.env.npm_config_user_agent ?? "";
128
+ if (ua.startsWith("pnpm")) return "pnpm";
129
+ if (ua.startsWith("yarn")) return "yarn";
130
+ if (ua.startsWith("bun")) return "bun";
131
+ if (ua.startsWith("npm")) return "npm";
69
132
  };
70
133
  /** The package managers actually on PATH, in {@link PACKAGE_MANAGERS} order. */
71
- export const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.sync(pm, { nothrow: true }) !== null);
72
- /**
73
- * Pick a package manager without prompting: the one the project at `cwd` uses,
74
- * else the one the CLI was invoked through, else the first one installed, else
75
- * npm. Used by non-interactive flows (e.g. `config init`) where there's no
76
- * scaffold prompt to hang a picker off.
77
- *
78
- * The project wins over the invocation on purpose. `npx neon …` inside a pnpm
79
- * repo should still install with pnpm — which tool launched us says nothing
80
- * about which one owns that project's `node_modules`, and running npm against
81
- * pnpm's symlinked tree is what this ordering exists to prevent.
82
- */
83
- export const resolvePackageManager = (cwd) => detectProjectPackageManager(cwd) ??
84
- detectPackageManager() ??
85
- installedPackageManagers()[0] ??
86
- "npm";
87
- /**
88
- * The argv that adds `packages` as runtime dependencies with `pm`. npm spells it
89
- * `install`; pnpm/yarn/bun use `add`.
90
- */
91
- export const addDependenciesArgs = (pm, packages) => (pm === "npm" ? ["install", ...packages] : ["add", ...packages]);
92
- /**
93
- * Run a command inheriting our stdio so the user sees install / link output
94
- * live and can answer any prompts the child raises. Resolves to whether it
95
- * exited cleanly; a non-zero exit is reported but never throws — the caller
96
- * decides whether to treat it as fatal.
97
- */
98
- export const runCommand = (cmd, args, cwd,
99
- /**
100
- * Extra environment for the child. Anything secret belongs here rather than in `args`:
101
- * arguments are visible to any process that can list processes, and both handlers below
102
- * print the full argument list when the command fails.
103
- */
104
- env) => new Promise((resolvePromise) => {
105
- // npm/pnpm/yarn ship as .cmd shims on Windows, which need a shell to run.
106
- const child = spawn(cmd, args, {
107
- cwd,
108
- stdio: "inherit",
109
- shell: process.platform === "win32",
110
- ...(env ? { env: { ...process.env, ...env } } : {}),
111
- });
112
- child.on("error", (err) => {
113
- log.warning("Could not run `%s %s`: %s", cmd, args.join(" "), err instanceof Error ? err.message : String(err));
114
- resolvePromise(false);
115
- });
116
- child.on("close", (code) => {
117
- if (code !== 0) {
118
- log.warning("`%s %s` exited with code %d.", cmd, args.join(" "), code);
119
- }
120
- resolvePromise(code === 0);
121
- });
134
+ const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.sync(pm, { nothrow: true }) !== null);
135
+ /**
136
+ * What can actually be inferred about the project at `cwd`: its lockfile, else
137
+ * the tool the CLI was invoked through, else nothing.
138
+ *
139
+ * Returns undefined rather than guessing, so an interactive caller can prompt.
140
+ * {@link resolvePackageManager} is the same chain with the unaskable fallbacks
141
+ * appended — use this one only where there is a user to ask.
142
+ */
143
+ const inferPackageManager = (cwd) => detectProjectPackageManager(cwd) ?? detectInvokingPackageManager();
144
+ /**
145
+ * Pick a package manager without prompting: the one the project at `cwd` uses,
146
+ * else the one the CLI was invoked through, else the first one installed, else
147
+ * npm. Used by non-interactive flows (e.g. `config init`) where there's no
148
+ * scaffold prompt to hang a picker off.
149
+ *
150
+ * The project wins over the invocation on purpose. `npx neon …` inside a pnpm
151
+ * repo should still install with pnpm — which tool launched us says nothing
152
+ * about which one owns that project's `node_modules`, and running npm against
153
+ * pnpm's symlinked tree is what this ordering exists to prevent.
154
+ */
155
+ const resolvePackageManager = (cwd) => inferPackageManager(cwd) ?? installedPackageManagers()[0] ?? "npm";
156
+ /**
157
+ * Pick a package manager when there is no project lockfile to read — global
158
+ * installs, or scaffolding into a directory that does not exist yet. Same chain
159
+ * as {@link resolvePackageManager} minus the lockfile walk.
160
+ */
161
+ const resolveInvokingPackageManager = () => detectInvokingPackageManager() ?? installedPackageManagers()[0] ?? "npm";
162
+ /**
163
+ * The argv for an install with `pm`: every dependency in the manifest when
164
+ * `packages` is empty, otherwise those packages added to it. npm spells the add
165
+ * verb `install`; pnpm/yarn/bun use `add`. All four spell the whole-manifest
166
+ * install `install`.
167
+ *
168
+ * The one argv builder — pass its result to {@link runCommand}, or use
169
+ * {@link formatInstallCommand} for a string. Nothing else assembles these words.
170
+ */
171
+ const installArgs = (pm, packages, options) => packages?.length ? [
172
+ pm === "npm" ? "install" : "add",
173
+ ...options?.dev ? ["-D"] : [],
174
+ ...packages
175
+ ] : ["install"];
176
+ /**
177
+ * yarn's major version, or undefined when it can't be read. Only consulted on
178
+ * the global-install path, which already spawns.
179
+ */
180
+ const yarnMajor = () => {
181
+ const major = spawnSync("yarn", ["--version"], {
182
+ encoding: "utf8",
183
+ timeout: 5e3,
184
+ shell: process.platform === "win32"
185
+ }).stdout?.trim().match(/^(\d+)\./);
186
+ return major ? Number(major[1]) : void 0;
187
+ };
188
+ /** The managers that can install a global CLI at all — yarn Berry cannot. */
189
+ const GLOBAL_CAPABLE = [
190
+ "npm",
191
+ "pnpm",
192
+ "bun"
193
+ ];
194
+ /**
195
+ * The argv that installs `pkg` globally — a standalone CLI, not a project
196
+ * dependency — or undefined when this machine has no way to do it.
197
+ *
198
+ * Yarn Berry removed global installs with no replacement, so `yarn global add`
199
+ * works on Classic only and Berry answers "Unknown command". Berry falls back to
200
+ * whichever global-capable manager is actually on PATH, which is normally npm.
201
+ *
202
+ * Substituting a manager is safe *here* and nowhere else in this module: a
203
+ * global install has no project dependency tree to be wrong about, which is the
204
+ * whole reason the project's manager must win for {@link installArgs}. What is
205
+ * not safe is naming a command that cannot run — npm ships with Node but is
206
+ * packaged separately on some Linux distributions, so returning `npm install -g`
207
+ * unchecked would hand a Berry user a command their machine does not have. The
208
+ * caller decides what to say when there is nothing to return.
209
+ */
210
+ const globalInstallCommand = (pm, pkg) => {
211
+ if (pm === "yarn" && yarnMajor() === 1) return {
212
+ command: "yarn",
213
+ args: [
214
+ "global",
215
+ "add",
216
+ pkg
217
+ ]
218
+ };
219
+ const installed = installedPackageManagers();
220
+ const usable = pm !== "yarn" && installed.includes(pm) ? pm : installed.find((candidate) => GLOBAL_CAPABLE.includes(candidate));
221
+ if (!usable) return void 0;
222
+ return usable === "npm" ? {
223
+ command: "npm",
224
+ args: [
225
+ "install",
226
+ "-g",
227
+ pkg
228
+ ]
229
+ } : {
230
+ command: usable,
231
+ args: [
232
+ "add",
233
+ "-g",
234
+ pkg
235
+ ]
236
+ };
237
+ };
238
+ /**
239
+ * A shell-ready line that runs `binary` out of the project's own
240
+ * `node_modules/.bin`.
241
+ *
242
+ * Every form here is local-only, which is the point: bare `npx` and `bunx` fall
243
+ * back to downloading a package that isn't installed, so a skipped install step
244
+ * would migrate someone's database with an unpinned tool fetched mid-run instead
245
+ * of failing. `npx --no` refuses with "canceled due to missing packages", and
246
+ * `bun run` with "Script not found".
247
+ *
248
+ * `pnpm exec` and `yarn run` are already local-only — `dlx` is the fetching
249
+ * counterpart in both. Use those deliberately, not these, to run something the
250
+ * project does not depend on.
251
+ *
252
+ * `yarn run` over `yarn exec`, which is documented for Berry but not Classic.
253
+ * Like `bun run`, it prefers a package.json script of the same name, so a script
254
+ * called `prisma` would shadow the binary.
255
+ */
256
+ const formatExecCommand = (pm, binary, args = []) => {
257
+ return [
258
+ {
259
+ npm: "npx --no",
260
+ pnpm: "pnpm exec",
261
+ yarn: "yarn run",
262
+ bun: "bun run"
263
+ }[pm],
264
+ binary,
265
+ ...args
266
+ ].join(" ");
267
+ };
268
+ /**
269
+ * Belongs in the description of any agent step whose command came from
270
+ * {@link formatExecCommand}.
271
+ *
272
+ * npm's own refusal reads "npx canceled due to missing packages and no YES
273
+ * option", which hands the reader the exact flag that defeats the protection —
274
+ * and the same payload contains `npx -y` elsewhere, so `-y` looks like the house
275
+ * style. Without this, an agent that hits the guard plausibly retries with `-y`
276
+ * and runs an unpinned migration tool against the user's database.
277
+ *
278
+ * Worded against the outcome rather than a message, because the four managers
279
+ * word it differently ("canceled due to missing packages", "Command not found",
280
+ * "Script not found"), and against "install the dependencies" rather than "the
281
+ * step above", because the `--apply` and connected getting-started payloads
282
+ * carry no install step for it to point at.
283
+ */
284
+ const MISSING_BINARY_HINT = "If this fails because the tool is not installed, install the project's dependencies — re-running the install step above if this payload has one — then retry this step. Do not add -y, and do not swap the runner for a fetching one (`npx` without `--no`, `pnpm dlx`, `yarn dlx`, `bunx`): those download an unpinned copy of the tool and run it against the user's database.";
285
+ /**
286
+ * Belongs in the description of any agent step whose command came from
287
+ * {@link formatInstallCommand}. The command already names the project's package
288
+ * manager, and an agent normalising it to npm is the bug this module exists to
289
+ * prevent.
290
+ */
291
+ const DO_NOT_SUBSTITUTE_HINT = "Run this step's `command` exactly as written — it already uses this project's package manager. Do not rewrite it to npm or any other manager.";
292
+ /**
293
+ * A shell-ready install line for agents and "next steps" hints — e.g.
294
+ * `pnpm add @neon/config @neon/env`, or `pnpm install` with no packages.
295
+ * All four managers spell the whole-manifest install `install`.
296
+ */
297
+ const formatInstallCommand = (pm, packages, options) => `${pm} ${installArgs(pm, packages, options).join(" ")}`;
298
+ /**
299
+ * Run a command inheriting our stdio so the user sees install / link output
300
+ * live and can answer any prompts the child raises. Resolves to whether it
301
+ * exited cleanly; a non-zero exit is reported but never throws — the caller
302
+ * decides whether to treat it as fatal.
303
+ */
304
+ const runCommand = (cmd, args, cwd, env) => new Promise((resolvePromise) => {
305
+ const child = spawn(cmd, args, {
306
+ cwd,
307
+ stdio: "inherit",
308
+ shell: process.platform === "win32",
309
+ ...env ? { env: {
310
+ ...process.env,
311
+ ...env
312
+ } } : {}
313
+ });
314
+ child.on("error", (err) => {
315
+ log.warning("Could not run `%s %s`: %s", cmd, args.join(" "), err instanceof Error ? err.message : String(err));
316
+ resolvePromise(false);
317
+ });
318
+ child.on("close", (code) => {
319
+ if (code !== 0) log.warning("`%s %s` exited with code %d.", cmd, args.join(" "), code);
320
+ resolvePromise(code === 0);
321
+ });
122
322
  });
323
+ //#endregion
324
+ export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolveInvokingPackageManager, resolvePackageManager, runCommand };