neon 8.2.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 CHANGED
@@ -1488,7 +1488,7 @@ Global options are supported with any Neon CLI command.
1488
1488
 
1489
1489
  - <a id="analytics"></a>`--analytics`
1490
1490
 
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`. 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`.
1492
1492
 
1493
1493
  - <a id="version"></a>`-v, --version`
1494
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";
@@ -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: new Date(props["expires-at"]).toISOString() } : {},
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: props.annotation ? JSON.parse(props.annotation) : void 0
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" ? new Date(props.expiresAt).toISOString() : null;
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,
@@ -9,9 +9,8 @@ import { t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "../_chunks/en
9
9
  import { ENV_PULL_KEY_HELP, ENV_PULL_SERVICES, envKeysForSelection, envPullFlagValue, ownedEnvServiceKeys, parseEnvPullKeys } from "../env_services.js";
10
10
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
11
11
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
12
- import { announceTargetBranch } from "../utils/branch_notice.js";
13
12
  import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-BWqJqBDJ.js";
14
- import { formatPulledEnv } from "./env_output.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
- if (opts.announce) announceTargetBranch(props, branch, "Pulling env from branch");
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("No Neon env variables to pull for this branch (no DATABASE_URL or enabled Auth / Data API).");
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 formatPulledEnv = (outcome, cwd) => {
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 lines = [`Pulled ${count} Neon variable${count === 1 ? "" : "s"} into ${displayPath(outcome.file, cwd)}`];
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 };
@@ -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 { INIT_NEEDS_YES_OR_TERMINAL } from "./plan.js";
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(INIT_NEEDS_YES_OR_TERMINAL);
12
+ throw new Error(INIT_NEEDS_YES);
14
13
  };
15
14
  const assertAgentSetupFlags = (input) => {
16
15
  if (!input.skipAgents) return;
@@ -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.2.0",
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,9 +65,9 @@
64
65
  "yaml": "^2.9.0",
65
66
  "yargs": "17.7.2",
66
67
  "yoctocolors": "^2.1.2",
68
+ "@neon/config": "1.8.6",
67
69
  "@neon/config-runtime": "1.8.0",
68
- "@neon/sdk": "7.0.1",
69
- "@neon/config": "1.8.6"
70
+ "@neon/sdk": "7.0.1"
70
71
  },
71
72
  "optionalDependencies": {
72
73
  "@napi-rs/keyring": "1.3.0",
@@ -100,8 +101,8 @@
100
101
  "typescript": "^5.9.0",
101
102
  "vitest": "^3.0.9",
102
103
  "@neon-internals/cli-core": "0.0.0",
103
- "@neon/e2e-harness": "0.0.0",
104
104
  "@neon-internals/env-core": "0.0.29",
105
+ "@neon/e2e-harness": "0.0.0",
105
106
  "@neon/functions": "0.11.0"
106
107
  },
107
108
  "publishConfig": {