neonctl 2.27.0 → 2.28.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 (151) hide show
  1. package/LICENSE.md +178 -0
  2. package/README.md +33 -1
  3. package/{analytics.js → dist/analytics.js} +21 -5
  4. package/dist/api.js +665 -0
  5. package/dist/cli.js +9 -0
  6. package/{commands → dist/commands}/auth.js +7 -0
  7. package/{commands → dist/commands}/branches.js +7 -4
  8. package/{commands → dist/commands}/bucket.js +69 -37
  9. package/{commands → dist/commands}/checkout.js +3 -3
  10. package/{commands → dist/commands}/config.js +22 -0
  11. package/{commands → dist/commands}/connection_string.js +1 -1
  12. package/{commands → dist/commands}/data_api.js +5 -6
  13. package/{commands → dist/commands}/databases.js +6 -3
  14. package/{commands → dist/commands}/functions.js +7 -9
  15. package/{commands → dist/commands}/index.js +2 -0
  16. package/{commands → dist/commands}/link.js +10 -17
  17. package/{commands → dist/commands}/neon_auth.js +8 -11
  18. package/{commands → dist/commands}/projects.js +4 -4
  19. package/{commands → dist/commands}/psql.js +1 -1
  20. package/{commands → dist/commands}/roles.js +6 -3
  21. package/{commands → dist/commands}/schema_diff.js +3 -4
  22. package/dist/commands/status.js +40 -0
  23. package/{context.js → dist/context.js} +16 -0
  24. package/dist/current_branch_fast_path.js +55 -0
  25. package/dist/errors.js +80 -0
  26. package/{functions_api.js → dist/functions_api.js} +1 -1
  27. package/{index.js → dist/index.js} +21 -20
  28. package/{parameters.gen.js → dist/parameters.gen.js} +14 -14
  29. package/{psql → dist/psql}/cli.js +0 -0
  30. package/{storage_api.js → dist/storage_api.js} +7 -8
  31. package/{test_utils → dist/test_utils}/fixtures.js +45 -15
  32. package/dist/utils/api_enums.js +33 -0
  33. package/{utils → dist/utils}/branch_picker.js +1 -1
  34. package/{utils → dist/utils}/enrichers.js +11 -4
  35. package/package.json +64 -67
  36. package/api.js +0 -35
  37. package/cli.js +0 -2
  38. package/errors.js +0 -17
  39. /package/{auth.js → dist/auth.js} +0 -0
  40. /package/{callback.html → dist/callback.html} +0 -0
  41. /package/{commands → dist/commands}/bootstrap.js +0 -0
  42. /package/{commands → dist/commands}/deploy.js +0 -0
  43. /package/{commands → dist/commands}/dev.js +0 -0
  44. /package/{commands → dist/commands}/env.js +0 -0
  45. /package/{commands → dist/commands}/init.js +0 -0
  46. /package/{commands → dist/commands}/ip_allow.js +0 -0
  47. /package/{commands → dist/commands}/operations.js +0 -0
  48. /package/{commands → dist/commands}/orgs.js +0 -0
  49. /package/{commands → dist/commands}/set_context.js +0 -0
  50. /package/{commands → dist/commands}/user.js +0 -0
  51. /package/{commands → dist/commands}/vpc_endpoints.js +0 -0
  52. /package/{config.js → dist/config.js} +0 -0
  53. /package/{config_format.js → dist/config_format.js} +0 -0
  54. /package/{dev → dist/dev}/env.js +0 -0
  55. /package/{dev → dist/dev}/functions.js +0 -0
  56. /package/{dev → dist/dev}/inputs.js +0 -0
  57. /package/{dev → dist/dev}/runtime.js +0 -0
  58. /package/{env.js → dist/env.js} +0 -0
  59. /package/{env_file.js → dist/env_file.js} +0 -0
  60. /package/{help.js → dist/help.js} +0 -0
  61. /package/{log.js → dist/log.js} +0 -0
  62. /package/{pkg.js → dist/pkg.js} +0 -0
  63. /package/{psql → dist/psql}/command/cmd_cond.js +0 -0
  64. /package/{psql → dist/psql}/command/cmd_connect.js +0 -0
  65. /package/{psql → dist/psql}/command/cmd_copy.js +0 -0
  66. /package/{psql → dist/psql}/command/cmd_describe.js +0 -0
  67. /package/{psql → dist/psql}/command/cmd_format.js +0 -0
  68. /package/{psql → dist/psql}/command/cmd_io.js +0 -0
  69. /package/{psql → dist/psql}/command/cmd_lo.js +0 -0
  70. /package/{psql → dist/psql}/command/cmd_meta.js +0 -0
  71. /package/{psql → dist/psql}/command/cmd_misc.js +0 -0
  72. /package/{psql → dist/psql}/command/cmd_pipeline.js +0 -0
  73. /package/{psql → dist/psql}/command/cmd_restrict.js +0 -0
  74. /package/{psql → dist/psql}/command/cmd_show.js +0 -0
  75. /package/{psql → dist/psql}/command/dispatch.js +0 -0
  76. /package/{psql → dist/psql}/command/inputQueue.js +0 -0
  77. /package/{psql → dist/psql}/command/shared.js +0 -0
  78. /package/{psql → dist/psql}/complete/filenames.js +0 -0
  79. /package/{psql → dist/psql}/complete/index.js +0 -0
  80. /package/{psql → dist/psql}/complete/matcher.js +0 -0
  81. /package/{psql → dist/psql}/complete/psqlVars.js +0 -0
  82. /package/{psql → dist/psql}/complete/queries.js +0 -0
  83. /package/{psql → dist/psql}/complete/rules.js +0 -0
  84. /package/{psql → dist/psql}/core/common.js +0 -0
  85. /package/{psql → dist/psql}/core/help.js +0 -0
  86. /package/{psql → dist/psql}/core/mainloop.js +0 -0
  87. /package/{psql → dist/psql}/core/prompt.js +0 -0
  88. /package/{psql → dist/psql}/core/settings.js +0 -0
  89. /package/{psql → dist/psql}/core/sqlHelp.js +0 -0
  90. /package/{psql → dist/psql}/core/startup.js +0 -0
  91. /package/{psql → dist/psql}/core/syncVars.js +0 -0
  92. /package/{psql → dist/psql}/core/variables.js +0 -0
  93. /package/{psql → dist/psql}/describe/formatters.js +0 -0
  94. /package/{psql → dist/psql}/describe/processNamePattern.js +0 -0
  95. /package/{psql → dist/psql}/describe/queries.js +0 -0
  96. /package/{psql → dist/psql}/describe/versionGate.js +0 -0
  97. /package/{psql → dist/psql}/index.js +0 -0
  98. /package/{psql → dist/psql}/io/history.js +0 -0
  99. /package/{psql → dist/psql}/io/input.js +0 -0
  100. /package/{psql → dist/psql}/io/lineEditor/buffer.js +0 -0
  101. /package/{psql → dist/psql}/io/lineEditor/complete.js +0 -0
  102. /package/{psql → dist/psql}/io/lineEditor/filename.js +0 -0
  103. /package/{psql → dist/psql}/io/lineEditor/index.js +0 -0
  104. /package/{psql → dist/psql}/io/lineEditor/keymap.js +0 -0
  105. /package/{psql → dist/psql}/io/lineEditor/vt100.js +0 -0
  106. /package/{psql → dist/psql}/io/pgpass.js +0 -0
  107. /package/{psql → dist/psql}/io/pgservice.js +0 -0
  108. /package/{psql → dist/psql}/io/psqlrc.js +0 -0
  109. /package/{psql → dist/psql}/print/aligned.js +0 -0
  110. /package/{psql → dist/psql}/print/asciidoc.js +0 -0
  111. /package/{psql → dist/psql}/print/crosstab.js +0 -0
  112. /package/{psql → dist/psql}/print/csv.js +0 -0
  113. /package/{psql → dist/psql}/print/html.js +0 -0
  114. /package/{psql → dist/psql}/print/json.js +0 -0
  115. /package/{psql → dist/psql}/print/latex.js +0 -0
  116. /package/{psql → dist/psql}/print/pager.js +0 -0
  117. /package/{psql → dist/psql}/print/troff.js +0 -0
  118. /package/{psql → dist/psql}/print/unaligned.js +0 -0
  119. /package/{psql → dist/psql}/print/units.js +0 -0
  120. /package/{psql → dist/psql}/scanner/slash.js +0 -0
  121. /package/{psql → dist/psql}/scanner/sql.js +0 -0
  122. /package/{psql → dist/psql}/scanner/stringutils.js +0 -0
  123. /package/{psql → dist/psql}/types/backslash.js +0 -0
  124. /package/{psql → dist/psql}/types/connection.js +0 -0
  125. /package/{psql → dist/psql}/types/index.js +0 -0
  126. /package/{psql → dist/psql}/types/printer.js +0 -0
  127. /package/{psql → dist/psql}/types/repl.js +0 -0
  128. /package/{psql → dist/psql}/types/scanner.js +0 -0
  129. /package/{psql → dist/psql}/types/settings.js +0 -0
  130. /package/{psql → dist/psql}/types/variables.js +0 -0
  131. /package/{psql → dist/psql}/wire/connection.js +0 -0
  132. /package/{psql → dist/psql}/wire/copy.js +0 -0
  133. /package/{psql → dist/psql}/wire/notify.js +0 -0
  134. /package/{psql → dist/psql}/wire/pipeline.js +0 -0
  135. /package/{psql → dist/psql}/wire/protocol.js +0 -0
  136. /package/{psql → dist/psql}/wire/sasl.js +0 -0
  137. /package/{psql → dist/psql}/wire/tls.js +0 -0
  138. /package/{test_utils → dist/test_utils}/oauth_server.js +0 -0
  139. /package/{types.js → dist/types.js} +0 -0
  140. /package/{utils → dist/utils}/auth.js +0 -0
  141. /package/{utils → dist/utils}/branch_notice.js +0 -0
  142. /package/{utils → dist/utils}/compute_units.js +0 -0
  143. /package/{utils → dist/utils}/esbuild.js +0 -0
  144. /package/{utils → dist/utils}/formats.js +0 -0
  145. /package/{utils → dist/utils}/middlewares.js +0 -0
  146. /package/{utils → dist/utils}/point_in_time.js +0 -0
  147. /package/{utils → dist/utils}/psql.js +0 -0
  148. /package/{utils → dist/utils}/string.js +0 -0
  149. /package/{utils → dist/utils}/ui.js +0 -0
  150. /package/{utils → dist/utils}/zip.js +0 -0
  151. /package/{writer.js → dist/writer.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { EndpointType } from '@neondatabase/api-client';
1
+ import { EndpointType } from '../utils/api_enums.js';
2
2
  import { contextBranch, readContextFile } from '../context.js';
3
3
  import { writer } from '../writer.js';
4
4
  import { branchCreateRequest } from '../parameters.gen.js';
@@ -367,9 +367,12 @@ const setDefault = async (props) => {
367
367
  const deleteBranch = async (props) => {
368
368
  const branchId = await branchIdFromProps(props);
369
369
  const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranch(props.projectId, branchId));
370
- writer(props).end(data.branch, {
371
- fields: BRANCH_FIELDS,
372
- });
370
+ // A 204 (branch already gone) carries no body; only a 200 returns it.
371
+ if (data) {
372
+ writer(props).end(data.branch, {
373
+ fields: BRANCH_FIELDS,
374
+ });
375
+ }
373
376
  };
374
377
  const get = async (props) => {
375
378
  const branchId = await branchIdFromProps(props);
@@ -2,8 +2,7 @@ import { createReadStream, createWriteStream } from 'node:fs';
2
2
  import { stat, unlink } from 'node:fs/promises';
3
3
  import { basename } from 'node:path';
4
4
  import { pipeline } from 'node:stream/promises';
5
- import axios, { isAxiosError } from 'axios';
6
- import { retryOnLock } from '../api.js';
5
+ import { isNeonApiError, retryOnLock } from '../api.js';
7
6
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
8
7
  import { log } from '../log.js';
9
8
  import { writer } from '../writer.js';
@@ -42,8 +41,9 @@ export const splitBucketTarget = (target) => {
42
41
  rest: target.slice(slash + 1),
43
42
  };
44
43
  };
45
- export const command = 'bucket';
44
+ export const command = 'buckets';
46
45
  export const describe = 'Manage branch object-storage buckets and their objects';
46
+ export const aliases = ['bucket'];
47
47
  export const builder = (argv) => argv
48
48
  .usage('$0 bucket <sub-command> [options]')
49
49
  .options({
@@ -221,7 +221,7 @@ const deleteBucket = async (props) => {
221
221
  }));
222
222
  }
223
223
  catch (err) {
224
- if (isAxiosError(err) && err.response?.status === 404) {
224
+ if (isNeonApiError(err) && err.status === 404) {
225
225
  throw new Error(`Bucket "${props.name}" not found on branch ${branchId}.`);
226
226
  }
227
227
  throw err;
@@ -331,14 +331,42 @@ const objectNotFoundFallback = (key, bucket, branchId) => `Object "${key}" not f
331
331
  // misreported as a missing object; otherwise fall back to a clean default. Used
332
332
  // for the JSON (non-streamed) endpoints where the body is already parsed.
333
333
  const objectNotFoundMessage = (err, key, bucket, branchId) => {
334
- if (isAxiosError(err)) {
335
- const serverMessage = serverErrorMessage(err.response?.data);
334
+ if (isNeonApiError(err)) {
335
+ const serverMessage = serverErrorMessage(err.data);
336
336
  if (serverMessage !== undefined) {
337
337
  return serverMessage;
338
338
  }
339
339
  }
340
340
  return objectNotFoundFallback(key, bucket, branchId);
341
341
  };
342
+ // Stream a file from disk as a WHATWG `ReadableStream` suitable for a `fetch`
343
+ // request body, applying backpressure so we never read faster than the upload
344
+ // drains. Uses the global `ReadableStream` (what `fetch` expects) directly, so
345
+ // there's no Node-vs-DOM stream type bridging.
346
+ const fileToWebStream = (path) => {
347
+ const source = createReadStream(path);
348
+ return new ReadableStream({
349
+ start(controller) {
350
+ source.on('data', (chunk) => {
351
+ controller.enqueue(new Uint8Array(chunk));
352
+ if ((controller.desiredSize ?? 0) <= 0)
353
+ source.pause();
354
+ });
355
+ source.on('end', () => {
356
+ controller.close();
357
+ });
358
+ source.on('error', (err) => {
359
+ controller.error(err instanceof Error ? err : new Error(String(err)));
360
+ });
361
+ },
362
+ pull() {
363
+ source.resume();
364
+ },
365
+ cancel() {
366
+ source.destroy();
367
+ },
368
+ });
369
+ };
342
370
  const getObject = async (props) => {
343
371
  const branchId = await branchIdFromProps(props);
344
372
  const { bucket, rest: key } = splitBucketTarget(props.target);
@@ -355,11 +383,11 @@ const getObject = async (props) => {
355
383
  });
356
384
  }
357
385
  catch (err) {
358
- if (isAxiosError(err) && err.response?.status === 404) {
386
+ if (isNeonApiError(err) && err.status === 404) {
359
387
  // The download response is a stream, so a 404 body arrives as a stream
360
388
  // too; drain and parse it to recover the server's message (which
361
389
  // distinguishes a missing bucket from a missing object).
362
- const serverMessage = await streamErrorMessage(err.response.data);
390
+ const serverMessage = await streamErrorMessage(err.data);
363
391
  throw new Error(serverMessage ?? objectNotFoundFallback(key, bucket, branchId));
364
392
  }
365
393
  throw err;
@@ -416,52 +444,56 @@ const putObject = async (props) => {
416
444
  }));
417
445
  }
418
446
  catch (err) {
419
- if (isAxiosError(err)) {
420
- const status = err.response?.status;
447
+ if (isNeonApiError(err)) {
448
+ const status = err.status;
421
449
  if (status === 404) {
422
450
  throw new Error(objectNotFoundMessage(err, key, bucket, branchId));
423
451
  }
424
452
  // Any other HTTP error from the console (e.g. 403 when the caller lacks
425
453
  // write permission on the bucket) carries the same JSON `{ message }`
426
- // body, so surface that rather than a bare axios message. When the body
427
- // has no usable message, fall back to a clean status-bearing error.
428
- const serverMessage = serverErrorMessage(err.response?.data);
454
+ // body, so surface that rather than a bare error. When the body has no
455
+ // usable message, fall back to a clean status-bearing error.
456
+ const serverMessage = serverErrorMessage(err.data);
429
457
  throw new Error(serverMessage ??
430
458
  `Failed to presign upload for "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ''}: ${err.message}`);
431
459
  }
432
460
  throw err;
433
461
  }
434
- // Stream the file straight into the PUT body; never buffer the whole file.
435
- // The presigned URL targets the branch S3 data-plane endpoint directly, so
436
- // this PUT goes through a plain axios call rather than the console api-client.
462
+ // Stream the file straight into the PUT body via `fetch`; never buffer the
463
+ // whole file. The presigned URL targets the branch S3 data-plane endpoint
464
+ // directly, so this PUT bypasses the console API entirely.
437
465
  //
438
466
  // `presign.headers` carries the signature-relevant headers (e.g. host,
439
467
  // content-type); the server does not sign Content-Length, so we set it
440
- // ourselves from the stat'd size to keep the upload streamed, not chunked.
441
- // `maxRedirects: 0` ensures we never resend the file bytes and signed headers
442
- // to a different host if the data-plane endpoint were to answer with a
443
- // redirect.
468
+ // ourselves from the stat'd size. `redirect: 'error'` ensures we never resend
469
+ // the file bytes and signed headers to a different host if the data-plane
470
+ // endpoint were to answer with a redirect. `duplex: 'half'` is required by
471
+ // fetch when streaming a request body.
472
+ const upload = {
473
+ method: 'PUT',
474
+ headers: {
475
+ ...presign.headers,
476
+ 'Content-Length': String(fileSize),
477
+ },
478
+ body: fileToWebStream(props.file),
479
+ redirect: 'error',
480
+ duplex: 'half',
481
+ };
482
+ let uploadResponse;
444
483
  try {
445
- await axios.put(presign.url, createReadStream(props.file), {
446
- headers: {
447
- ...presign.headers,
448
- 'Content-Length': fileSize,
449
- },
450
- maxBodyLength: Infinity,
451
- maxContentLength: Infinity,
452
- maxRedirects: 0,
453
- });
484
+ uploadResponse = await fetch(presign.url, upload);
454
485
  }
455
486
  catch (err) {
487
+ // A transport-level failure (DNS, connection reset, redirect when none is
488
+ // allowed). Surface a clean message without leaking the signed URL.
489
+ throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}: ${err instanceof Error ? err.message : String(err)}`);
490
+ }
491
+ if (!uploadResponse.ok) {
456
492
  // The upload targets the S3 data plane, whose error bodies are XML rather
457
493
  // than the JSON `{ message }` the console returns, so surface the status
458
- // (and axios message) rather than leaking a raw error. Never include the
459
- // presigned URL, which carries the signature.
460
- if (isAxiosError(err)) {
461
- const status = err.response?.status;
462
- throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId}${status !== undefined ? ` (HTTP ${status})` : ''}: ${err.message}`);
463
- }
464
- throw err;
494
+ // rather than the body. Never include the presigned URL, which carries the
495
+ // signature.
496
+ throw new Error(`Failed to upload "${props.file}" to "${key}" in bucket "${bucket}" on branch ${branchId} (HTTP ${uploadResponse.status}): Request failed with status code ${uploadResponse.status}`);
465
497
  }
466
498
  log.info(`File "${props.file}" uploaded to "${key}" in bucket "${bucket}" on branch ${branchId}`);
467
499
  };
@@ -496,7 +528,7 @@ const deleteObject = async (props) => {
496
528
  }));
497
529
  }
498
530
  catch (err) {
499
- if (isAxiosError(err) && err.response?.status === 404) {
531
+ if (isNeonApiError(err) && err.status === 404) {
500
532
  throw new Error(objectNotFoundMessage(err, rest, bucket, branchId));
501
533
  }
502
534
  throw err;
@@ -1,4 +1,4 @@
1
- import { isAxiosError } from 'axios';
1
+ import { isNeonApiError } from '../api.js';
2
2
  import chalk from 'chalk';
3
3
  import prompts from 'prompts';
4
4
  import { applyContext, contextBranch, readContextFile } from '../context.js';
@@ -206,7 +206,7 @@ const resolveOrgId = async (props, projectId) => {
206
206
  return data.project.org_id ?? undefined;
207
207
  }
208
208
  catch (err) {
209
- if (isAxiosError(err) && err.response?.status === 401) {
209
+ if (isNeonApiError(err) && err.status === 401) {
210
210
  throw err;
211
211
  }
212
212
  log.debug('checkout: could not resolve org id for project %s: %s', projectId, err instanceof Error ? err.message : String(err));
@@ -280,7 +280,7 @@ const tryAutoDetectProject = async (props) => {
280
280
  // `fillSingleProject` throws on "No projects found" / "Multiple projects
281
281
  // found" — both mean we can't pick a project automatically. Network/auth
282
282
  // errors are real and should surface to the user.
283
- if (isAxiosError(err)) {
283
+ if (isNeonApiError(err)) {
284
284
  throw err;
285
285
  }
286
286
  log.debug('checkout: could not auto-detect a single project: %s', err instanceof Error ? err.message : String(err));
@@ -2,6 +2,7 @@ import chalk from 'chalk';
2
2
  import { resolveConfig } from '@neondatabase/config';
3
3
  import { apply, createBranch as createBranchFromPolicy, inspect, loadConfigFromFile, plan, } from '@neondatabase/config-runtime';
4
4
  import { toNeonConfigView } from '../config_format.js';
5
+ import { contextBranch, readContextFile } from '../context.js';
5
6
  import { log } from '../log.js';
6
7
  import { isCi } from '../env.js';
7
8
  import { loadEnvFileIntoProcess } from '../env_file.js';
@@ -93,6 +94,12 @@ export const builder = (argv) => argv
93
94
  type: 'boolean',
94
95
  default: false,
95
96
  },
97
+ 'current-branch': {
98
+ describe: 'Print only the linked branch name from the local .neon file ' +
99
+ '(no network). Exits non-zero when no branch is pinned.',
100
+ type: 'boolean',
101
+ default: false,
102
+ },
96
103
  }), (args) => status(args))
97
104
  .command('plan', 'Show what `config apply` would change (dry run)', (yargs) => yargs.options({
98
105
  config: {
@@ -126,6 +133,21 @@ const loadConfig = async (props) => {
126
133
  return config;
127
134
  };
128
135
  export const status = async (props) => {
136
+ // `--current-branch` short-circuits here (before resolveBranchRef), so it wins
137
+ // over --config-json and ignores --output. See ConfigProps.currentBranch / isCurrentBranchProbe.
138
+ if (props.currentBranch) {
139
+ const branch = contextBranch(readContextFile(props.contextFile));
140
+ if (branch) {
141
+ process.stdout.write(`${branch}\n`);
142
+ }
143
+ else {
144
+ // No branch pinned: hint on stderr and exit non-zero (grep-style) so a prompt's
145
+ // `when` hides the segment cleanly instead of rendering a bare icon.
146
+ log.info('No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.');
147
+ process.exitCode = 1;
148
+ }
149
+ return;
150
+ }
129
151
  const branch = await resolveBranchRef(props);
130
152
  // `--config-json` is a script-friendly mode that emits only JSON to stdout, so keep it
131
153
  // pristine; the regular human view gets the "which branch am I inspecting" guardrail.
@@ -1,4 +1,4 @@
1
- import { EndpointType, } from '@neondatabase/api-client';
1
+ import { EndpointType } from '../utils/api_enums.js';
2
2
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
3
3
  import { writer } from '../writer.js';
4
4
  import { psql } from '../utils/psql.js';
@@ -1,5 +1,4 @@
1
- import { isAxiosError } from 'axios';
2
- import { retryOnLock } from '../api.js';
1
+ import { isNeonApiError, retryOnLock } from '../api.js';
3
2
  import { branchIdFromProps, fillSingleProject, resolveSingleDatabase, } from '../utils/enrichers.js';
4
3
  import { log } from '../log.js';
5
4
  import { writer } from '../writer.js';
@@ -222,7 +221,7 @@ const update = async (props) => {
222
221
  current = data.settings ?? undefined;
223
222
  }
224
223
  catch (err) {
225
- if (isAxiosError(err) && err.response?.status === 404) {
224
+ if (isNeonApiError(err) && err.status === 404) {
226
225
  throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`neonctl data-api create\` first.`);
227
226
  }
228
227
  throw err;
@@ -239,7 +238,7 @@ const update = async (props) => {
239
238
  await retryOnLock(() => props.apiClient.updateProjectBranchDataApi(props.projectId, branchId, database, body));
240
239
  }
241
240
  catch (err) {
242
- if (isAxiosError(err) && err.response?.status === 404) {
241
+ if (isNeonApiError(err) && err.status === 404) {
243
242
  throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`neonctl data-api create\` first.`);
244
243
  }
245
244
  throw err;
@@ -258,7 +257,7 @@ const refreshSchema = async (props) => {
258
257
  await retryOnLock(() => props.apiClient.updateProjectBranchDataApi(props.projectId, branchId, database, {}));
259
258
  }
260
259
  catch (err) {
261
- if (isAxiosError(err) && err.response?.status === 404) {
260
+ if (isNeonApiError(err) && err.status === 404) {
262
261
  throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`neonctl data-api create\` first.`);
263
262
  }
264
263
  throw err;
@@ -277,7 +276,7 @@ const deleteDataApi = async (props) => {
277
276
  await retryOnLock(() => props.apiClient.deleteProjectBranchDataApi(props.projectId, branchId, database));
278
277
  }
279
278
  catch (err) {
280
- if (isAxiosError(err) && err.response?.status === 404) {
279
+ if (isNeonApiError(err) && err.status === 404) {
281
280
  throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}.`);
282
281
  }
283
282
  throw err;
@@ -73,7 +73,10 @@ export const create = async (props) => {
73
73
  export const deleteDb = async (props) => {
74
74
  const branchId = await branchIdFromProps(props);
75
75
  const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchDatabase(props.projectId, branchId, props.database));
76
- writer(props).end(data.database, {
77
- fields: DATABASE_FIELDS,
78
- });
76
+ // A 204 (database already gone) carries no body; only a 200 returns it.
77
+ if (data) {
78
+ writer(props).end(data.database, {
79
+ fields: DATABASE_FIELDS,
80
+ });
81
+ }
79
82
  };
@@ -1,6 +1,6 @@
1
1
  import { existsSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
- import { isAxiosError } from 'axios';
3
+ import { isNeonApiError } from '../api.js';
4
4
  import { retryOnLock } from '../api.js';
5
5
  import { log } from '../log.js';
6
6
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
@@ -58,9 +58,9 @@ const POLL_INTERVAL_MS = Number(process.env.NEON_FUNCTIONS_POLL_INTERVAL_MS) ||
58
58
  // never shows up as current_deployment). Overridable so tests can time out fast;
59
59
  // defaults to 10 minutes in real use.
60
60
  const POLL_TIMEOUT_MS = Number(process.env.NEON_FUNCTIONS_POLL_TIMEOUT_MS) || 600000;
61
- export const command = 'function';
61
+ export const command = 'functions';
62
62
  export const describe = 'Manage Neon Functions';
63
- export const aliases = ['functions'];
63
+ export const aliases = ['function'];
64
64
  export const builder = (argv) => argv
65
65
  .usage('$0 function <sub-command> [options]')
66
66
  .options({
@@ -159,10 +159,8 @@ const emitDeployResult = (props, deployment, fn) => {
159
159
  };
160
160
  // A poll error worth retrying: a network error (no HTTP response), a 5xx, or a
161
161
  // 404 from eventual consistency. Anything else (e.g. 401/403) is surfaced.
162
- const isTransient = (err) => isAxiosError(err) &&
163
- (err.response === undefined ||
164
- err.response.status === 404 ||
165
- err.response.status >= 500);
162
+ const isTransient = (err) => isNeonApiError(err) &&
163
+ (err.status === undefined || err.status === 404 || err.status >= 500);
166
164
  const deploy = async (props) => {
167
165
  if (props.path !== undefined || props.entry !== undefined) {
168
166
  throw new Error('--path and --entry were removed. Use --src <dir>; the entry point ' +
@@ -206,7 +204,7 @@ const deploy = async (props) => {
206
204
  before = fn.current_deployment?.id;
207
205
  }
208
206
  catch (err) {
209
- if (!(isAxiosError(err) && err.response?.status === 404))
207
+ if (!(isNeonApiError(err) && err.status === 404))
210
208
  throw err;
211
209
  }
212
210
  await retryOnLock(() => createDeployment(props.apiClient, props.projectId, branchId, props.slug, {
@@ -341,7 +339,7 @@ const deleteFn = async (props) => {
341
339
  await retryOnLock(() => deleteFunction(props.apiClient, props.projectId, branchId, props.slug));
342
340
  }
343
341
  catch (err) {
344
- if (isAxiosError(err) && err.response?.status === 404) {
342
+ if (isNeonApiError(err) && err.status === 404) {
345
343
  throw new Error(`Function "${props.slug}" not found on branch ${branchId}.`);
346
344
  }
347
345
  throw err;
@@ -19,6 +19,7 @@ import * as neonAuth from './neon_auth.js';
19
19
  import * as functions from './functions.js';
20
20
  import * as dev from './dev.js';
21
21
  import * as config from './config.js';
22
+ import * as status from './status.js';
22
23
  import * as deploy from './deploy.js';
23
24
  import * as env from './env.js';
24
25
  import * as bucket from './bucket.js';
@@ -45,6 +46,7 @@ export default [
45
46
  functions,
46
47
  dev,
47
48
  config,
49
+ status,
48
50
  deploy,
49
51
  env,
50
52
  bucket,
@@ -1,4 +1,4 @@
1
- import { isAxiosError } from 'axios';
1
+ import { isNeonApiError, messageFromBody } from '../api.js';
2
2
  import prompts from 'prompts';
3
3
  import { applyContext, contextBranch, readContextFile, setContext, updateContextFile, } from '../context.js';
4
4
  import { isCi } from '../env.js';
@@ -267,7 +267,7 @@ class LinkInputError extends Error {
267
267
  this.agentCode = agentCode;
268
268
  }
269
269
  }
270
- const httpStatus = (err) => isAxiosError(err) ? err.response?.status : undefined;
270
+ const httpStatus = (err) => isNeonApiError(err) ? err.status : undefined;
271
271
  /**
272
272
  * Fetch a project, turning the common failure modes into clear, actionable
273
273
  * errors. 401 is rethrown so the global handler can refresh credentials;
@@ -803,13 +803,9 @@ const emitAgent = (response) => {
803
803
  // ----------------------------------------------------------------------------
804
804
  const ORG_KEY_LIMITED_FRAGMENT = 'not allowed for organization API keys';
805
805
  const isOrgKeyLimitedError = (err) => {
806
- if (!isAxiosError(err))
806
+ if (!isNeonApiError(err))
807
807
  return false;
808
- const data = err.response?.data;
809
- if (data === undefined || data === null || typeof data !== 'object') {
810
- return false;
811
- }
812
- const message = data.message;
808
+ const message = messageFromBody(err.data);
813
809
  return (typeof message === 'string' && message.includes(ORG_KEY_LIMITED_FRAGMENT));
814
810
  };
815
811
  const fetchOrganizations = async (props) => {
@@ -879,13 +875,10 @@ const toAgentError = (err) => {
879
875
  if (err instanceof LinkInputError) {
880
876
  return { status: 'error', code: err.agentCode, message: err.message };
881
877
  }
882
- if (isAxiosError(err)) {
883
- const status = err.response?.status;
884
- const data = err.response?.data;
885
- const apiMessage = typeof data === 'object' && data !== null
886
- ? data.message
887
- : undefined;
888
- const message = typeof apiMessage === 'string' && apiMessage.length > 0
878
+ if (isNeonApiError(err)) {
879
+ const status = err.status;
880
+ const apiMessage = messageFromBody(err.data);
881
+ const message = apiMessage !== undefined && apiMessage.length > 0
889
882
  ? apiMessage
890
883
  : err.message;
891
884
  let code = 'API_ERROR';
@@ -968,8 +961,8 @@ const fetchRegions = async (props) => {
968
961
  }
969
962
  }
970
963
  catch (err) {
971
- if (isAxiosError(err)) {
972
- log.debug('getActiveRegions failed (%s), falling back to the static region list.', err.response?.status ?? err.code ?? err.message);
964
+ if (isNeonApiError(err)) {
965
+ log.debug('getActiveRegions failed (%s), falling back to the static region list.', err.status ?? err.code ?? err.message);
973
966
  }
974
967
  else {
975
968
  const message = err instanceof Error ? err.message : String(err);
@@ -1,7 +1,6 @@
1
- import { NeonAuthSupportedAuthProvider, NeonAuthOauthProviderId, NeonAuthOauthProviderType, NeonAuthEmailVerificationMethod, } from '@neondatabase/api-client';
2
- import { isAxiosError } from 'axios';
1
+ import { NeonAuthSupportedAuthProvider, NeonAuthOauthProviderId, NeonAuthOauthProviderType, NeonAuthEmailVerificationMethod, } from '../utils/api_enums.js';
3
2
  import chalk from 'chalk';
4
- import { retryOnLock } from '../api.js';
3
+ import { codeFromBody, isNeonApiError, retryOnLock } from '../api.js';
5
4
  import { branchIdFromProps, fillSingleProject } from '../utils/enrichers.js';
6
5
  import { writer } from '../writer.js';
7
6
  // Shared styled output helpers
@@ -453,7 +452,7 @@ const enable = async (props) => {
453
452
  })));
454
453
  }
455
454
  catch (err) {
456
- if (isAxiosError(err) && err.response?.status === 409) {
455
+ if (isNeonApiError(err) && err.status === 409) {
457
456
  alreadyEnabled = true;
458
457
  ({ data } = await props.apiClient.getNeonAuth(props.projectId, branchId));
459
458
  }
@@ -492,7 +491,7 @@ const status = async (props) => {
492
491
  ({ data } = await props.apiClient.getNeonAuth(props.projectId, branchId));
493
492
  }
494
493
  catch (err) {
495
- if (isAxiosError(err) && err.response?.status === 404) {
494
+ if (isNeonApiError(err) && err.status === 404) {
496
495
  printMessage('Neon Auth is not configured for this branch');
497
496
  return;
498
497
  }
@@ -546,9 +545,8 @@ const oauthProviderAdd = async (props) => {
546
545
  }));
547
546
  }
548
547
  catch (err) {
549
- if (isAxiosError(err) &&
550
- err.response?.data?.code ===
551
- 'INVALID_SHARED_OAUTH_PROVIDER') {
548
+ if (isNeonApiError(err) &&
549
+ codeFromBody(err.data) === 'INVALID_SHARED_OAUTH_PROVIDER') {
552
550
  throw new Error(`The "${props.providerId}" provider requires your own OAuth app credentials.\n` +
553
551
  `Re-run with --oauth-client-id and --oauth-client-secret to provide them.\n` +
554
552
  `Create an OAuth app at your provider and use those credentials.`);
@@ -579,9 +577,8 @@ const oauthProviderUpdate = async (props) => {
579
577
  }));
580
578
  }
581
579
  catch (err) {
582
- if (isAxiosError(err) &&
583
- err.response?.data?.code ===
584
- 'INVALID_SHARED_OAUTH_PROVIDER') {
580
+ if (isNeonApiError(err) &&
581
+ codeFromBody(err.data) === 'INVALID_SHARED_OAUTH_PROVIDER') {
585
582
  throw new Error(`The "${props.providerId}" provider requires your own OAuth app credentials.\n` +
586
583
  `Re-run with --oauth-client-id and --oauth-client-secret to provide them.\n` +
587
584
  `Create an OAuth app at your provider and use those credentials.`);
@@ -4,7 +4,7 @@ import { writer } from '../writer.js';
4
4
  import { psql } from '../utils/psql.js';
5
5
  import { updateContextFile } from '../context.js';
6
6
  import { getComputeUnits } from '../utils/compute_units.js';
7
- import { isAxiosError } from 'axios';
7
+ import { isNeonApiError, messageFromBody } from '../api.js';
8
8
  import prompts from 'prompts';
9
9
  import { isCi } from '../env.js';
10
10
  export const PROJECT_FIELDS = [
@@ -315,9 +315,9 @@ const handleMissingOrgId = async (args, cmd) => {
315
315
  }
316
316
  };
317
317
  const isOrgIdError = (err) => {
318
- return (isAxiosError(err) &&
319
- err.response?.status == 400 &&
320
- err.response?.data?.message?.includes('org_id is required'));
318
+ return (isNeonApiError(err) &&
319
+ err.status === 400 &&
320
+ messageFromBody(err.data)?.includes('org_id is required'));
321
321
  };
322
322
  const selectOrg = async (props) => {
323
323
  const { data: { organizations }, } = await props.apiClient.getCurrentUserOrganizations();
@@ -1,4 +1,4 @@
1
- import { EndpointType } from '@neondatabase/api-client';
1
+ import { EndpointType } from '../utils/api_enums.js';
2
2
  import { fillSingleProject } from '../utils/enrichers.js';
3
3
  import { handler as connectionStringHandler, SSL_MODES, } from './connection_string.js';
4
4
  export const command = 'psql [branch]';
@@ -56,7 +56,10 @@ export const create = async (props) => {
56
56
  export const deleteRole = async (props) => {
57
57
  const branchId = await branchIdFromProps(props);
58
58
  const { data } = await retryOnLock(() => props.apiClient.deleteProjectBranchRole(props.projectId, branchId, props.role));
59
- writer(props).end(data.role, {
60
- fields: ROLES_FIELDS,
61
- });
59
+ // A 204 (role already gone) carries no body; only a 200 returns the role.
60
+ if (data) {
61
+ writer(props).end(data.role, {
62
+ fields: ROLES_FIELDS,
63
+ });
64
+ }
62
65
  };
@@ -3,7 +3,7 @@ import chalk from 'chalk';
3
3
  import { writer } from '../writer.js';
4
4
  import { branchIdFromProps } from '../utils/enrichers.js';
5
5
  import { parsePointInTime, } from '../utils/point_in_time.js';
6
- import { isAxiosError } from 'axios';
6
+ import { isNeonApiError, messageFromBody } from '../api.js';
7
7
  import { sendError } from '../analytics.js';
8
8
  import { log } from '../log.js';
9
9
  const COLORS = {
@@ -68,10 +68,9 @@ const fetchSchema = async (pointInTime, database, props) => {
68
68
  return response.data.sql ?? '';
69
69
  }
70
70
  catch (error) {
71
- if (isAxiosError(error)) {
72
- const data = error.response?.data;
71
+ if (isNeonApiError(error)) {
73
72
  sendError(error, 'API_ERROR');
74
- throw new Error(data.message ??
73
+ throw new Error(messageFromBody(error.data) ??
75
74
  `Error while fetching schema for branch ${pointInTime.branchId}`);
76
75
  }
77
76
  throw error;
@@ -0,0 +1,40 @@
1
+ import { fillSingleProject } from '../utils/enrichers.js';
2
+ import { status } from './config.js';
3
+ /**
4
+ * `neon status` is a top-level alias for `neon config status` — the most-reached-for
5
+ * config subcommand. It mirrors that command's options (including `--current-branch`,
6
+ * the offline branch probe) and delegates to the same `status` handler.
7
+ *
8
+ * Because it has a handler but no subcommands, `status` must also be listed in
9
+ * `NO_SUBCOMMANDS_VERBS` (see index.ts) so the help-fallback middleware doesn't
10
+ * intercept a bare `neon status`.
11
+ */
12
+ export const command = 'status';
13
+ export const describe = "Show the branch's live Neon state (alias of `config status`)";
14
+ export const builder = (argv) => argv
15
+ .usage('$0 status [options]')
16
+ .options({
17
+ 'project-id': {
18
+ describe: 'Project ID',
19
+ type: 'string',
20
+ },
21
+ branch: {
22
+ describe: 'Branch ID or name',
23
+ type: 'string',
24
+ },
25
+ 'config-json': {
26
+ describe: "Print only the branch's live config as neon.ts-shaped JSON " +
27
+ '(services + branch tuning + preview), to stdout. Useful for ' +
28
+ 'scripting or copying into a neon.ts.',
29
+ type: 'boolean',
30
+ default: false,
31
+ },
32
+ 'current-branch': {
33
+ describe: 'Print only the linked branch name from the local .neon file ' +
34
+ '(no network). Exits non-zero when no branch is pinned.',
35
+ type: 'boolean',
36
+ default: false,
37
+ },
38
+ })
39
+ .middleware(fillSingleProject);
40
+ export const handler = (args) => status(args);
@@ -8,6 +8,22 @@ import { log } from './log.js';
8
8
  * working.
9
9
  */
10
10
  export const contextBranch = (context) => context.branch ?? context.branchId;
11
+ /**
12
+ * True when the invocation is the offline "current branch" probe:
13
+ * `(config) status --current-branch`. This mode only reads the pinned branch
14
+ * from the local `.neon` file (for shell prompts like starship), so it MUST
15
+ * NOT touch the network — several middlewares (auth, analytics, single-project
16
+ * resolution) consult this to early-return and skip their API calls / login.
17
+ *
18
+ * Gated on the exact command as well as the flag so an accidental
19
+ * `--current-branch` on an unrelated command (e.g. `config plan`, where the flag
20
+ * is undefined but non-strict yargs still parses it) can't silently skip
21
+ * auth/analytics. The probe is only `status` (the top-level alias) or
22
+ * `config status` (`_ = ['config', 'status']`).
23
+ */
24
+ export const isCurrentBranchProbe = (args) => args.currentBranch === true &&
25
+ (args._[0] === 'status' ||
26
+ (args._[0] === 'config' && args._[1] === 'status'));
11
27
  const CONTEXT_FILE = '.neon';
12
28
  const GITIGNORE_FILE = '.gitignore';
13
29
  const wrapWithContextFile = (dir) => resolve(dir, CONTEXT_FILE);