neonctl 2.27.1 → 2.29.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 (136) hide show
  1. package/README.md +35 -3
  2. package/dist/analytics.js +52 -34
  3. package/dist/api.js +643 -13
  4. package/dist/auth.js +50 -44
  5. package/dist/cli.js +8 -1
  6. package/dist/commands/auth.js +64 -51
  7. package/dist/commands/bootstrap.js +115 -157
  8. package/dist/commands/branches.js +160 -150
  9. package/dist/commands/bucket.js +183 -146
  10. package/dist/commands/checkout.js +51 -51
  11. package/dist/commands/config.js +228 -82
  12. package/dist/commands/connection_string.js +62 -62
  13. package/dist/commands/data_api.js +100 -101
  14. package/dist/commands/databases.js +29 -26
  15. package/dist/commands/deploy.js +12 -12
  16. package/dist/commands/dev.js +114 -114
  17. package/dist/commands/env.js +43 -43
  18. package/dist/commands/functions.js +101 -104
  19. package/dist/commands/index.js +27 -25
  20. package/dist/commands/init.js +23 -22
  21. package/dist/commands/ip_allow.js +29 -29
  22. package/dist/commands/link.js +232 -182
  23. package/dist/commands/neon_auth.js +385 -370
  24. package/dist/commands/operations.js +11 -11
  25. package/dist/commands/orgs.js +8 -8
  26. package/dist/commands/projects.js +103 -101
  27. package/dist/commands/psql.js +31 -31
  28. package/dist/commands/roles.js +27 -24
  29. package/dist/commands/schema_diff.js +25 -26
  30. package/dist/commands/set_context.js +17 -17
  31. package/dist/commands/status.js +40 -0
  32. package/dist/commands/user.js +5 -5
  33. package/dist/commands/vpc_endpoints.js +50 -50
  34. package/dist/config.js +7 -7
  35. package/dist/config_format.js +5 -5
  36. package/dist/context.js +37 -14
  37. package/dist/current_branch_fast_path.js +55 -0
  38. package/dist/dev/env.js +33 -33
  39. package/dist/dev/functions.js +4 -4
  40. package/dist/dev/inputs.js +6 -6
  41. package/dist/dev/runtime.js +25 -25
  42. package/dist/env.js +14 -14
  43. package/dist/env_file.js +13 -13
  44. package/dist/errors.js +68 -5
  45. package/dist/functions_api.js +10 -10
  46. package/dist/help.js +15 -15
  47. package/dist/index.js +110 -107
  48. package/dist/log.js +2 -2
  49. package/dist/parameters.gen.js +14 -14
  50. package/dist/pkg.js +5 -5
  51. package/dist/psql/cli.js +4 -2
  52. package/dist/psql/command/cmd_cond.js +61 -61
  53. package/dist/psql/command/cmd_connect.js +159 -154
  54. package/dist/psql/command/cmd_copy.js +107 -97
  55. package/dist/psql/command/cmd_describe.js +368 -363
  56. package/dist/psql/command/cmd_format.js +276 -263
  57. package/dist/psql/command/cmd_io.js +269 -263
  58. package/dist/psql/command/cmd_lo.js +74 -66
  59. package/dist/psql/command/cmd_meta.js +148 -148
  60. package/dist/psql/command/cmd_misc.js +17 -17
  61. package/dist/psql/command/cmd_pipeline.js +142 -135
  62. package/dist/psql/command/cmd_restrict.js +25 -25
  63. package/dist/psql/command/cmd_show.js +183 -168
  64. package/dist/psql/command/dispatch.js +26 -26
  65. package/dist/psql/command/shared.js +14 -14
  66. package/dist/psql/complete/filenames.js +16 -16
  67. package/dist/psql/complete/index.js +4 -4
  68. package/dist/psql/complete/matcher.js +33 -32
  69. package/dist/psql/complete/psqlVars.js +173 -173
  70. package/dist/psql/complete/queries.js +5 -3
  71. package/dist/psql/complete/rules.js +900 -863
  72. package/dist/psql/core/common.js +136 -133
  73. package/dist/psql/core/help.js +343 -343
  74. package/dist/psql/core/mainloop.js +160 -153
  75. package/dist/psql/core/prompt.js +126 -123
  76. package/dist/psql/core/settings.js +111 -111
  77. package/dist/psql/core/sqlHelp.js +150 -150
  78. package/dist/psql/core/startup.js +211 -205
  79. package/dist/psql/core/syncVars.js +14 -14
  80. package/dist/psql/core/variables.js +24 -24
  81. package/dist/psql/describe/formatters.js +302 -289
  82. package/dist/psql/describe/processNamePattern.js +28 -28
  83. package/dist/psql/describe/queries.js +656 -651
  84. package/dist/psql/index.js +436 -411
  85. package/dist/psql/io/history.js +36 -36
  86. package/dist/psql/io/input.js +15 -15
  87. package/dist/psql/io/lineEditor/buffer.js +27 -25
  88. package/dist/psql/io/lineEditor/complete.js +15 -15
  89. package/dist/psql/io/lineEditor/filename.js +22 -22
  90. package/dist/psql/io/lineEditor/index.js +65 -62
  91. package/dist/psql/io/lineEditor/keymap.js +325 -318
  92. package/dist/psql/io/lineEditor/vt100.js +60 -60
  93. package/dist/psql/io/pgpass.js +18 -18
  94. package/dist/psql/io/pgservice.js +14 -14
  95. package/dist/psql/io/psqlrc.js +46 -46
  96. package/dist/psql/print/aligned.js +175 -166
  97. package/dist/psql/print/asciidoc.js +51 -51
  98. package/dist/psql/print/crosstab.js +34 -31
  99. package/dist/psql/print/csv.js +25 -22
  100. package/dist/psql/print/html.js +54 -54
  101. package/dist/psql/print/json.js +12 -12
  102. package/dist/psql/print/latex.js +118 -118
  103. package/dist/psql/print/pager.js +28 -26
  104. package/dist/psql/print/troff.js +48 -48
  105. package/dist/psql/print/unaligned.js +15 -14
  106. package/dist/psql/print/units.js +17 -17
  107. package/dist/psql/scanner/slash.js +48 -46
  108. package/dist/psql/scanner/sql.js +88 -84
  109. package/dist/psql/scanner/stringutils.js +21 -17
  110. package/dist/psql/types/index.js +7 -7
  111. package/dist/psql/types/scanner.js +8 -8
  112. package/dist/psql/wire/connection.js +341 -327
  113. package/dist/psql/wire/copy.js +7 -7
  114. package/dist/psql/wire/pipeline.js +26 -24
  115. package/dist/psql/wire/protocol.js +102 -102
  116. package/dist/psql/wire/sasl.js +62 -62
  117. package/dist/psql/wire/tls.js +79 -73
  118. package/dist/storage_api.js +22 -23
  119. package/dist/test_utils/fixtures.js +74 -41
  120. package/dist/test_utils/oauth_server.js +5 -5
  121. package/dist/utils/api_enums.js +33 -0
  122. package/dist/utils/branch_notice.js +5 -5
  123. package/dist/utils/branch_picker.js +26 -26
  124. package/dist/utils/compute_units.js +4 -4
  125. package/dist/utils/enrichers.js +28 -16
  126. package/dist/utils/esbuild.js +28 -28
  127. package/dist/utils/formats.js +1 -1
  128. package/dist/utils/middlewares.js +3 -3
  129. package/dist/utils/package_manager.js +68 -0
  130. package/dist/utils/point_in_time.js +12 -12
  131. package/dist/utils/psql.js +30 -30
  132. package/dist/utils/string.js +2 -2
  133. package/dist/utils/ui.js +9 -9
  134. package/dist/utils/zip.js +1 -1
  135. package/dist/writer.js +17 -17
  136. package/package.json +10 -12
package/dist/dev/env.js CHANGED
@@ -1,7 +1,7 @@
1
- import { loadConfigFromFile, } from '@neondatabase/config';
2
- import { plan, pullConfig, } from '@neondatabase/config-runtime';
3
- import { fetchEnv, toEntries } from '@neondatabase/env';
4
- import { log } from '../log.js';
1
+ import { loadConfigFromFile } from "@neon/config";
2
+ import { plan, pullConfig } from "@neon/config-runtime";
3
+ import { fetchEnv, toEntries } from "@neon/env";
4
+ import { log } from "../log.js";
5
5
  /** The API-targeting options every runtime call forwards from the context. */
6
6
  const apiOptions = (ctx) => ({
7
7
  ...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
@@ -19,7 +19,7 @@ const apiOptions = (ctx) => ({
19
19
  export class DevEnvMismatchError extends Error {
20
20
  constructor() {
21
21
  super(...arguments);
22
- this.name = 'DevEnvMismatchError';
22
+ this.name = "DevEnvMismatchError";
23
23
  }
24
24
  }
25
25
  /**
@@ -30,7 +30,7 @@ export class DevEnvMismatchError extends Error {
30
30
  export class MissingBranchContextError extends Error {
31
31
  constructor() {
32
32
  super(...arguments);
33
- this.name = 'MissingBranchContextError';
33
+ this.name = "MissingBranchContextError";
34
34
  }
35
35
  }
36
36
  /**
@@ -56,9 +56,9 @@ export const resolveNeonEnvVars = async (ctx) => {
56
56
  const config = await loadNeonConfig(ctx.cwd);
57
57
  if (config) {
58
58
  if (!ctx.projectId || !ctx.branchId) {
59
- throw new MissingBranchContextError('Found a neon.ts but could not resolve the project/branch. ' +
60
- 'Run `neonctl link` and `neonctl checkout <branch>`, or pass ' +
61
- '--project-id / --branch.');
59
+ throw new MissingBranchContextError("Found a neon.ts but could not resolve the project/branch. " +
60
+ "Run `neonctl link` and `neonctl checkout <branch>`, or pass " +
61
+ "--project-id / --branch.");
62
62
  }
63
63
  // Resolve env from the policy with its `preview.functions` removed. Functions carry no
64
64
  // branch-level secrets — their env comes from the local `neon.ts` `functions.<slug>.env`,
@@ -82,8 +82,8 @@ export const resolveNeonEnvVars = async (ctx) => {
82
82
  // tuning closure), so it feeds straight into fetchEnv — no wrapping needed.
83
83
  return await fetchAndProject(pulled.config, ctx);
84
84
  }
85
- throw new MissingBranchContextError('No project/branch context found. Link a branch (`neonctl link` / ' +
86
- '`neonctl checkout`) or pass --project-id and --branch.');
85
+ throw new MissingBranchContextError("No project/branch context found. Link a branch (`neonctl link` / " +
86
+ "`neonctl checkout`) or pass --project-id and --branch.");
87
87
  };
88
88
  /**
89
89
  * `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
@@ -104,17 +104,17 @@ export const resolveDevEnv = async (ctx) => {
104
104
  if (err instanceof DevEnvMismatchError)
105
105
  throw err;
106
106
  if (err instanceof MissingBranchContextError) {
107
- log.debug('dev: %s; skipping env injection', err.message);
107
+ log.debug("dev: %s; skipping env injection", err.message);
108
108
  return {
109
109
  vars: {},
110
110
  skipped: {
111
- reason: 'no linked Neon branch — run `neonctl link`, then ' +
112
- '`neonctl checkout <branch>`, to inject DATABASE_URL and friends',
111
+ reason: "no linked Neon branch — run `neonctl link`, then " +
112
+ "`neonctl checkout <branch>`, to inject DATABASE_URL and friends",
113
113
  },
114
114
  };
115
115
  }
116
116
  const detail = err instanceof Error ? err.message : String(err);
117
- log.debug('dev: env resolution failed: %s', detail);
117
+ log.debug("dev: env resolution failed: %s", detail);
118
118
  return {
119
119
  vars: {},
120
120
  skipped: {
@@ -159,11 +159,11 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
159
159
  const missing = result.applied.filter(isMissingResource);
160
160
  if (missing.length === 0)
161
161
  return;
162
- const names = missing.map((change) => change.identifier).join(', ');
162
+ const names = missing.map((change) => change.identifier).join(", ");
163
163
  throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
164
- 'does not have it yet, so the matching env vars cannot be injected. ' +
165
- 'Provision it first with `neonctl deploy` (or `neonctl config apply`), ' +
166
- 'then re-run `neonctl dev`.');
164
+ "does not have it yet, so the matching env vars cannot be injected. " +
165
+ "Provision it first with `neonctl deploy` (or `neonctl config apply`), " +
166
+ "then re-run `neonctl dev`.");
167
167
  };
168
168
  /**
169
169
  * A planned change that provisions a branch-level resource the branch lacks: a
@@ -171,9 +171,9 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
171
171
  * setting drift (`update`) and `noop`s are ignored — they don't block local dev
172
172
  * — and functions are excluded (see {@link assertPolicyMatchesBranch}).
173
173
  */
174
- const isMissingResource = (change) => change.kind === 'service' &&
175
- change.action === 'create' &&
176
- !change.identifier.startsWith('function:');
174
+ const isMissingResource = (change) => change.kind === "service" &&
175
+ change.action === "create" &&
176
+ !change.identifier.startsWith("function:");
177
177
  const fetchAndProject = async (config, ctx) => {
178
178
  const env = await fetchEnv(config, {
179
179
  projectId: ctx.projectId,
@@ -191,17 +191,17 @@ const fetchAndProject = async (config, ctx) => {
191
191
  /**
192
192
  * Substrings that mark a module-resolution failure while loading `neon.ts` —
193
193
  * almost always because the project's dependencies aren't installed yet (the
194
- * config imports `@neondatabase/config` & friends). Deliberately specific:
194
+ * config imports `@neon/config` & friends). Deliberately specific:
195
195
  * the generic "…or a missing dependency…" hint the loader always appends is
196
196
  * NOT in here, so a real syntax/runtime error doesn't get mislabeled.
197
197
  */
198
198
  const MISSING_DEPENDENCY_HINTS = [
199
- 'cannot find module',
200
- 'cannot find package',
201
- 'err_module_not_found',
202
- 'failed to resolve',
203
- 'could not resolve',
204
- 'module not found',
199
+ "cannot find module",
200
+ "cannot find package",
201
+ "err_module_not_found",
202
+ "failed to resolve",
203
+ "could not resolve",
204
+ "module not found",
205
205
  ];
206
206
  /** Flatten an error and its `cause` chain to one lowercased string for matching. */
207
207
  const errorChainText = (err) => {
@@ -211,7 +211,7 @@ const errorChainText = (err) => {
211
211
  parts.push(current.message);
212
212
  current = current.cause;
213
213
  }
214
- return parts.join('\n').toLowerCase();
214
+ return parts.join("\n").toLowerCase();
215
215
  };
216
216
  const looksLikeMissingDependency = (err) => {
217
217
  const text = errorChainText(err);
@@ -230,9 +230,9 @@ const loadNeonConfig = async (cwd) => {
230
230
  // A neon.ts that imports a package which isn't installed fails here with a
231
231
  // cryptic "Cannot find module …". Turn that into the actionable thing to do.
232
232
  if (looksLikeMissingDependency(err)) {
233
- throw new Error('Could not load neon.ts: a package it imports is not installed. ' +
234
- 'Did you run `npm install`? Install your dependencies ' +
235
- '(npm / pnpm / yarn / bun), then try again.\n' +
233
+ throw new Error("Could not load neon.ts: a package it imports is not installed. " +
234
+ "Did you run `npm install`? Install your dependencies " +
235
+ "(npm / pnpm / yarn / bun), then try again.\n" +
236
236
  `Original error: ${message}`);
237
237
  }
238
238
  throw err;
@@ -1,6 +1,6 @@
1
- import { dirname, isAbsolute, resolve } from 'node:path';
2
- import { existsSync } from 'node:fs';
3
- import { loadConfigFromFile, resolveConfig, } from '@neondatabase/config';
1
+ import { existsSync } from "node:fs";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { loadConfigFromFile, resolveConfig, } from "@neon/config";
4
4
  /**
5
5
  * Load `neon.ts` (if any) and resolve the list of functions it declares into
6
6
  * {@link PlannedFunction}s for `neon dev` to serve. Returns `null` when there is no
@@ -16,7 +16,7 @@ export const resolveFunctionsFromConfig = async (cwd, branchName) => {
16
16
  return null;
17
17
  const { config, configDir, configPath } = loaded;
18
18
  const resolved = resolveConfig(config, {
19
- name: branchName ?? 'local',
19
+ name: branchName ?? "local",
20
20
  exists: branchName !== undefined,
21
21
  });
22
22
  const functions = resolved.preview?.functions ?? [];
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path';
1
+ import { resolve } from "node:path";
2
2
  const defaultDeps = {
3
3
  isPackaged: () => process.pkg !== undefined,
4
4
  loadEsbuild: (name) => import(name),
@@ -25,7 +25,7 @@ export const resolveWatchInputs = async (source, deps = defaultDeps) => {
25
25
  // 'esbuild', for the same reason as src/utils/esbuild.ts: rollup and
26
26
  // @yao-pkg/pkg statically scan for literal import()/require() and would pull
27
27
  // esbuild's native Go binary into the bundle/snapshot. Keep it invisible.
28
- const name = ['es', 'build'].join('');
28
+ const name = ["es", "build"].join("");
29
29
  let esbuild;
30
30
  try {
31
31
  esbuild = await deps.loadEsbuild(name);
@@ -42,10 +42,10 @@ export const resolveWatchInputs = async (source, deps = defaultDeps) => {
42
42
  bundle: true,
43
43
  write: false,
44
44
  metafile: true,
45
- format: 'esm',
46
- platform: 'node',
47
- packages: 'external',
48
- logLevel: 'silent',
45
+ format: "esm",
46
+ platform: "node",
47
+ packages: "external",
48
+ logLevel: "silent",
49
49
  });
50
50
  metafile = result.metafile;
51
51
  }
@@ -1,12 +1,12 @@
1
- import { createServer } from 'node:http';
2
- import { pathToFileURL } from 'node:url';
3
- import { resolve } from 'node:path';
4
- import { getRequestListener } from '@hono/node-server';
5
- const isFunction = (value) => typeof value === 'function';
6
- const hasFetchMethod = (value) => typeof value === 'object' &&
1
+ import { createServer } from "node:http";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ import { getRequestListener } from "@hono/node-server";
5
+ const isFunction = (value) => typeof value === "function";
6
+ const hasFetchMethod = (value) => typeof value === "object" &&
7
7
  value !== null &&
8
- 'fetch' in value &&
9
- typeof value.fetch === 'function';
8
+ "fetch" in value &&
9
+ typeof value.fetch === "function";
10
10
  /**
11
11
  * Resolve the user's exported handler to a single fetch callback.
12
12
  *
@@ -23,9 +23,9 @@ export const resolveFetchHandler = (mod) => {
23
23
  if (isFunction(defaultExport)) {
24
24
  return defaultExport;
25
25
  }
26
- throw new Error('No request handler found in the source module. Export one of:\n' +
27
- ' export default { fetch(req) { /* ... */ } }\n' +
28
- ' export default function (req) { /* ... */ }');
26
+ throw new Error("No request handler found in the source module. Export one of:\n" +
27
+ " export default { fetch(req) { /* ... */ } }\n" +
28
+ " export default function (req) { /* ... */ }");
29
29
  };
30
30
  /**
31
31
  * Wrap a fetch handler so user errors become a 500 response (with the message
@@ -41,18 +41,18 @@ export const withErrorBoundary = (handler) => {
41
41
  process.stderr.write(`Request handler threw an error:\n${message}\n`);
42
42
  return new Response(`Internal Server Error\n\n${message}`, {
43
43
  status: 500,
44
- headers: { 'content-type': 'text/plain; charset=utf-8' },
44
+ headers: { "content-type": "text/plain; charset=utf-8" },
45
45
  });
46
46
  }
47
47
  };
48
48
  };
49
- const isAddressInUse = (err) => typeof err === 'object' &&
49
+ const isAddressInUse = (err) => typeof err === "object" &&
50
50
  err !== null &&
51
- err.code === 'EADDRINUSE';
51
+ err.code === "EADDRINUSE";
52
52
  const DEFAULT_SEARCH_BASE = 8787;
53
53
  const MAX_SEARCH_STEPS = 100;
54
54
  const bindPort = async (server, selection, hostname) => {
55
- if (selection.mode === 'explicit') {
55
+ if (selection.mode === "explicit") {
56
56
  return listen(server, selection.port, hostname);
57
57
  }
58
58
  for (let step = 0; step < MAX_SEARCH_STEPS; step++) {
@@ -68,15 +68,15 @@ const bindPort = async (server, selection, hostname) => {
68
68
  };
69
69
  const listen = (server, port, hostname) => new Promise((resolveListen, rejectListen) => {
70
70
  const onError = (err) => {
71
- server.off('listening', onListening);
71
+ server.off("listening", onListening);
72
72
  rejectListen(err);
73
73
  };
74
74
  const onListening = () => {
75
- server.off('error', onError);
75
+ server.off("error", onError);
76
76
  resolveListen(server.address().port);
77
77
  };
78
- server.once('error', onError);
79
- server.once('listening', onListening);
78
+ server.once("error", onError);
79
+ server.once("listening", onListening);
80
80
  server.listen(port, hostname);
81
81
  });
82
82
  /**
@@ -106,16 +106,16 @@ export const startRuntime = async ({ source, port, hostname, }) => {
106
106
  */
107
107
  export const portSelectionFromEnv = (env) => {
108
108
  const explicit = env.NEON_DEV_PORT;
109
- if (explicit !== undefined && explicit !== '') {
110
- return { mode: 'explicit', port: parsePort(explicit, 'NEON_DEV_PORT') };
109
+ if (explicit !== undefined && explicit !== "") {
110
+ return { mode: "explicit", port: parsePort(explicit, "NEON_DEV_PORT") };
111
111
  }
112
112
  const injected = env.PORT;
113
- if (injected !== undefined && injected !== '') {
114
- return { mode: 'explicit', port: parsePort(injected, 'PORT') };
113
+ if (injected !== undefined && injected !== "") {
114
+ return { mode: "explicit", port: parsePort(injected, "PORT") };
115
115
  }
116
116
  const base = Number(env.NEON_DEV_PORT_BASE ?? DEFAULT_SEARCH_BASE);
117
117
  return {
118
- mode: 'search',
118
+ mode: "search",
119
119
  from: Number.isInteger(base) ? base : DEFAULT_SEARCH_BASE,
120
120
  };
121
121
  };
@@ -135,7 +135,7 @@ const isDirectExecution = () => {
135
135
  if (isDirectExecution()) {
136
136
  const source = process.env.NEON_DEV_SOURCE ?? process.argv[2];
137
137
  if (!source) {
138
- process.stderr.write('neon-dev runtime: missing source path\n');
138
+ process.stderr.write("neon-dev runtime: missing source path\n");
139
139
  process.exit(1);
140
140
  }
141
141
  startRuntime({ source, port: portSelectionFromEnv(process.env) }).catch((err) => {
package/dist/env.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export const isCi = () => {
2
- return process.env.CI !== 'false' && Boolean(process.env.CI);
2
+ return process.env.CI !== "false" && Boolean(process.env.CI);
3
3
  };
4
4
  export const isDebug = () => {
5
5
  return Boolean(process.env.DEBUG);
@@ -7,27 +7,27 @@ export const isDebug = () => {
7
7
  export const getGithubEnvVars = (env) => {
8
8
  const vars = [
9
9
  // github action info
10
- 'GITHUB_ACTION_PATH',
10
+ "GITHUB_ACTION_PATH",
11
11
  // source github repository
12
- 'GITHUB_REPOSITORY',
12
+ "GITHUB_REPOSITORY",
13
13
  // environment info
14
- 'GITHUB_RUN_ID',
15
- 'GITHUB_RUN_NUMBER',
16
- 'GITHUB_SERVER_URL',
17
- 'GITHUB_WORKFLOW_REF',
18
- 'RUNNER_ARCH',
19
- 'RUNNER_ENVIRONMENT',
20
- 'RUNNER_OS',
14
+ "GITHUB_RUN_ID",
15
+ "GITHUB_RUN_NUMBER",
16
+ "GITHUB_SERVER_URL",
17
+ "GITHUB_WORKFLOW_REF",
18
+ "RUNNER_ARCH",
19
+ "RUNNER_ENVIRONMENT",
20
+ "RUNNER_OS",
21
21
  ];
22
22
  const map = new Map();
23
23
  vars.forEach((v) => {
24
24
  let value = env[v];
25
- if (value === undefined || value === '') {
25
+ if (value === undefined || value === "") {
26
26
  return;
27
27
  }
28
- if (v === 'GITHUB_ACTION_PATH') {
29
- value = value.includes('actions/')
30
- ? value.replace(/^.*actions\/(.+)$/, '$1')
28
+ if (v === "GITHUB_ACTION_PATH") {
29
+ value = value.includes("actions/")
30
+ ? value.replace(/^.*actions\/(.+)$/, "$1")
31
31
  : value;
32
32
  }
33
33
  map.set(v, value);
package/dist/env_file.js CHANGED
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
- import { join } from 'node:path';
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
3
  /**
4
4
  * Default dotenv file `env pull` writes to: `.env` when one already exists in the working
5
5
  * directory (update where secrets already live), otherwise `.env.local` — matching the
@@ -8,9 +8,9 @@ import { join } from 'node:path';
8
8
  export const resolveEnvFilePath = (cwd, file) => {
9
9
  if (file)
10
10
  return join(cwd, file);
11
- if (existsSync(join(cwd, '.env')))
12
- return join(cwd, '.env');
13
- return join(cwd, '.env.local');
11
+ if (existsSync(join(cwd, ".env")))
12
+ return join(cwd, ".env");
13
+ return join(cwd, ".env.local");
14
14
  };
15
15
  /**
16
16
  * Merge `updates` into the dotenv content at `path`, preserving every other line
@@ -20,7 +20,7 @@ export const resolveEnvFilePath = (cwd, file) => {
20
20
  * `updates` is removed. Returns the keys written and the (managed) keys removed.
21
21
  */
22
22
  export const mergeEnvFile = (path, updates, options = {}) => {
23
- const original = existsSync(path) ? readFileSync(path, 'utf8') : '';
23
+ const original = existsSync(path) ? readFileSync(path, "utf8") : "";
24
24
  const { content, written, removed } = mergeEnvContent(original, updates, options);
25
25
  writeFileSync(path, content);
26
26
  return { written, removed };
@@ -40,7 +40,7 @@ export const mergeEnvContent = (original, updates, options = {}) => {
40
40
  }
41
41
  const remaining = new Set(keys);
42
42
  const removed = [];
43
- const lines = original === '' ? [] : original.split('\n');
43
+ const lines = original === "" ? [] : original.split("\n");
44
44
  // Walk the file: drop stale owned lines, update existing keys in place (so their position
45
45
  // and any surrounding comments are preserved), and pass everything else through untouched.
46
46
  const updatedLines = [];
@@ -62,10 +62,10 @@ export const mergeEnvContent = (original, updates, options = {}) => {
62
62
  .filter((key) => remaining.has(key))
63
63
  .map((key) => formatLine(key, updates[key]));
64
64
  const body = trimTrailingBlank(updatedLines);
65
- const content = [...body, ...appended].join('\n');
65
+ const content = [...body, ...appended].join("\n");
66
66
  return {
67
67
  // A dotenv file ends with a trailing newline.
68
- content: content === '' ? '' : `${content}\n`,
68
+ content: content === "" ? "" : `${content}\n`,
69
69
  written: keys,
70
70
  removed,
71
71
  };
@@ -82,7 +82,7 @@ export const readEnvFile = (path) => {
82
82
  throw new Error(`Env file not found: ${path}`);
83
83
  }
84
84
  const out = {};
85
- for (const line of readFileSync(path, 'utf8').split('\n')) {
85
+ for (const line of readFileSync(path, "utf8").split("\n")) {
86
86
  const parsed = parseAssignment(line);
87
87
  if (parsed)
88
88
  out[parsed.key] = parsed.value;
@@ -130,7 +130,7 @@ const parseAssignment = (line) => {
130
130
  /** Strip matching surrounding quotes and unescape `\"` / `\\` inside double quotes. */
131
131
  const unquote = (value) => {
132
132
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
133
- return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
133
+ return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
134
134
  }
135
135
  if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
136
136
  return value.slice(1, -1);
@@ -147,13 +147,13 @@ const formatLine = (key, value) => {
147
147
  const needsQuotes = /[\s#"'=]/.test(value);
148
148
  if (!needsQuotes)
149
149
  return `${key}=${value}`;
150
- const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
150
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
151
151
  return `${key}="${escaped}"`;
152
152
  };
153
153
  /** Drop trailing blank lines so we don't accumulate them across repeated merges. */
154
154
  const trimTrailingBlank = (lines) => {
155
155
  const out = [...lines];
156
- while (out.length > 0 && out[out.length - 1]?.trim() === '')
156
+ while (out.length > 0 && out[out.length - 1]?.trim() === "")
157
157
  out.pop();
158
158
  return out;
159
159
  };
package/dist/errors.js CHANGED
@@ -1,11 +1,11 @@
1
1
  const ERROR_MATCHERS = [
2
- [/^Unknown command: (.*)$/, 'UNKNOWN_COMMAND'],
3
- [/^Missing required argument: (.*)$/, 'MISSING_ARGUMENT'],
4
- [/^Failed to open web browser. (.*)$/, 'AUTH_BROWSER_FAILED'],
2
+ [/^Unknown command: (.*)$/, "UNKNOWN_COMMAND"],
3
+ [/^Missing required argument: (.*)$/, "MISSING_ARGUMENT"],
4
+ [/^Failed to open web browser. (.*)$/, "AUTH_BROWSER_FAILED"],
5
5
  ];
6
6
  export const matchErrorCode = (message) => {
7
7
  if (!message) {
8
- return 'UNKNOWN_ERROR';
8
+ return "UNKNOWN_ERROR";
9
9
  }
10
10
  for (const [matcher, code] of ERROR_MATCHERS) {
11
11
  const match = message.match(matcher);
@@ -13,5 +13,68 @@ export const matchErrorCode = (message) => {
13
13
  return code;
14
14
  }
15
15
  }
16
- return 'UNKNOWN_ERROR';
16
+ return "UNKNOWN_ERROR";
17
+ };
18
+ /**
19
+ * The single, human-readable line shown when the CLI couldn't reach the Neon API because
20
+ * of a connection-level failure (DNS, refused/reset connection, offline). It replaces the
21
+ * cryptic `fetch failed` / empty axios message a network blip otherwise surfaces (see
22
+ * {@link isNetworkError}), pointing at the two things the user can actually check.
23
+ */
24
+ export const NETWORK_ERROR_MESSAGE = "Could not reach the Neon API. Please check your internet connection and try again. " +
25
+ "If your connection is fine and this keeps happening, check https://neonstatus.com for ongoing incidents.";
26
+ /**
27
+ * Node-level socket/DNS error codes that mean the request never reached the server — a
28
+ * genuine connectivity problem rather than an API response we should surface. Deliberately
29
+ * excludes `ECONNABORTED` (axios' timeout), which the CLI already reports as a timeout.
30
+ */
31
+ const NETWORK_ERROR_CODES = new Set([
32
+ "ECONNREFUSED",
33
+ "ECONNRESET",
34
+ "ETIMEDOUT",
35
+ "ENOTFOUND",
36
+ "EAI_AGAIN",
37
+ "EPIPE",
38
+ "EHOSTUNREACH",
39
+ "ENETUNREACH",
40
+ "EHOSTDOWN",
41
+ "ENETDOWN",
42
+ ]);
43
+ /**
44
+ * Message fragments that mark a connection-level failure across our two transports: the
45
+ * `@neon/sdk` / global `fetch` path (used by `env pull` / `config` / `deploy`, and `link`'s
46
+ * bundled env pull) throws a bare `TypeError: fetch failed` / "Failed to fetch", while the
47
+ * axios `api-client` path throws an `AxiosError` whose message is "Network Error".
48
+ */
49
+ const NETWORK_ERROR_MESSAGE_PATTERN = /fetch failed|failed to fetch|network error/i;
50
+ const readErrorCode = (value) => {
51
+ if (value === null || typeof value !== "object")
52
+ return undefined;
53
+ const code = value.code;
54
+ return typeof code === "string" ? code : undefined;
55
+ };
56
+ /**
57
+ * Whether `err` is a connection-level failure (the network blip the user sees as a cryptic
58
+ * `fetch failed`, or no message at all on the axios path) rather than an actual API response.
59
+ *
60
+ * A Node `fetch` failure is a bare `TypeError: fetch failed` whose underlying `cause` carries
61
+ * the real socket `code` (e.g. `ECONNREFUSED`, `ENOTFOUND`, `EAI_AGAIN`), so we walk the
62
+ * `cause` chain checking both the code and the message. This is what lets the CLI swap the
63
+ * confusing default for {@link NETWORK_ERROR_MESSAGE}. It matches only failures where no
64
+ * response was ever received, so it never masks a real 4xx/5xx the user needs to see.
65
+ */
66
+ export const isNetworkError = (err) => {
67
+ let current = err;
68
+ for (let depth = 0; depth < 6 && current !== null && current !== undefined; depth++) {
69
+ const code = readErrorCode(current);
70
+ if (code !== undefined && NETWORK_ERROR_CODES.has(code)) {
71
+ return true;
72
+ }
73
+ if (current instanceof Error &&
74
+ NETWORK_ERROR_MESSAGE_PATTERN.test(current.message)) {
75
+ return true;
76
+ }
77
+ current = current.cause;
78
+ }
79
+ return false;
17
80
  };
@@ -1,42 +1,42 @@
1
- import { ContentType } from '@neondatabase/api-client';
1
+ import { ContentType } from "./api.js";
2
2
  const functionsPath = (projectId, branchId) => `/projects/${encodeURIComponent(projectId)}/branches/${encodeURIComponent(branchId)}/functions`;
3
3
  export const listFunctions = async (apiClient, projectId, branchId, { cursor, limit } = {}) => {
4
4
  const { data } = await apiClient.request({
5
5
  path: functionsPath(projectId, branchId),
6
- method: 'GET',
6
+ method: "GET",
7
7
  query: { cursor, limit },
8
8
  secure: true,
9
- format: 'json',
9
+ format: "json",
10
10
  });
11
11
  return { functions: data.functions ?? [], next: data.pagination?.next };
12
12
  };
13
13
  export const getFunction = async (apiClient, projectId, branchId, slug) => {
14
14
  const { data } = await apiClient.request({
15
15
  path: `${functionsPath(projectId, branchId)}/${encodeURIComponent(slug)}`,
16
- method: 'GET',
16
+ method: "GET",
17
17
  secure: true,
18
- format: 'json',
18
+ format: "json",
19
19
  });
20
20
  return data.function;
21
21
  };
22
22
  export const deleteFunction = async (apiClient, projectId, branchId, slug) => {
23
23
  await apiClient.request({
24
24
  path: `${functionsPath(projectId, branchId)}/${encodeURIComponent(slug)}`,
25
- method: 'DELETE',
25
+ method: "DELETE",
26
26
  secure: true,
27
27
  });
28
28
  };
29
29
  export const createDeployment = async (apiClient, projectId, branchId, slug, params) => {
30
30
  const form = new FormData();
31
- form.append('zip', new Blob([params.zip]), 'bundle.zip');
32
- form.append('runtime', params.runtime);
31
+ form.append("zip", new Blob([params.zip]), "bundle.zip");
32
+ form.append("runtime", params.runtime);
33
33
  if (params.environment)
34
- form.append('environment', params.environment);
34
+ form.append("environment", params.environment);
35
35
  // The deploy POST returns an operation the CLI cannot poll; the body is
36
36
  // ignored. We only need the request to succeed.
37
37
  await apiClient.request({
38
38
  path: `${functionsPath(projectId, branchId)}/${encodeURIComponent(slug)}/deployments`,
39
- method: 'POST',
39
+ method: "POST",
40
40
  type: ContentType.FormData,
41
41
  body: form,
42
42
  secure: true,