neon 8.1.0 → 8.3.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/README.md +19 -12
- package/dist/analytics.js +26 -0
- package/dist/auth.js +1 -1
- package/dist/commands/api_keys.js +1 -1
- package/dist/commands/auth.js +1 -1
- package/dist/commands/branches.js +13 -4
- package/dist/commands/checkout.js +2 -2
- package/dist/commands/config.js +51 -15
- package/dist/commands/dev.js +3 -3
- package/dist/commands/env.js +5 -6
- package/dist/commands/env_output.js +6 -3
- package/dist/commands/git.js +2 -2
- package/dist/commands/profile.js +6 -2
- package/dist/commands/snapshots.js +1 -7
- package/dist/error_reporting.js +117 -0
- package/dist/errors.js +7 -1
- package/dist/index.js +1 -1
- package/dist/init/copy.js +2 -1
- package/dist/init/mode.js +2 -3
- package/dist/log.js +1 -1
- package/dist/mcp/mint.js +1 -1
- package/dist/retire_credential.js +3 -2
- package/dist/utils/config_diff.js +1 -3
- package/dist/utils/formats.js +7 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -474,11 +474,11 @@ The resolved branch is then written (by name) to the same `.neon` file `link` us
|
|
|
474
474
|
|
|
475
475
|
```bash
|
|
476
476
|
$ neon checkout main --project-id polished-snowflake-12345678
|
|
477
|
-
|
|
477
|
+
Checked out branch br-main-branch-87654321 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
|
|
478
478
|
|
|
479
479
|
$ neon checkout dev --create --project-id polished-snowflake-12345678
|
|
480
|
-
|
|
481
|
-
|
|
480
|
+
Created branch dev (br-dev-branch-12345678).
|
|
481
|
+
Checked out branch br-dev-branch-12345678 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
|
|
482
482
|
|
|
483
483
|
$ cat .neon
|
|
484
484
|
{
|
|
@@ -698,9 +698,9 @@ Each command prints what it changed and the next step:
|
|
|
698
698
|
|
|
699
699
|
```
|
|
700
700
|
$ neon config add function sendemail
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
701
|
+
Created functions/sendemail.ts.
|
|
702
|
+
Updated neon.ts: added functions.sendemail.
|
|
703
|
+
Next: `neon dev` to run it locally, `neon config apply` to deploy.
|
|
704
704
|
```
|
|
705
705
|
|
|
706
706
|
- **Functions** take a slug of 1-20 lowercase letters and digits, the same rule `plan` enforces. The handler file is written first and an existing file is never overwritten, so pointing `--source` at code you already have works. A `.js` `neon.ts` gets a `.js` handler.
|
|
@@ -740,11 +740,18 @@ neon deploy
|
|
|
740
740
|
|
|
741
741
|
The branch is chosen with `--branch <id|name>`; without it the project's default branch is used. The policy is the `neon.ts` in the project directory (next to `.neon`, or the current directory without one); pass `--config <path>` to point at another file.
|
|
742
742
|
|
|
743
|
-
**
|
|
743
|
+
**Confirmations** (also on `deploy`). Two things need a yes before `apply` changes anything: applying to a branch Neon marks as protected, and overriding settings the branch already has (compute, TTL, `protected`, Data API settings, custom-domain targets, and disabling an existing Data API). Adding services and deploying functions is not an override, so on an unprotected branch it never asks. In a terminal, `apply` shows the settings it would override and asks once, default No. Without a terminal (CI, agents, pipes) or with `-o json|yaml`, it exits 1 before changing anything and names the flag to pass:
|
|
744
744
|
|
|
745
|
-
- `--update-existing`
|
|
746
|
-
- `--allow-protected` —
|
|
747
|
-
|
|
745
|
+
- `--update-existing` — override settings that differ from `neon.ts`.
|
|
746
|
+
- `--allow-protected` — apply to a protected branch.
|
|
747
|
+
- `-y`, `--yes` — both.
|
|
748
|
+
|
|
749
|
+
```bash
|
|
750
|
+
neon deploy --env .env.prod --allow-protected # production main is protected, no drift expected
|
|
751
|
+
neon deploy -y # protected branch and drift, no questions
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
**Output**: `status` prints the project, branch, and reverse-engineered config. `plan` / `apply` render a **`git diff`-style report** (matching [`neon diff`](#diff)): service changes (Neon Auth, Data API, buckets, functions) list as green `+` additions, while **branch setting changes** (TTL, `protected`, compute) show grouped under a `~ <branch>` header, one sorted `field → value` line each. When `apply` would override settings already present remotely, it prints them as a sorted **before→after** diff (`current → desired`, old in red / new in green) before asking or, without a terminal, before exiting non-zero. Pass `--output json` (or `--output yaml`) to emit the full machine-readable result (`PushResult`) instead, for piping into other tools or CI.
|
|
748
755
|
|
|
749
756
|
**`config status --current-branch`** (alias `neon status --current-branch`) prints _only_ the branch pinned in the local `.neon` file — no network, no auth, no analytics — and exits non-zero when none is pinned. This behavior lets it safely drive a shell prompt. Example [starship](https://starship.rs) segment:
|
|
750
757
|
|
|
@@ -1342,7 +1349,7 @@ Project proj-in-org
|
|
|
1342
1349
|
|
|
1343
1350
|
napi_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|
1344
1351
|
WARNING: Store this key now: it is not shown again.
|
|
1345
|
-
|
|
1352
|
+
Limited to proj-in-org: it cannot create projects, mint API keys, or read any other project. It can still change and delete everything inside that project.
|
|
1346
1353
|
```
|
|
1347
1354
|
|
|
1348
1355
|
`--org-id` and `--project-id` are mutually exclusive. A project-scoped key *is* an organization key, and its organization is looked up from the project rather than chosen separately. With neither flag you get an account key.
|
|
@@ -1481,7 +1488,7 @@ Global options are supported with any Neon CLI command.
|
|
|
1481
1488
|
|
|
1482
1489
|
- <a id="analytics"></a>`--analytics`
|
|
1483
1490
|
|
|
1484
|
-
Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to `neon ask --prompt`. To opt-out of analytics data collection, specify `--no-analytics` or `--analytics false`.
|
|
1491
|
+
Analytics are enabled by default to gather information about the CLI commands and options that are used by our customers. This data collection assists in offering support, and allows for a better understanding of typical usage patterns so that we can improve user experience. Neon does not collect user-defined data, such as project IDs or command payloads, except the question passed to `neon ask --prompt`. When the CLI fails on an internal bug (a JavaScript `TypeError`, `ReferenceError`, `RangeError`, or `SyntaxError`), it also sends that error to Neon's Sentry: the error type, the message only when it is a standard JavaScript message that names nothing but code (any other message is replaced), the stack trace with file paths cut to the package (`neon/dist/...`), the CLI and Node.js versions, and whether it ran in CI or a coding agent. To opt-out of analytics data collection and error reports, specify `--no-analytics` or `--analytics false`.
|
|
1485
1492
|
|
|
1486
1493
|
- <a id="version"></a>`-v, --version`
|
|
1487
1494
|
|
package/dist/analytics.js
CHANGED
|
@@ -7,6 +7,7 @@ import { getAuthContext } from "./auth_context.js";
|
|
|
7
7
|
import { n as credentialsPath } from "./_chunks/paths-DMq0Lt7a.js";
|
|
8
8
|
import "./config.js";
|
|
9
9
|
import { isCurrentBranchProbe } from "./context.js";
|
|
10
|
+
import { isUnexpectedError } from "./errors.js";
|
|
10
11
|
import { Analytics } from "@segment/analytics-node";
|
|
11
12
|
//#region src/analytics.ts
|
|
12
13
|
const WRITE_KEY = "3SQXn5ejjXWLEJ8xU2PRYhAotLtTaeeV";
|
|
@@ -190,6 +191,7 @@ const analyticsMiddleware = (args) => {
|
|
|
190
191
|
/** How long closing waits for an unfinished account lookup before aborting it. */
|
|
191
192
|
const ATTRIBUTION_WAIT_MS = 1e3;
|
|
192
193
|
let closing;
|
|
194
|
+
const errorReports = [];
|
|
193
195
|
/**
|
|
194
196
|
* Send queued events, then close the client and flush. Later calls share the first close: the
|
|
195
197
|
* psql launcher starts one while psql runs, and a second `closeAndFlush` would only make the SDK
|
|
@@ -198,6 +200,7 @@ let closing;
|
|
|
198
200
|
const closeAnalytics = (opts) => {
|
|
199
201
|
const analytics = client;
|
|
200
202
|
if (!analytics) return Promise.resolve();
|
|
203
|
+
if (closing && opts?.timeout !== void 0) return untilDeadline(closing, Date.now() + opts.timeout);
|
|
201
204
|
if (!closing) {
|
|
202
205
|
const started = Date.now();
|
|
203
206
|
closing = (async () => {
|
|
@@ -215,11 +218,27 @@ const closeAnalytics = (opts) => {
|
|
|
215
218
|
log.debug("Flushing CLI analytics");
|
|
216
219
|
const flush = opts?.timeout === void 0 ? void 0 : { timeout: Math.max(1, opts.timeout - (Date.now() - started)) };
|
|
217
220
|
await analytics.closeAndFlush(flush);
|
|
221
|
+
await settleErrorReports(opts?.timeout === void 0 ? void 0 : started + opts.timeout);
|
|
218
222
|
log.debug("Flushed CLI analytics");
|
|
219
223
|
})();
|
|
220
224
|
}
|
|
221
225
|
return closing;
|
|
222
226
|
};
|
|
227
|
+
const untilDeadline = async (work, deadline) => {
|
|
228
|
+
let timer;
|
|
229
|
+
await Promise.race([work, new Promise((resolve) => {
|
|
230
|
+
timer = setTimeout(resolve, Math.max(0, deadline - Date.now()));
|
|
231
|
+
})]);
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
};
|
|
234
|
+
const settleErrorReports = async (deadline) => {
|
|
235
|
+
const reports = Promise.all(errorReports);
|
|
236
|
+
if (deadline === void 0) {
|
|
237
|
+
await reports;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
await untilDeadline(reports, deadline);
|
|
241
|
+
};
|
|
223
242
|
const getErrorAnalyticsEventContext = (_args) => ({
|
|
224
243
|
version: pkg_default.version,
|
|
225
244
|
ci: isCi(),
|
|
@@ -252,6 +271,13 @@ const sendError = (err, errCode) => {
|
|
|
252
271
|
});
|
|
253
272
|
});
|
|
254
273
|
log.debug("Sent CLI error event: %s", errCode);
|
|
274
|
+
if (errCode === "UNKNOWN_ERROR" && isUnexpectedError(err)) {
|
|
275
|
+
const report = import("./error_reporting.js").then(({ reportUnexpectedError }) => reportUnexpectedError(err, redactCredentialTokenIds)).catch((reportError) => {
|
|
276
|
+
log.debug("Could not report the error to Sentry: %s", reportError);
|
|
277
|
+
});
|
|
278
|
+
errorReports.push(report);
|
|
279
|
+
if (closing) closing = closing.then(() => report);
|
|
280
|
+
}
|
|
255
281
|
};
|
|
256
282
|
const trackEvent = (event, properties) => {
|
|
257
283
|
if (!client) return;
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { log } from "./log.js";
|
|
2
|
-
import { sendError } from "./analytics.js";
|
|
3
2
|
import { matchErrorCode } from "./errors.js";
|
|
3
|
+
import { sendError } from "./analytics.js";
|
|
4
4
|
import { extendTokenSet } from "./utils/auth.js";
|
|
5
5
|
import { createReadStream } from "node:fs";
|
|
6
6
|
import { join } from "node:path";
|
|
@@ -242,7 +242,7 @@ const withdraw = async (props, orgId, keyId) => {
|
|
|
242
242
|
const { data } = orgId ? await props.apiClient.revokeOrgApiKey(orgId, keyId) : await props.apiClient.revokeApiKey(keyId);
|
|
243
243
|
return data.revoked === true && data.id === keyId;
|
|
244
244
|
} catch (err) {
|
|
245
|
-
log.
|
|
245
|
+
log.warning("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
|
|
246
246
|
return false;
|
|
247
247
|
}
|
|
248
248
|
};
|
package/dist/commands/auth.js
CHANGED
|
@@ -451,7 +451,7 @@ const ensureAuth = async (props) => {
|
|
|
451
451
|
}
|
|
452
452
|
if (props._[0] === "me") {
|
|
453
453
|
const state = loaded === null ? "has no stored credential" : "holds a session that has expired";
|
|
454
|
-
|
|
454
|
+
log.info(`Not signed in: profile "${selection.profile}" ${state}. Run \`${getCliName()} login --profile ${selection.profile}\` to sign in, or use an API key with --api-key or NEON_API_KEY.`);
|
|
455
455
|
process.exitCode = 1;
|
|
456
456
|
return;
|
|
457
457
|
}
|
|
@@ -5,7 +5,7 @@ import { contextBranch, readContextFile } from "../context.js";
|
|
|
5
5
|
import { branchCreateRequest } from "../parameters.gen.js";
|
|
6
6
|
import { writer } from "../writer.js";
|
|
7
7
|
import { EndpointType } from "../utils/api_enums.js";
|
|
8
|
-
import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp } from "../utils/formats.js";
|
|
8
|
+
import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, toIso } from "../utils/formats.js";
|
|
9
9
|
import { branchIdFromProps, fillSingleProject, resolveBranchFromProps } from "../utils/enrichers.js";
|
|
10
10
|
import { getComputeUnits } from "../utils/compute_units.js";
|
|
11
11
|
import { psql } from "../utils/psql.js";
|
|
@@ -230,6 +230,14 @@ const list = async (props) => {
|
|
|
230
230
|
})
|
|
231
231
|
});
|
|
232
232
|
};
|
|
233
|
+
const parseAnnotation = (value) => {
|
|
234
|
+
try {
|
|
235
|
+
return JSON.parse(value);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
throw new Error(`Failed to parse --annotation JSON: ${message}`);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
233
241
|
const create = async (props) => {
|
|
234
242
|
const branches = await props.apiClient.listProjectBranches({ projectId: props.projectId }).then(({ data }) => data.branches);
|
|
235
243
|
const parentProps = (() => {
|
|
@@ -245,6 +253,7 @@ const create = async (props) => {
|
|
|
245
253
|
if (!branch) throw new Error(`Branch ${props.parent} not found`);
|
|
246
254
|
return { parent_id: branch.id };
|
|
247
255
|
})();
|
|
256
|
+
const annotationValue = props.annotation ? parseAnnotation(props.annotation) : void 0;
|
|
248
257
|
if (props.schemaOnly) {
|
|
249
258
|
if (!props.compute) throw new Error("Schema-only branches require a compute endpoint");
|
|
250
259
|
if (props.type !== EndpointType.ReadWrite) throw new Error("Schema-only branches require a read-write compute endpoint");
|
|
@@ -254,7 +263,7 @@ const create = async (props) => {
|
|
|
254
263
|
name: props.name,
|
|
255
264
|
...parentProps,
|
|
256
265
|
...props.schemaOnly ? { init_source: "schema-only" } : {},
|
|
257
|
-
...props["expires-at"] ? { expires_at:
|
|
266
|
+
...props["expires-at"] ? { expires_at: toIso(props["expires-at"], "--expires-at") } : {},
|
|
258
267
|
...props.protected !== void 0 ? { protected: props.protected } : {}
|
|
259
268
|
},
|
|
260
269
|
endpoints: props.compute ? [{
|
|
@@ -262,7 +271,7 @@ const create = async (props) => {
|
|
|
262
271
|
suspend_timeout_seconds: props.suspendTimeout === 0 ? void 0 : props.suspendTimeout,
|
|
263
272
|
...props.cu ? getComputeUnits(props.cu) : void 0
|
|
264
273
|
}] : [],
|
|
265
|
-
annotation_value:
|
|
274
|
+
annotation_value: annotationValue
|
|
266
275
|
}));
|
|
267
276
|
if (branches.find((b) => b.id === data.branch.parent_id)?.protected) log.warning("The parent branch is protected; a unique role password has been generated for the new branch.");
|
|
268
277
|
const out = writer(props);
|
|
@@ -437,7 +446,7 @@ const setExpiration = async (props) => {
|
|
|
437
446
|
...props,
|
|
438
447
|
id: props.branchId
|
|
439
448
|
});
|
|
440
|
-
const expiresAt = typeof props.expiresAt === "string" ?
|
|
449
|
+
const expiresAt = typeof props.expiresAt === "string" ? toIso(props.expiresAt, "--expires-at") : null;
|
|
441
450
|
const { data } = await retryOnLock(() => props.apiClient.updateProjectBranch(props.projectId, branchId, { branch: { expires_at: expiresAt } }));
|
|
442
451
|
writer(props).end(data.branch, {
|
|
443
452
|
fields: BRANCH_FIELDS,
|
|
@@ -223,7 +223,7 @@ const resolveBranchId = async (props, projectId, hookCtx) => {
|
|
|
223
223
|
if (props.create) return createCheckoutBranch(props, projectId, ref, branches, hookCtx);
|
|
224
224
|
const missingName = notFoundMessage(ref, branches, { suggestCreate: true });
|
|
225
225
|
if (isCi() || !process.stdout.isTTY) throw new Error(missingName);
|
|
226
|
-
log.
|
|
226
|
+
log.info(missingName);
|
|
227
227
|
const { create } = await prompts({
|
|
228
228
|
type: "confirm",
|
|
229
229
|
name: "create",
|
|
@@ -310,7 +310,7 @@ const resolveProjectId = async (props) => {
|
|
|
310
310
|
if (autoDetected) return autoDetected;
|
|
311
311
|
const missingProjectMessage = `Could not determine which Neon project to check out a branch from. Provide one via the --project-id flag or a .neon file (created by \`${getCliName()} link\` / \`${getCliName()} set-context\`).`;
|
|
312
312
|
if (isCi() || !process.stdout.isTTY) throw new Error(missingProjectMessage);
|
|
313
|
-
log.
|
|
313
|
+
log.info(missingProjectMessage);
|
|
314
314
|
const { runLink } = await prompts({
|
|
315
315
|
type: "confirm",
|
|
316
316
|
name: "runLink",
|
package/dist/commands/config.js
CHANGED
|
@@ -27,7 +27,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
27
27
|
import { dirname, join, relative, resolve } from "node:path";
|
|
28
28
|
import chalk from "chalk";
|
|
29
29
|
import { packagesToStage, previewGaWarningForConfig, resolveConfig } from "@neon/config";
|
|
30
|
-
import { PushConflictError, apply, assertZipWithinLimits, bundleAsIs, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, plan, resolveEsbuildEntry, traceNativePackages, zipFunctionBundle } from "@neon/config-runtime";
|
|
30
|
+
import { PushAbortedError, PushConflictError, apply, assertZipWithinLimits, bundleAsIs, createBranch, describeNativeFinding, enforceLimits, findUndeclaredNativePackages, inspect, isPartialBranchCreateError, plan, resolveEsbuildEntry, traceNativePackages, zipFunctionBundle } from "@neon/config-runtime";
|
|
31
31
|
//#region src/commands/config.ts
|
|
32
32
|
var config_exports = /* @__PURE__ */ __exportAll({
|
|
33
33
|
ConfigInstallFailed: () => ConfigInstallFailed,
|
|
@@ -88,13 +88,18 @@ const envFlag = { env: {
|
|
|
88
88
|
/** Apply-only flags, exported so `deploy` can reuse the exact same surface. */
|
|
89
89
|
const applyFlags = {
|
|
90
90
|
"update-existing": {
|
|
91
|
-
|
|
92
|
-
describe: "Auto-confirm overriding existing remote settings on the branch",
|
|
91
|
+
describe: "Override branch settings that differ from neon.ts without asking",
|
|
93
92
|
type: "boolean",
|
|
94
93
|
default: false
|
|
95
94
|
},
|
|
96
95
|
"allow-protected": {
|
|
97
|
-
describe: "
|
|
96
|
+
describe: "Apply to a branch marked protected on Neon without asking",
|
|
97
|
+
type: "boolean",
|
|
98
|
+
default: false
|
|
99
|
+
},
|
|
100
|
+
yes: {
|
|
101
|
+
alias: "y",
|
|
102
|
+
describe: "Answer yes to every confirmation: --update-existing and --allow-protected",
|
|
98
103
|
type: "boolean",
|
|
99
104
|
default: false
|
|
100
105
|
}
|
|
@@ -754,14 +759,8 @@ const applyCmd = async (props) => {
|
|
|
754
759
|
...branch.parentId ? { parentId: branch.parentId } : {},
|
|
755
760
|
...branch.expiresAt ? { expiresAt: branch.expiresAt } : {}
|
|
756
761
|
};
|
|
757
|
-
await runDeployBeforeHook({
|
|
758
|
-
hooks,
|
|
759
|
-
branch: hookBranch,
|
|
760
|
-
git,
|
|
761
|
-
event,
|
|
762
|
-
cwd
|
|
763
|
-
});
|
|
764
762
|
let result;
|
|
763
|
+
let declined = false;
|
|
765
764
|
try {
|
|
766
765
|
result = await apply(config, {
|
|
767
766
|
projectId: props.projectId,
|
|
@@ -769,15 +768,29 @@ const applyCmd = async (props) => {
|
|
|
769
768
|
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
770
769
|
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
771
770
|
...props.runtimeApi ? { api: props.runtimeApi } : {},
|
|
772
|
-
...props.updateExisting ? { updateExisting: true } : {},
|
|
773
|
-
...props.allowProtected ? { allowProtectedBranch: true } : {},
|
|
771
|
+
...props.updateExisting || props.yes ? { updateExisting: true } : {},
|
|
772
|
+
...props.allowProtected || props.yes ? { allowProtectedBranch: true } : {},
|
|
773
|
+
confirm: async (context) => {
|
|
774
|
+
if (context.overrides.length > 0) reportConflicts(props, context.overrides);
|
|
775
|
+
if (!canPromptForApply(props)) return false;
|
|
776
|
+
const proceed = await askToApply(context);
|
|
777
|
+
declined = !proceed;
|
|
778
|
+
return proceed;
|
|
779
|
+
},
|
|
780
|
+
beforeMutations: () => runDeployBeforeHook({
|
|
781
|
+
hooks,
|
|
782
|
+
branch: hookBranch,
|
|
783
|
+
git,
|
|
784
|
+
event,
|
|
785
|
+
cwd
|
|
786
|
+
}),
|
|
774
787
|
bundleFunction: neonctlBundler
|
|
775
788
|
});
|
|
776
789
|
} catch (err) {
|
|
790
|
+
if (err instanceof PushAbortedError) throw new Error(declined ? `Aborted: nothing was applied to branch "${err.branchName}".` : abortedApplyMessage(err));
|
|
777
791
|
if (err instanceof PushConflictError) {
|
|
778
792
|
reportConflicts(props, err.conflicts);
|
|
779
|
-
|
|
780
|
-
throw new Error(overrideable ? "Branch settings conflict with the policy. Re-run with --update-existing to apply the changes shown above." : "The policy conflicts with remote state. --update-existing will not resolve every conflict shown above.");
|
|
793
|
+
throw new Error("The policy conflicts with remote state. --update-existing will not resolve every conflict shown above.");
|
|
781
794
|
}
|
|
782
795
|
throw err;
|
|
783
796
|
}
|
|
@@ -864,6 +877,29 @@ const reportPushResult = (props, result, mode, services) => {
|
|
|
864
877
|
if (noChanges) log.info(`No changes — branch ${result.branchName} already matches the policy.`);
|
|
865
878
|
out.text(`\nUtilized services: ${services.join(", ")}\n`);
|
|
866
879
|
};
|
|
880
|
+
/** A prompt needs both ends of a terminal, and would corrupt `-o json|yaml` on stdout. */
|
|
881
|
+
const canPromptForApply = (props) => !isCi() && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY) && props.output !== "json" && props.output !== "yaml";
|
|
882
|
+
const askToApply = async (context) => {
|
|
883
|
+
const branch = JSON.stringify(context.branchName);
|
|
884
|
+
const message = context.protectedBranch && context.overrideUpdates ? `Branch ${branch} is protected, and this overrides the settings shown above. Apply anyway?` : context.protectedBranch ? `Branch ${branch} is protected. Apply to it anyway?` : `Override the settings shown above on branch ${branch}?`;
|
|
885
|
+
const { default: prompts } = await import("prompts");
|
|
886
|
+
const { proceed } = await prompts({
|
|
887
|
+
type: "confirm",
|
|
888
|
+
name: "proceed",
|
|
889
|
+
message,
|
|
890
|
+
initial: false
|
|
891
|
+
});
|
|
892
|
+
return proceed === true;
|
|
893
|
+
};
|
|
894
|
+
/** Without a terminal there is no one to ask, so name the flags that answer the question. */
|
|
895
|
+
const abortedApplyMessage = (err) => {
|
|
896
|
+
const branch = JSON.stringify(err.branchName);
|
|
897
|
+
const isProtected = err.reasons.includes("protected-branch");
|
|
898
|
+
const overrides = err.reasons.includes("override-updates");
|
|
899
|
+
if (isProtected && overrides) return `Branch ${branch} is protected and its settings differ from the policy. Re-run with -y (or --allow-protected --update-existing) to apply the changes shown above.`;
|
|
900
|
+
if (isProtected) return `Branch ${branch} is protected. Re-run with --allow-protected (or -y) to apply to it.`;
|
|
901
|
+
return "Branch settings conflict with the policy. Re-run with --update-existing (or -y) to apply the changes shown above.";
|
|
902
|
+
};
|
|
867
903
|
/**
|
|
868
904
|
* Render the branch-setting {@link ConflictReport}s a bare `apply` refused to override (drift
|
|
869
905
|
* without `--update-existing`) as the git-style before→after diff. JSON/YAML output emits the
|
package/dist/commands/dev.js
CHANGED
|
@@ -4,11 +4,11 @@ import { isClaimableEnvTarget } from "../claimable/state.js";
|
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
5
|
import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
6
6
|
import { l as functionBaseUrlKey } from "../_chunks/env-HYNFtxu5.js";
|
|
7
|
+
import { branchIdResolve } from "../utils/enrichers.js";
|
|
8
|
+
import { bundleEntry } from "../utils/esbuild.js";
|
|
7
9
|
import { a as resolveDevEnv } from "../_chunks/env-BWqJqBDJ.js";
|
|
8
10
|
import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
|
|
9
11
|
import { resolveWatchInputs } from "../dev/inputs.js";
|
|
10
|
-
import { branchIdResolve } from "../utils/enrichers.js";
|
|
11
|
-
import { bundleEntry } from "../utils/esbuild.js";
|
|
12
12
|
import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -550,7 +550,7 @@ const reconcileOnce = async (running, replan, ops) => {
|
|
|
550
550
|
try {
|
|
551
551
|
desired = await replan(nextSearchBase(running), keepPortsFromRunning(running));
|
|
552
552
|
} catch (err) {
|
|
553
|
-
log.
|
|
553
|
+
log.warning("neon.ts change ignored: " + (err instanceof Error ? err.message : String(err)) + chalk.dim(" (fix it and save again)"));
|
|
554
554
|
return;
|
|
555
555
|
}
|
|
556
556
|
if (ops.isShuttingDown()) return;
|
package/dist/commands/env.js
CHANGED
|
@@ -7,11 +7,10 @@ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOpt
|
|
|
7
7
|
import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
8
8
|
import { t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "../_chunks/env-HYNFtxu5.js";
|
|
9
9
|
import { ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, ownedEnvServiceKeys, parseEnvPullKeys } from "../env_services.js";
|
|
10
|
-
import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-BWqJqBDJ.js";
|
|
11
10
|
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
12
11
|
import { warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
13
|
-
import {
|
|
14
|
-
import { formatPulledEnv } from "./env_output.js";
|
|
12
|
+
import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-BWqJqBDJ.js";
|
|
13
|
+
import { formatPulledEnv, pulledBranch } from "./env_output.js";
|
|
15
14
|
import { existsSync } from "node:fs";
|
|
16
15
|
import chalk from "chalk";
|
|
17
16
|
//#region src/commands/env.ts
|
|
@@ -135,7 +134,7 @@ const pull = async (props, opts = {}) => {
|
|
|
135
134
|
const selectionEnvKeys = dropped ? dropped.envKeys : props.envKeys ?? [];
|
|
136
135
|
const selectedKeys = props.services !== void 0 || props.envKeys !== void 0 ? envKeysForSelection(selectionServices, selectionEnvKeys) : void 0;
|
|
137
136
|
const branch = opts.branch ?? await resolveBranchRef(props);
|
|
138
|
-
|
|
137
|
+
const namedBranch = opts.announce ? branch : void 0;
|
|
139
138
|
const branchId = branch.branchId;
|
|
140
139
|
const location = {
|
|
141
140
|
cwd,
|
|
@@ -164,7 +163,7 @@ const pull = async (props, opts = {}) => {
|
|
|
164
163
|
});
|
|
165
164
|
const neonVars = pickSelectedVars(pickNeonVars(vars), selectedKeys, props.services !== void 0 ? selectionServices : void 0);
|
|
166
165
|
if (Object.keys(neonVars).length === 0) {
|
|
167
|
-
if (!(dropped !== null && dropped.services.length === 0 && dropped.envKeys.length === 0 && dropped.skipped.length > 0)) log.info(
|
|
166
|
+
if (!(dropped !== null && dropped.services.length === 0 && dropped.envKeys.length === 0 && dropped.skipped.length > 0)) log.info(`No Neon env variables to pull from ${namedBranch ? pulledBranch(namedBranch) : "this branch"}: no DATABASE_URL or enabled Auth / Data API.`);
|
|
168
167
|
return { status: "empty" };
|
|
169
168
|
}
|
|
170
169
|
const { written, removed } = mergeEnvFile(targetPath, neonVars, { managedKeys: managedKeysFor(selectedKeys, unreachedButCurrent(skipped, existingEnv, branchId), existingEnv, props.services !== void 0 ? selectionServices : void 0, props.envKeys !== void 0 ? selectionEnvKeys : void 0) });
|
|
@@ -174,7 +173,7 @@ const pull = async (props, opts = {}) => {
|
|
|
174
173
|
removed,
|
|
175
174
|
file: targetPath,
|
|
176
175
|
...credential && credential.keys.length > 0 ? { credential } : {}
|
|
177
|
-
}, cwd));
|
|
176
|
+
}, cwd, namedBranch));
|
|
178
177
|
if (!fileExisted) ensureGitignored(targetPath);
|
|
179
178
|
const gatewayBaseUrl = neonVars.NEON_AI_GATEWAY_BASE_URL;
|
|
180
179
|
const gatewayToken = neonVars.NEON_AI_GATEWAY_TOKEN;
|
|
@@ -3,6 +3,7 @@ import { u as isFunctionBaseUrlKey } from "../_chunks/env-HYNFtxu5.js";
|
|
|
3
3
|
import { BRANCH_ENV_KEY, ENV_PULL_KEYS, serviceForEnvKey } from "../env_services.js";
|
|
4
4
|
import { isAbsolute, relative } from "node:path";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
|
+
import chalk from "chalk";
|
|
6
7
|
//#region src/commands/env_output.ts
|
|
7
8
|
/** Wide enough for the longest label ("Object Storage") plus a gutter, so blocks align. */
|
|
8
9
|
const LABEL_WIDTH = 16;
|
|
@@ -43,10 +44,12 @@ const groupKeys = (keys) => {
|
|
|
43
44
|
}
|
|
44
45
|
return groups;
|
|
45
46
|
};
|
|
46
|
-
const
|
|
47
|
+
const pulledBranch = (branch) => `branch ${chalk.cyan.bold(branch.branchName)} ${chalk.dim(`(${branch.branchId}${branch.usedDefault ? ", project default" : ""})`)}`;
|
|
48
|
+
const formatPulledEnv = (outcome, cwd, branch) => {
|
|
47
49
|
const fresh = new Set(outcome.credential?.fresh ?? []);
|
|
48
50
|
const count = outcome.written.length;
|
|
49
|
-
const
|
|
51
|
+
const source = branch ? ` from ${pulledBranch(branch)}` : "";
|
|
52
|
+
const lines = [`Pulled ${count} Neon variable${count === 1 ? "" : "s"} into ${displayPath(outcome.file, cwd)}${source}`];
|
|
50
53
|
const groups = groupKeys(outcome.written);
|
|
51
54
|
for (const group of GROUP_ORDER) {
|
|
52
55
|
const keys = groups.get(group);
|
|
@@ -61,4 +64,4 @@ const formatPulledEnv = (outcome, cwd) => {
|
|
|
61
64
|
return `${lines.join("\n")}\n`;
|
|
62
65
|
};
|
|
63
66
|
//#endregion
|
|
64
|
-
export { displayPath, formatPulledEnv, row };
|
|
67
|
+
export { displayPath, formatPulledEnv, pulledBranch, row };
|
package/dist/commands/git.js
CHANGED
|
@@ -110,7 +110,7 @@ const uninstall = (_props) => {
|
|
|
110
110
|
log.info("Removed the neon git post-checkout hook. Git → Neon sync is off.");
|
|
111
111
|
break;
|
|
112
112
|
case "foreign":
|
|
113
|
-
log.
|
|
113
|
+
log.info("Left the existing `post-checkout` hook in place (not managed by neon). Git → Neon sync flag cleared.");
|
|
114
114
|
break;
|
|
115
115
|
case "absent": log.info("No neon git hook was installed. Git → Neon sync flag cleared.");
|
|
116
116
|
}
|
|
@@ -295,7 +295,7 @@ const cleanup = async (props) => {
|
|
|
295
295
|
const orphanNeonNames = new Set(orphaned.map(([, neonBranch]) => neonBranch));
|
|
296
296
|
const branches = (await props.apiClient.listProjectBranches({ projectId: props.projectId })).data.branches;
|
|
297
297
|
const { toDelete, skipped } = partitionBranchesToPrune(branches, orphanNeonNames);
|
|
298
|
-
for (const { name, reason } of skipped) log.
|
|
298
|
+
for (const { name, reason } of skipped) log.info("Keeping Neon branch %s (%s) — never auto-deleted.", name, reason);
|
|
299
299
|
if (toDelete.length === 0) {
|
|
300
300
|
log.info("No orphaned Neon branches to delete.");
|
|
301
301
|
return;
|
package/dist/commands/profile.js
CHANGED
|
@@ -466,7 +466,10 @@ const createByMinting = async (props) => {
|
|
|
466
466
|
});
|
|
467
467
|
warnAboutReach(scope);
|
|
468
468
|
} finally {
|
|
469
|
-
if (minted !== void 0 && !keyIsReachable)
|
|
469
|
+
if (minted !== void 0 && !keyIsReachable) {
|
|
470
|
+
if (await withdrawKey(session, mintedScope, minted.id)) log.info(`Revoked the key that was minted but not stored (id ${minted.id}).`);
|
|
471
|
+
else log.warning(`Minted key ${minted.id} could be neither stored nor revoked, and may still be live. Remove it with: neon api-keys revoke ${minted.id}${mintedScope.orgId ? ` --org-id ${mintedScope.orgId}` : ""}`);
|
|
472
|
+
}
|
|
470
473
|
const revoked = await revokeToken(oauthProps, tokenSet);
|
|
471
474
|
log.info(revoked ? "Signed the browser session back out, so nothing but the key remains." : "Could not sign the browser session back out; it will expire on its own.");
|
|
472
475
|
}
|
|
@@ -604,7 +607,8 @@ const remove = async (props) => {
|
|
|
604
607
|
apiKey: stored.apiKey,
|
|
605
608
|
apiHost: props.apiHost
|
|
606
609
|
});
|
|
607
|
-
|
|
610
|
+
if (await withdrawKey(client, scope, keyId)) log.info(`Revoked the API key (id ${keyId})`);
|
|
611
|
+
else log.warning(`Could not revoke the API key (id ${keyId}) — it may still be live. Remove it with: neon api-keys revoke ${keyId}${scope.orgId ? ` --org-id ${scope.orgId}` : ""}`);
|
|
608
612
|
}
|
|
609
613
|
} else if (stored !== null) {
|
|
610
614
|
const revoked = await revokeTokenSet(stored.tokens, props);
|
|
@@ -2,7 +2,7 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { retryOnLock } from "../api.js";
|
|
4
4
|
import { writer } from "../writer.js";
|
|
5
|
-
import { looksLikeLSN, looksLikeTimestamp } from "../utils/formats.js";
|
|
5
|
+
import { looksLikeLSN, looksLikeTimestamp, toIso } from "../utils/formats.js";
|
|
6
6
|
import { branchIdResolve, fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
7
7
|
import { BRANCH_FIELDS } from "./branches.js";
|
|
8
8
|
//#region src/commands/snapshots.ts
|
|
@@ -152,12 +152,6 @@ const handler = (args) => {
|
|
|
152
152
|
};
|
|
153
153
|
/** Narrow an unknown parsed JSON value to a plain object without type casting. */
|
|
154
154
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
155
|
-
/** Normalize a user-supplied date to an ISO 8601 string, or throw a friendly error. */
|
|
156
|
-
const toIso = (value, flag) => {
|
|
157
|
-
const ms = Date.parse(value);
|
|
158
|
-
if (Number.isNaN(ms)) throw new Error(`Invalid ${flag} value: "${value}". Use an RFC 3339 timestamp, e.g. 2025-12-31T23:59:59Z.`);
|
|
159
|
-
return new Date(ms).toISOString();
|
|
160
|
-
};
|
|
161
155
|
/**
|
|
162
156
|
* Resolve a snapshot from an id **or** a name. Snapshot names are not guaranteed
|
|
163
157
|
* unique, so an id match wins; a name that resolves to more than one snapshot is a
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getCliAgent, isCi } from "./env.js";
|
|
2
|
+
import pkg_default from "./pkg.js";
|
|
3
|
+
import { Scope, createStackParser, createTransport } from "@sentry/core";
|
|
4
|
+
import { ServerRuntimeClient, nodeStackLineParser } from "@sentry/core/server";
|
|
5
|
+
//#region src/error_reporting.ts
|
|
6
|
+
let dsn = "https://d2af98d872a47127826e36635023fcac@o1373725.ingest.us.sentry.io/4512222983290880";
|
|
7
|
+
/** Lets tests send to a local collector; the CLI itself always reports to sentry.io. */
|
|
8
|
+
const useErrorReportingDsnForTests = (testDsn) => {
|
|
9
|
+
dsn = testDsn;
|
|
10
|
+
};
|
|
11
|
+
const FLUSH_TIMEOUT_MS = 2e3;
|
|
12
|
+
const fetchTransport = (options) => createTransport(options, async (request) => {
|
|
13
|
+
const response = await fetch(options.url, {
|
|
14
|
+
method: "POST",
|
|
15
|
+
body: typeof request.body === "string" ? request.body : new Uint8Array(request.body)
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
statusCode: response.status,
|
|
19
|
+
headers: {
|
|
20
|
+
"x-sentry-rate-limits": response.headers.get("x-sentry-rate-limits"),
|
|
21
|
+
"retry-after": response.headers.get("retry-after")
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Stack frames carry absolute paths, which include the user's home directory. Keep the part
|
|
27
|
+
* from the package onward (`neon/dist/index.js`); anything outside `node_modules` keeps only
|
|
28
|
+
* its file name.
|
|
29
|
+
*/
|
|
30
|
+
const packageRelativePath = (path) => {
|
|
31
|
+
const normalized = path.replace(/\\/g, "/").replace(/^file:\/\//, "");
|
|
32
|
+
if (normalized.startsWith("node:")) return normalized;
|
|
33
|
+
const at = normalized.lastIndexOf("/node_modules/");
|
|
34
|
+
if (at >= 0) return normalized.slice(at + 14);
|
|
35
|
+
if (!normalized.includes("/")) return normalized;
|
|
36
|
+
return normalized.slice(normalized.lastIndexOf("/") + 1);
|
|
37
|
+
};
|
|
38
|
+
const scrubFrame = (frame) => {
|
|
39
|
+
if (frame.filename === void 0) return frame;
|
|
40
|
+
const filename = packageRelativePath(frame.filename);
|
|
41
|
+
return {
|
|
42
|
+
...frame,
|
|
43
|
+
filename,
|
|
44
|
+
...frame.abs_path !== void 0 ? { abs_path: packageRelativePath(frame.abs_path) } : {},
|
|
45
|
+
...filename.startsWith("neon/") ? { in_app: true } : {}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* V8 messages whose only variable part is an identifier from the code. Any other message can
|
|
50
|
+
* quote user input (`JSON.parse` echoes what it failed on, Node's argument errors print the
|
|
51
|
+
* received value), so it is replaced rather than sent.
|
|
52
|
+
*/
|
|
53
|
+
const SAFE_MESSAGES = [
|
|
54
|
+
/^Cannot read properties of (undefined|null) \(reading '[\w$]+'\)$/,
|
|
55
|
+
/^Cannot set properties of (undefined|null) \(setting '[\w$]+'\)$/,
|
|
56
|
+
/^[\w$.()]+ is not a function$/,
|
|
57
|
+
/^[\w$.()]+ is not iterable$/,
|
|
58
|
+
/^[\w$]+ is not defined$/,
|
|
59
|
+
/^Cannot access '[\w$]+' before initialization$/,
|
|
60
|
+
/^Assignment to constant variable\.$/,
|
|
61
|
+
/^Invalid time value$/,
|
|
62
|
+
/^Maximum call stack size exceeded$/
|
|
63
|
+
];
|
|
64
|
+
const OMITTED_MESSAGE = "(message omitted)";
|
|
65
|
+
const safeMessage = (message) => SAFE_MESSAGES.some((pattern) => pattern.test(message)) ? message : OMITTED_MESSAGE;
|
|
66
|
+
const stackFrameLines = (error) => {
|
|
67
|
+
const stack = error.stack ?? "";
|
|
68
|
+
const header = error.message ? `${error.name}: ${error.message}` : error.name;
|
|
69
|
+
return (stack.startsWith(header) ? stack.slice(header.length) : stack).split("\n").filter((line) => /^ {4}at /.test(line));
|
|
70
|
+
};
|
|
71
|
+
const sanitizeError = (error, redact) => {
|
|
72
|
+
const message = redact(safeMessage(error.message));
|
|
73
|
+
const sanitized = new Error(message);
|
|
74
|
+
sanitized.name = error.name;
|
|
75
|
+
sanitized.stack = [`${error.name}: ${message}`, ...stackFrameLines(error)].join("\n");
|
|
76
|
+
return sanitized;
|
|
77
|
+
};
|
|
78
|
+
const scrubEvent = (event) => {
|
|
79
|
+
const { server_name: _serverName, ...rest } = event;
|
|
80
|
+
return {
|
|
81
|
+
...rest,
|
|
82
|
+
...event.exception?.values ? { exception: { values: event.exception.values.map((exception) => ({
|
|
83
|
+
...exception,
|
|
84
|
+
...exception.stacktrace?.frames ? { stacktrace: {
|
|
85
|
+
...exception.stacktrace,
|
|
86
|
+
frames: exception.stacktrace.frames.map(scrubFrame)
|
|
87
|
+
} } : {}
|
|
88
|
+
})) } } : {}
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const reportUnexpectedError = async (error, redact) => {
|
|
92
|
+
const client = new ServerRuntimeClient({
|
|
93
|
+
dsn,
|
|
94
|
+
release: `neon@${pkg_default.version}`,
|
|
95
|
+
environment: "production",
|
|
96
|
+
platform: "node",
|
|
97
|
+
runtime: {
|
|
98
|
+
name: "node",
|
|
99
|
+
version: process.version
|
|
100
|
+
},
|
|
101
|
+
stackParser: createStackParser(nodeStackLineParser()),
|
|
102
|
+
integrations: [],
|
|
103
|
+
transport: fetchTransport,
|
|
104
|
+
beforeSend: scrubEvent
|
|
105
|
+
});
|
|
106
|
+
client.init();
|
|
107
|
+
const scope = new Scope();
|
|
108
|
+
scope.setClient(client);
|
|
109
|
+
scope.setTags({
|
|
110
|
+
ci: String(isCi()),
|
|
111
|
+
agent: getCliAgent(process.env) ?? "none"
|
|
112
|
+
});
|
|
113
|
+
client.captureException(sanitizeError(error, redact), void 0, scope);
|
|
114
|
+
await client.flush(FLUSH_TIMEOUT_MS);
|
|
115
|
+
};
|
|
116
|
+
//#endregion
|
|
117
|
+
export { packageRelativePath, reportUnexpectedError, useErrorReportingDsnForTests };
|
package/dist/errors.js
CHANGED
|
@@ -11,6 +11,12 @@ const matchErrorCode = (message) => {
|
|
|
11
11
|
return "UNKNOWN_ERROR";
|
|
12
12
|
};
|
|
13
13
|
/**
|
|
14
|
+
* The CLI reports failures it handles by throwing a plain `Error` with the message to print.
|
|
15
|
+
* One of JavaScript's own error types is a case it did not handle: a bug, or input it never
|
|
16
|
+
* validated.
|
|
17
|
+
*/
|
|
18
|
+
const isUnexpectedError = (err) => err instanceof TypeError || err instanceof ReferenceError || err instanceof RangeError || err instanceof SyntaxError;
|
|
19
|
+
/**
|
|
14
20
|
* The single, human-readable line shown when the CLI couldn't reach the Neon API because
|
|
15
21
|
* of a connection-level failure (DNS, refused/reset connection, offline). It replaces the
|
|
16
22
|
* cryptic `fetch failed` / empty axios message a network blip otherwise surfaces (see
|
|
@@ -67,4 +73,4 @@ const isNetworkError = (err) => {
|
|
|
67
73
|
return false;
|
|
68
74
|
};
|
|
69
75
|
//#endregion
|
|
70
|
-
export { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode };
|
|
76
|
+
export { NETWORK_ERROR_MESSAGE, isNetworkError, isUnexpectedError, matchErrorCode };
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { getAuthContext } from "./auth_context.js";
|
|
|
5
5
|
import { r as defaultDir } from "./_chunks/paths-DMq0Lt7a.js";
|
|
6
6
|
import { ensureConfigDir } from "./config.js";
|
|
7
7
|
import { currentContextFile, enrichFromContext } from "./context.js";
|
|
8
|
-
import { analyticsMiddleware, closeAnalytics, initAnalyticsClientMiddleware, sendError, takeCommandSuccessExtras, trackCommandSuccess } from "./analytics.js";
|
|
9
8
|
import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.js";
|
|
9
|
+
import { analyticsMiddleware, closeAnalytics, initAnalyticsClientMiddleware, sendError, takeCommandSuccessExtras, trackCommandSuccess } from "./analytics.js";
|
|
10
10
|
import { AuthRefreshError, defaultClientID } from "./auth.js";
|
|
11
11
|
import { getCliName } from "./utils/cli_name.js";
|
|
12
12
|
import { ensureAuth } from "./commands/auth.js";
|
package/dist/init/copy.js
CHANGED
|
@@ -49,6 +49,7 @@ const SERVICES_MESSAGE = "Which services should neon.ts declare? (space to toggl
|
|
|
49
49
|
const PACKAGE_MANAGER_MESSAGE = "Which package manager should install the Neon dependencies?";
|
|
50
50
|
const NO_AGENTS_FALLBACK_STATUS = "No coding agents detected. Installing the default Neon skills for Cursor and Codex in this directory.";
|
|
51
51
|
const NO_AGENT_SETUP_CONFLICT = "--no-agent-setup cannot be combined with --agent, --skill, or MCP flags.";
|
|
52
|
+
const INIT_NEEDS_YES = "No interactive terminal. Re-run with -y to set up this directory with Recommended defaults: `neon init -y`. To pick each step instead, run `neon init` in a terminal, or see `neon init --help` for the flag that sets each step.";
|
|
52
53
|
const NON_TTY_LINK_NEEDS_AUTH = "No interactive terminal. Sign in with `neon login`, then re-run, or pass --claimable.";
|
|
53
54
|
const YES_LINK_NEEDS_AUTH = "-y cannot sign in. Sign in with `neon login`, then re-run, or pass --claimable.";
|
|
54
55
|
const MCP_SCOPED_NEEDS_PROJECT = "--mcp-project-scoped requires a linked project. Link a project first, or omit --mcp-project-scoped.";
|
|
@@ -136,4 +137,4 @@ const headingForOutcome = (outcome) => {
|
|
|
136
137
|
}
|
|
137
138
|
};
|
|
138
139
|
//#endregion
|
|
139
|
-
export { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, HEADING_CANCELLED, HEADING_COMPLETE, HEADING_FAILED, HEADING_PENDING, INIT_SUBTITLE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPED_NEEDS_PROJECT, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, NON_TTY_AGENT_SETUP, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, NO_AGENT_SETUP_CONFLICT, PACKAGE_MANAGER_MESSAGE, PROGRESS, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, YES_LINK_NEEDS_AUTH, agentMissingCommandDescription, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, headingForOutcome, installFailedNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, namedAgentsUnavailable, skippedLinkNext, unattendedUnauthedNext };
|
|
140
|
+
export { AGENT_DETECTED_DESCRIPTION, AGENT_MCP_PICKER_MESSAGE, AGENT_MCP_SKIP_MESSAGE, AGENT_OTHER_DESCRIPTION, AGENT_PICKER_MESSAGE, AGENT_PLUGIN_SKIP_MESSAGE, AGENT_SETUP_MESSAGE, AGENT_SETUP_PLUGIN_DESCRIPTION, AGENT_SETUP_PLUGIN_TITLE, AGENT_SETUP_SKILLS_DESCRIPTION, AGENT_SETUP_SKILLS_TITLE, AGENT_SETUP_SKIP_DESCRIPTION, AGENT_SETUP_SKIP_TITLE, AGENT_SKILLS_PICKER_MESSAGE, AGENT_SKILLS_SKIP_MESSAGE, CANCELLED_BODY, CLAIMABLE_ACCOUNT_FLAGS, CLAIMABLE_ALREADY_LINKED, CLAIMABLE_MCP_API_KEY, CLAIMABLE_NO_LINK, CONFIG_CONFIRM_HINT, CONFIG_CONFIRM_MESSAGE, HEADING_CANCELLED, HEADING_COMPLETE, HEADING_FAILED, HEADING_PENDING, INIT_NEEDS_YES, INIT_SUBTITLE, MCP_AUTH_API_KEY_DESCRIPTION, MCP_AUTH_API_KEY_TITLE, MCP_AUTH_MESSAGE, MCP_AUTH_OAUTH_DESCRIPTION, MCP_AUTH_OAUTH_TITLE, MCP_SCOPED_NEEDS_PROJECT, MCP_SCOPE_GLOBAL_DESCRIPTION, MCP_SCOPE_GLOBAL_TITLE, MCP_SCOPE_MESSAGE, MCP_SCOPE_PROJECT_DESCRIPTION, MCP_SCOPE_PROJECT_TITLE, MODE_CUSTOM_DESCRIPTION, MODE_CUSTOM_TITLE, MODE_MESSAGE, MODE_RECOMMENDED_DESCRIPTION, MODE_RECOMMENDED_TITLE, NON_TTY_AGENT_SETUP, NON_TTY_LINK_NEEDS_AUTH, NO_AGENTS_FALLBACK_STATUS, NO_AGENT_SETUP_CONFLICT, PACKAGE_MANAGER_MESSAGE, PROGRESS, PROJECT_SETUP_CLAIMABLE_DESCRIPTION, PROJECT_SETUP_CLAIMABLE_TITLE, PROJECT_SETUP_LINK_DESCRIPTION, PROJECT_SETUP_LINK_TITLE, PROJECT_SETUP_MESSAGE, SERVICES_HINT, SERVICES_MESSAGE, SKILLS_PICKER_MESSAGE, YES_LINK_NEEDS_AUTH, agentMissingCommandDescription, claimableNext, claimableThenServicesNext, envPullFailedNext, existingConfigNext, extraServicesNext, headingForOutcome, installFailedNext, mcpConfigLocationSkipped, mcpConfigLocationUnavailable, namedAgentsUnavailable, skippedLinkNext, unattendedUnauthedNext };
|
package/dist/init/mode.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { NON_TTY_AGENT_SETUP, NO_AGENT_SETUP_CONFLICT } from "./copy.js";
|
|
1
|
+
import { INIT_NEEDS_YES, NON_TTY_AGENT_SETUP, NO_AGENT_SETUP_CONFLICT } from "./copy.js";
|
|
3
2
|
//#region src/init/mode.ts
|
|
4
3
|
const hasInitMcpFlags = (input) => input.mcpConfigLocation !== void 0 || input.mcpAuth !== void 0 || input.mcpProjectScoped === true;
|
|
5
4
|
const hasSkillsFlag = (skills) => skills !== void 0 && skills.length > 0;
|
|
@@ -10,7 +9,7 @@ const resolveInitMode = (input) => {
|
|
|
10
9
|
if (input.yes) return { kind: "recommended" };
|
|
11
10
|
if (hasExistingSetupFlags(input)) return { kind: "custom" };
|
|
12
11
|
if (input.interactive) return { kind: "ask" };
|
|
13
|
-
throw new Error(
|
|
12
|
+
throw new Error(INIT_NEEDS_YES);
|
|
14
13
|
};
|
|
15
14
|
const assertAgentSetupFlags = (input) => {
|
|
16
15
|
if (!input.skipAgents) return;
|
package/dist/log.js
CHANGED
|
@@ -9,7 +9,7 @@ const log = {
|
|
|
9
9
|
process.stderr.write(`WARNING: ${format(...args)}\n`);
|
|
10
10
|
},
|
|
11
11
|
info: (...args) => {
|
|
12
|
-
process.stderr.write(
|
|
12
|
+
process.stderr.write(`${format(...args)}\n`);
|
|
13
13
|
},
|
|
14
14
|
error: (...args) => {
|
|
15
15
|
process.stderr.write(`ERROR: ${format(...args)}\n`);
|
package/dist/mcp/mint.js
CHANGED
|
@@ -14,7 +14,7 @@ const withdraw = async (client, keyId, orgId) => {
|
|
|
14
14
|
const { data } = orgId ? await client.revokeOrgApiKey(orgId, keyId) : await client.revokeApiKey(keyId);
|
|
15
15
|
return data.revoked === true && data.id === keyId;
|
|
16
16
|
} catch (err) {
|
|
17
|
-
log.
|
|
17
|
+
log.warning("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
|
|
18
18
|
return false;
|
|
19
19
|
}
|
|
20
20
|
};
|
|
@@ -57,7 +57,8 @@ const retirePreviousCredential = async (props, name, existing, replacementKey) =
|
|
|
57
57
|
apiKey: existing.apiKey,
|
|
58
58
|
apiHost: props.apiHost
|
|
59
59
|
});
|
|
60
|
-
|
|
60
|
+
if (await withdrawKey(client, existing.scope, existing.keyId)) log.info(`Revoked the key it replaces (id ${existing.keyId})`);
|
|
61
|
+
else log.warning(`Could not revoke the key it replaces (id ${existing.keyId}); it may still be live. Remove it with: neon api-keys revoke ${existing.keyId}`);
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
64
|
const revoked = await revokeTokenSet(existing.tokens, props);
|
|
@@ -70,7 +71,7 @@ const withdrawKey = async (client, scope, keyId) => {
|
|
|
70
71
|
const { data } = scope.orgId ? await client.revokeOrgApiKey(scope.orgId, keyId) : await client.revokeApiKey(keyId);
|
|
71
72
|
return data.revoked === true && data.id === keyId;
|
|
72
73
|
} catch (err) {
|
|
73
|
-
log.
|
|
74
|
+
log.warning("Failed to revoke API key %d: %s", keyId, err instanceof Error ? err.message : String(err));
|
|
74
75
|
return false;
|
|
75
76
|
}
|
|
76
77
|
};
|
|
@@ -130,7 +130,6 @@ const renderAppliedChanges = (changes, title, opts) => {
|
|
|
130
130
|
lines.push(...renderBranchGroups(byBranch, paint));
|
|
131
131
|
return lines.join("\n");
|
|
132
132
|
};
|
|
133
|
-
const isOverrideableConflict = (conflict) => conflict.reason.includes("Pass `updateExisting: true`");
|
|
134
133
|
const CUSTOM_DOMAIN_REASON = /^custom domain "([^"]+)"/;
|
|
135
134
|
const labeledConflictField = (conflict) => {
|
|
136
135
|
if (conflict.field !== "customDomain") return conflict.field;
|
|
@@ -145,8 +144,7 @@ const renderBranchSettingConflicts = (conflicts, opts) => {
|
|
|
145
144
|
const existing = byBranch.get(conflict.identifier) ?? [];
|
|
146
145
|
byBranch.set(conflict.identifier, [...existing, ...expandField(labeledConflictField(conflict), conflict.current, conflict.desired)]);
|
|
147
146
|
}
|
|
148
|
-
|
|
149
|
-
return [paint.title(heading), ...renderBranchGroups(byBranch, paint)].join("\n");
|
|
147
|
+
return [paint.title("Branch settings differ"), ...renderBranchGroups(byBranch, paint)].join("\n");
|
|
150
148
|
};
|
|
151
149
|
const renderCustomDomainFollowup = (result, opts) => {
|
|
152
150
|
const paint = palette(opts.color);
|
package/dist/utils/formats.js
CHANGED
|
@@ -3,9 +3,15 @@ const HAIKU_REGEX = /^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/;
|
|
|
3
3
|
const looksLikeBranchId = (branch) => branch.startsWith("br-") && HAIKU_REGEX.test(branch.substring(3));
|
|
4
4
|
const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
|
|
5
5
|
const looksLikeLSN = (lsn) => LSN_REGEX.test(lsn);
|
|
6
|
+
/** Normalize a user-supplied date to an ISO 8601 string, or throw a friendly error. */
|
|
7
|
+
const toIso = (value, flag) => {
|
|
8
|
+
const ms = Date.parse(value);
|
|
9
|
+
if (Number.isNaN(ms)) throw new Error(`Invalid ${flag} value: "${value}". Use an RFC 3339 timestamp, e.g. 2025-12-31T23:59:59Z.`);
|
|
10
|
+
return new Date(ms).toISOString();
|
|
11
|
+
};
|
|
6
12
|
const looksLikeTimestamp = (timestamp) => {
|
|
7
13
|
if (isNaN(Date.parse(timestamp))) return false;
|
|
8
14
|
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?([+-]\d{2}:\d{2}|Z)$/.test(timestamp);
|
|
9
15
|
};
|
|
10
16
|
//#endregion
|
|
11
|
-
export { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp };
|
|
17
|
+
export { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp, toIso };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
"@clack/prompts": "0.10.1",
|
|
47
47
|
"@hono/node-server": "2.0.4",
|
|
48
48
|
"@segment/analytics-node": "1.3.0",
|
|
49
|
+
"@sentry/core": "11.2.0",
|
|
49
50
|
"add-mcp": "2.0.0",
|
|
50
51
|
"chalk": "5.3.0",
|
|
51
52
|
"chokidar": "5.0.0",
|
|
@@ -64,8 +65,8 @@
|
|
|
64
65
|
"yaml": "^2.9.0",
|
|
65
66
|
"yargs": "17.7.2",
|
|
66
67
|
"yoctocolors": "^2.1.2",
|
|
67
|
-
"@neon/config": "1.8.
|
|
68
|
-
"@neon/config-runtime": "1.
|
|
68
|
+
"@neon/config": "1.8.6",
|
|
69
|
+
"@neon/config-runtime": "1.8.0",
|
|
69
70
|
"@neon/sdk": "7.0.1"
|
|
70
71
|
},
|
|
71
72
|
"optionalDependencies": {
|
|
@@ -100,9 +101,9 @@
|
|
|
100
101
|
"typescript": "^5.9.0",
|
|
101
102
|
"vitest": "^3.0.9",
|
|
102
103
|
"@neon-internals/cli-core": "0.0.0",
|
|
104
|
+
"@neon-internals/env-core": "0.0.29",
|
|
103
105
|
"@neon/e2e-harness": "0.0.0",
|
|
104
|
-
"@neon/functions": "0.11.0"
|
|
105
|
-
"@neon-internals/env-core": "0.0.28"
|
|
106
|
+
"@neon/functions": "0.11.0"
|
|
106
107
|
},
|
|
107
108
|
"publishConfig": {
|
|
108
109
|
"access": "public",
|