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.
- package/LICENSE.md +178 -0
- package/README.md +33 -1
- package/{analytics.js → dist/analytics.js} +21 -5
- package/dist/api.js +665 -0
- package/dist/cli.js +9 -0
- package/{commands → dist/commands}/auth.js +7 -0
- package/{commands → dist/commands}/branches.js +7 -4
- package/{commands → dist/commands}/bucket.js +69 -37
- package/{commands → dist/commands}/checkout.js +3 -3
- package/{commands → dist/commands}/config.js +22 -0
- package/{commands → dist/commands}/connection_string.js +1 -1
- package/{commands → dist/commands}/data_api.js +5 -6
- package/{commands → dist/commands}/databases.js +6 -3
- package/{commands → dist/commands}/functions.js +7 -9
- package/{commands → dist/commands}/index.js +2 -0
- package/{commands → dist/commands}/link.js +10 -17
- package/{commands → dist/commands}/neon_auth.js +8 -11
- package/{commands → dist/commands}/projects.js +4 -4
- package/{commands → dist/commands}/psql.js +1 -1
- package/{commands → dist/commands}/roles.js +6 -3
- package/{commands → dist/commands}/schema_diff.js +3 -4
- package/dist/commands/status.js +40 -0
- package/{context.js → dist/context.js} +16 -0
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/errors.js +80 -0
- package/{functions_api.js → dist/functions_api.js} +1 -1
- package/{index.js → dist/index.js} +21 -20
- package/{parameters.gen.js → dist/parameters.gen.js} +14 -14
- package/{psql → dist/psql}/cli.js +0 -0
- package/{storage_api.js → dist/storage_api.js} +7 -8
- package/{test_utils → dist/test_utils}/fixtures.js +45 -15
- package/dist/utils/api_enums.js +33 -0
- package/{utils → dist/utils}/branch_picker.js +1 -1
- package/{utils → dist/utils}/enrichers.js +11 -4
- package/package.json +64 -67
- package/api.js +0 -35
- package/cli.js +0 -2
- package/errors.js +0 -17
- /package/{auth.js → dist/auth.js} +0 -0
- /package/{callback.html → dist/callback.html} +0 -0
- /package/{commands → dist/commands}/bootstrap.js +0 -0
- /package/{commands → dist/commands}/deploy.js +0 -0
- /package/{commands → dist/commands}/dev.js +0 -0
- /package/{commands → dist/commands}/env.js +0 -0
- /package/{commands → dist/commands}/init.js +0 -0
- /package/{commands → dist/commands}/ip_allow.js +0 -0
- /package/{commands → dist/commands}/operations.js +0 -0
- /package/{commands → dist/commands}/orgs.js +0 -0
- /package/{commands → dist/commands}/set_context.js +0 -0
- /package/{commands → dist/commands}/user.js +0 -0
- /package/{commands → dist/commands}/vpc_endpoints.js +0 -0
- /package/{config.js → dist/config.js} +0 -0
- /package/{config_format.js → dist/config_format.js} +0 -0
- /package/{dev → dist/dev}/env.js +0 -0
- /package/{dev → dist/dev}/functions.js +0 -0
- /package/{dev → dist/dev}/inputs.js +0 -0
- /package/{dev → dist/dev}/runtime.js +0 -0
- /package/{env.js → dist/env.js} +0 -0
- /package/{env_file.js → dist/env_file.js} +0 -0
- /package/{help.js → dist/help.js} +0 -0
- /package/{log.js → dist/log.js} +0 -0
- /package/{pkg.js → dist/pkg.js} +0 -0
- /package/{psql → dist/psql}/command/cmd_cond.js +0 -0
- /package/{psql → dist/psql}/command/cmd_connect.js +0 -0
- /package/{psql → dist/psql}/command/cmd_copy.js +0 -0
- /package/{psql → dist/psql}/command/cmd_describe.js +0 -0
- /package/{psql → dist/psql}/command/cmd_format.js +0 -0
- /package/{psql → dist/psql}/command/cmd_io.js +0 -0
- /package/{psql → dist/psql}/command/cmd_lo.js +0 -0
- /package/{psql → dist/psql}/command/cmd_meta.js +0 -0
- /package/{psql → dist/psql}/command/cmd_misc.js +0 -0
- /package/{psql → dist/psql}/command/cmd_pipeline.js +0 -0
- /package/{psql → dist/psql}/command/cmd_restrict.js +0 -0
- /package/{psql → dist/psql}/command/cmd_show.js +0 -0
- /package/{psql → dist/psql}/command/dispatch.js +0 -0
- /package/{psql → dist/psql}/command/inputQueue.js +0 -0
- /package/{psql → dist/psql}/command/shared.js +0 -0
- /package/{psql → dist/psql}/complete/filenames.js +0 -0
- /package/{psql → dist/psql}/complete/index.js +0 -0
- /package/{psql → dist/psql}/complete/matcher.js +0 -0
- /package/{psql → dist/psql}/complete/psqlVars.js +0 -0
- /package/{psql → dist/psql}/complete/queries.js +0 -0
- /package/{psql → dist/psql}/complete/rules.js +0 -0
- /package/{psql → dist/psql}/core/common.js +0 -0
- /package/{psql → dist/psql}/core/help.js +0 -0
- /package/{psql → dist/psql}/core/mainloop.js +0 -0
- /package/{psql → dist/psql}/core/prompt.js +0 -0
- /package/{psql → dist/psql}/core/settings.js +0 -0
- /package/{psql → dist/psql}/core/sqlHelp.js +0 -0
- /package/{psql → dist/psql}/core/startup.js +0 -0
- /package/{psql → dist/psql}/core/syncVars.js +0 -0
- /package/{psql → dist/psql}/core/variables.js +0 -0
- /package/{psql → dist/psql}/describe/formatters.js +0 -0
- /package/{psql → dist/psql}/describe/processNamePattern.js +0 -0
- /package/{psql → dist/psql}/describe/queries.js +0 -0
- /package/{psql → dist/psql}/describe/versionGate.js +0 -0
- /package/{psql → dist/psql}/index.js +0 -0
- /package/{psql → dist/psql}/io/history.js +0 -0
- /package/{psql → dist/psql}/io/input.js +0 -0
- /package/{psql → dist/psql}/io/lineEditor/buffer.js +0 -0
- /package/{psql → dist/psql}/io/lineEditor/complete.js +0 -0
- /package/{psql → dist/psql}/io/lineEditor/filename.js +0 -0
- /package/{psql → dist/psql}/io/lineEditor/index.js +0 -0
- /package/{psql → dist/psql}/io/lineEditor/keymap.js +0 -0
- /package/{psql → dist/psql}/io/lineEditor/vt100.js +0 -0
- /package/{psql → dist/psql}/io/pgpass.js +0 -0
- /package/{psql → dist/psql}/io/pgservice.js +0 -0
- /package/{psql → dist/psql}/io/psqlrc.js +0 -0
- /package/{psql → dist/psql}/print/aligned.js +0 -0
- /package/{psql → dist/psql}/print/asciidoc.js +0 -0
- /package/{psql → dist/psql}/print/crosstab.js +0 -0
- /package/{psql → dist/psql}/print/csv.js +0 -0
- /package/{psql → dist/psql}/print/html.js +0 -0
- /package/{psql → dist/psql}/print/json.js +0 -0
- /package/{psql → dist/psql}/print/latex.js +0 -0
- /package/{psql → dist/psql}/print/pager.js +0 -0
- /package/{psql → dist/psql}/print/troff.js +0 -0
- /package/{psql → dist/psql}/print/unaligned.js +0 -0
- /package/{psql → dist/psql}/print/units.js +0 -0
- /package/{psql → dist/psql}/scanner/slash.js +0 -0
- /package/{psql → dist/psql}/scanner/sql.js +0 -0
- /package/{psql → dist/psql}/scanner/stringutils.js +0 -0
- /package/{psql → dist/psql}/types/backslash.js +0 -0
- /package/{psql → dist/psql}/types/connection.js +0 -0
- /package/{psql → dist/psql}/types/index.js +0 -0
- /package/{psql → dist/psql}/types/printer.js +0 -0
- /package/{psql → dist/psql}/types/repl.js +0 -0
- /package/{psql → dist/psql}/types/scanner.js +0 -0
- /package/{psql → dist/psql}/types/settings.js +0 -0
- /package/{psql → dist/psql}/types/variables.js +0 -0
- /package/{psql → dist/psql}/wire/connection.js +0 -0
- /package/{psql → dist/psql}/wire/copy.js +0 -0
- /package/{psql → dist/psql}/wire/notify.js +0 -0
- /package/{psql → dist/psql}/wire/pipeline.js +0 -0
- /package/{psql → dist/psql}/wire/protocol.js +0 -0
- /package/{psql → dist/psql}/wire/sasl.js +0 -0
- /package/{psql → dist/psql}/wire/tls.js +0 -0
- /package/{test_utils → dist/test_utils}/oauth_server.js +0 -0
- /package/{types.js → dist/types.js} +0 -0
- /package/{utils → dist/utils}/auth.js +0 -0
- /package/{utils → dist/utils}/branch_notice.js +0 -0
- /package/{utils → dist/utils}/compute_units.js +0 -0
- /package/{utils → dist/utils}/esbuild.js +0 -0
- /package/{utils → dist/utils}/formats.js +0 -0
- /package/{utils → dist/utils}/middlewares.js +0 -0
- /package/{utils → dist/utils}/point_in_time.js +0 -0
- /package/{utils → dist/utils}/psql.js +0 -0
- /package/{utils → dist/utils}/string.js +0 -0
- /package/{utils → dist/utils}/ui.js +0 -0
- /package/{utils → dist/utils}/zip.js +0 -0
- /package/{writer.js → dist/writer.js} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EndpointType } from '
|
|
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
|
-
|
|
371
|
-
|
|
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
|
|
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 = '
|
|
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 (
|
|
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 (
|
|
335
|
-
const serverMessage = serverErrorMessage(err.
|
|
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 (
|
|
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.
|
|
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 (
|
|
420
|
-
const status = err.
|
|
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
|
|
427
|
-
//
|
|
428
|
-
const serverMessage = serverErrorMessage(err.
|
|
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
|
|
435
|
-
// The presigned URL targets the branch S3 data-plane endpoint
|
|
436
|
-
// this PUT
|
|
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
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
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
|
|
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
|
-
//
|
|
459
|
-
//
|
|
460
|
-
|
|
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 (
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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
|
|
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 {
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
77
|
-
|
|
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 {
|
|
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 = '
|
|
61
|
+
export const command = 'functions';
|
|
62
62
|
export const describe = 'Manage Neon Functions';
|
|
63
|
-
export const aliases = ['
|
|
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) =>
|
|
163
|
-
(err.
|
|
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 (!(
|
|
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 (
|
|
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 {
|
|
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) =>
|
|
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 (!
|
|
806
|
+
if (!isNeonApiError(err))
|
|
807
807
|
return false;
|
|
808
|
-
const
|
|
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 (
|
|
883
|
-
const status = err.
|
|
884
|
-
const
|
|
885
|
-
const
|
|
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 (
|
|
972
|
-
log.debug('getActiveRegions failed (%s), falling back to the static region list.', err.
|
|
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 '
|
|
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 (
|
|
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 (
|
|
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 (
|
|
550
|
-
err.
|
|
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 (
|
|
583
|
-
err.
|
|
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 {
|
|
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 (
|
|
319
|
-
err.
|
|
320
|
-
err.
|
|
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 '
|
|
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
|
-
|
|
60
|
-
|
|
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 {
|
|
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 (
|
|
72
|
-
const data = error.response?.data;
|
|
71
|
+
if (isNeonApiError(error)) {
|
|
73
72
|
sendError(error, 'API_ERROR');
|
|
74
|
-
throw new Error(data
|
|
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);
|