neon 8.3.6 → 8.4.1

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.
@@ -0,0 +1,50 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { log } from "../log.js";
3
+ import { retryOnLock } from "../api.js";
4
+ import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
5
+ import { defineConfig } from "@neon/config";
6
+ //#region src/commands/realtime.ts
7
+ var realtime_exports = /* @__PURE__ */ __exportAll({
8
+ builder: () => builder,
9
+ command: () => command,
10
+ describe: () => describe,
11
+ disable: () => disable,
12
+ enable: () => enable,
13
+ handler: () => handler
14
+ });
15
+ /** Request Realtime provisioning or an allowed-origin update for a branch. */
16
+ const enable = async (props) => {
17
+ const realtime = props.allowedOrigins === void 0 ? true : { allowedOrigins: props.allowedOrigins };
18
+ defineConfig({ preview: { realtime } });
19
+ const branchId = await branchIdFromProps(props);
20
+ await retryOnLock(() => props.apiClient.enableProjectBranchRealtime(props.projectId, branchId, props.allowedOrigins === void 0 ? {} : { allowed_origins: [...props.allowedOrigins] }));
21
+ log.info("Realtime enable requested for branch %s.", branchId);
22
+ };
23
+ /** Request Realtime deprovisioning for a branch. */
24
+ const disable = async (props) => {
25
+ const branchId = await branchIdFromProps(props);
26
+ await retryOnLock(() => props.apiClient.disableProjectBranchRealtime(props.projectId, branchId));
27
+ log.info("Realtime disable requested for branch %s.", branchId);
28
+ };
29
+ const command = "realtime";
30
+ const describe = "Manage Neon Realtime for a branch";
31
+ const builder = (argv) => argv.usage("$0 realtime <sub-command> [options]").options({
32
+ "project-id": {
33
+ describe: "Project ID",
34
+ type: "string"
35
+ },
36
+ branch: {
37
+ describe: "Branch ID or name",
38
+ type: "string"
39
+ }
40
+ }).middleware(fillSingleProject).command("enable", "Enable Realtime on a branch", (yargs) => yargs.strict().option("allowed-origin", {
41
+ describe: "Allowed browser origin (repeatable). \"*\" allows every origin",
42
+ type: "array",
43
+ string: true
44
+ }), (args) => enable({
45
+ ...args,
46
+ ...args.allowedOrigin !== void 0 ? { allowedOrigins: args.allowedOrigin } : {}
47
+ })).command("disable", "Disable Realtime on a branch", (yargs) => yargs.strict(), (args) => disable(args));
48
+ const handler = (_args) => {};
49
+ //#endregion
50
+ export { builder, command, describe, disable, enable, handler, realtime_exports as t };
@@ -0,0 +1,247 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { writer } from "../writer.js";
3
+ import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
4
+ import { o as resolveNeonEnvVars } from "../_chunks/env-4WGRW2nw.js";
5
+ import YAML from "yaml";
6
+ //#region src/commands/subscribe.ts
7
+ var subscribe_exports = /* @__PURE__ */ __exportAll({
8
+ builder: () => builder,
9
+ command: () => command,
10
+ describe: () => describe,
11
+ handler: () => handler,
12
+ subscribe: () => subscribe
13
+ });
14
+ const command = "subscribe <query>";
15
+ const describe = "Subscribe to a live SQL query with Neon Realtime";
16
+ const builder = (argv) => argv.usage("$0 subscribe \"<query>\" [options]").positional("query", {
17
+ describe: "SELECT query to subscribe to",
18
+ type: "string"
19
+ }).options({
20
+ "project-id": {
21
+ describe: "Project ID",
22
+ type: "string"
23
+ },
24
+ branch: {
25
+ describe: "Branch ID or name",
26
+ type: "string"
27
+ },
28
+ changes: {
29
+ describe: "Show raw resets and transaction batches instead of full query results",
30
+ type: "boolean",
31
+ default: false
32
+ }
33
+ }).example("$0 subscribe \"SELECT * FROM items\"", "Print the full result whenever it changes").example("$0 subscribe --changes \"SELECT * FROM items\"", "Print raw reset and delta events").epilogue("The subscription runs until interrupted. JSON output is newline-delimited; YAML output uses one document per event.").middleware(fillSingleProject).strict();
34
+ const handler = async (props) => {
35
+ const controller = new AbortController();
36
+ const stop = (exitCode) => {
37
+ controller.abort();
38
+ setImmediate(() => process.exit(exitCode));
39
+ };
40
+ const onSigint = () => stop(130);
41
+ const onSigterm = () => stop(143);
42
+ process.once("SIGINT", onSigint);
43
+ process.once("SIGTERM", onSigterm);
44
+ try {
45
+ await subscribe({
46
+ ...props,
47
+ signal: controller.signal
48
+ });
49
+ } finally {
50
+ process.removeListener("SIGINT", onSigint);
51
+ process.removeListener("SIGTERM", onSigterm);
52
+ }
53
+ };
54
+ /** Resolve Realtime credentials and stream one live SQL query until interrupted. */
55
+ const subscribe = async (props, dependencies = {}) => {
56
+ const resolveBranchId = dependencies.resolveBranchId ?? branchIdFromProps;
57
+ const resolveEnv = dependencies.resolveEnv ?? resolveNeonEnvVars;
58
+ const connect = dependencies.connect ?? connectRealtime;
59
+ const branchId = await resolveBranchId(props);
60
+ const { vars } = await resolveEnv({
61
+ cwd: props.cwd ?? process.cwd(),
62
+ contextFile: props.contextFile,
63
+ projectId: props.projectId,
64
+ branchId,
65
+ apiKey: props.apiKey,
66
+ apiHost: props.apiHost,
67
+ services: ["postgres", "realtime"],
68
+ ...props.runtimeApi ? { api: props.runtimeApi } : {}
69
+ });
70
+ const realtime = await connect({
71
+ url: requiredEnv(vars, "NEON_REALTIME_URL"),
72
+ secret: requiredEnv(vars, "NEON_REALTIME_SECRET"),
73
+ database: await databaseName(requiredEnv(vars, "DATABASE_URL"))
74
+ });
75
+ const render = createRenderer(props);
76
+ let subscription;
77
+ try {
78
+ if (props.changes) {
79
+ subscription = await realtime.subscribeChanges(props.query);
80
+ await consumeChanges(subscription, render, props.signal);
81
+ } else {
82
+ const materialized = await realtime.subscribeMaterialized(props.query);
83
+ subscription = materialized;
84
+ await consumeSnapshots(materialized, render, props.signal);
85
+ }
86
+ } finally {
87
+ subscription?.unsubscribe();
88
+ realtime.close();
89
+ }
90
+ };
91
+ const connectRealtime = async (options) => {
92
+ await ensureWebSocket();
93
+ const { createRealtime, rawSql } = await import("@neon/realtime");
94
+ const realtime = createRealtime({
95
+ url: options.url,
96
+ secret: options.secret,
97
+ db: options.database
98
+ });
99
+ return {
100
+ subscribeMaterialized: (query) => realtime.subscribe(rawSql(query)),
101
+ subscribeChanges: (query) => realtime.subscribe(rawSql(query), { materialize: false }),
102
+ close: () => realtime.close()
103
+ };
104
+ };
105
+ /** Node 20 does not expose WebSocket without an experimental flag. */
106
+ const ensureWebSocket = async () => {
107
+ if (typeof globalThis.WebSocket !== "undefined") return;
108
+ const { WebSocket } = await import("undici");
109
+ Object.defineProperty(globalThis, "WebSocket", {
110
+ configurable: true,
111
+ writable: true,
112
+ value: WebSocket
113
+ });
114
+ };
115
+ const requiredEnv = (vars, key) => {
116
+ const value = vars[key];
117
+ if (value) return value;
118
+ throw new Error(`Could not resolve ${key} for the selected branch.`);
119
+ };
120
+ const databaseName = async (connectionString) => {
121
+ const { parse } = await import("pg-connection-string");
122
+ const database = parse(connectionString, { useLibpqCompat: true }).database;
123
+ if (database) return database;
124
+ throw new Error("DATABASE_URL must include a database name.");
125
+ };
126
+ const createRenderer = (props) => {
127
+ const out = props.out ?? process.stdout;
128
+ let renderedTableFrame = false;
129
+ return (event) => {
130
+ if (props.output === "json") {
131
+ out.write(`${JSON.stringify(event)}\n`);
132
+ return;
133
+ }
134
+ if (props.output === "yaml") {
135
+ out.write(`---\n${YAML.stringify(event)}`);
136
+ return;
137
+ }
138
+ if (renderedTableFrame) out.write("\n");
139
+ renderedTableFrame = true;
140
+ renderTableEvent(out, event);
141
+ };
142
+ };
143
+ const renderTableEvent = (out, event) => {
144
+ if (event.type === "snapshot") {
145
+ const fields = rowFields(event.rows);
146
+ writer({
147
+ output: "table",
148
+ out
149
+ }).end(event.rows, {
150
+ fields,
151
+ humanTitle: "Query result",
152
+ emptyMessage: "Query returned no rows."
153
+ });
154
+ return;
155
+ }
156
+ if (event.type === "reset") {
157
+ const rows = event.rows.map(({ rowId, row }) => ({
158
+ row_id: rowId,
159
+ row
160
+ }));
161
+ writer({
162
+ output: "table",
163
+ out
164
+ }).end(rows, {
165
+ fields: ["row_id", "row"],
166
+ humanTitle: "Reset",
167
+ emptyMessage: "Reset contains no rows."
168
+ });
169
+ return;
170
+ }
171
+ const rows = event.changes.map((change) => ({
172
+ type: change.type,
173
+ row_id: change.rowId,
174
+ row: change.type === "upsert" ? change.row : void 0
175
+ }));
176
+ writer({
177
+ output: "table",
178
+ out
179
+ }).end(rows, {
180
+ fields: [
181
+ "type",
182
+ "row_id",
183
+ "row"
184
+ ],
185
+ humanTitle: `Batch (txids: ${event.txids.join(", ") || "none"})`,
186
+ emptyMessage: "Batch contains no row changes."
187
+ });
188
+ };
189
+ const rowFields = (rows) => {
190
+ const seen = /* @__PURE__ */ new Set();
191
+ for (const row of rows) for (const key of Object.keys(row)) seen.add(key);
192
+ return [...seen];
193
+ };
194
+ const consumeSnapshots = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onReset(() => attempt(fail, () => {
195
+ const rows = subscription.getSnapshot().data;
196
+ if (rows !== void 0) render({
197
+ type: "snapshot",
198
+ rows
199
+ });
200
+ })), subscription.onBatch(() => attempt(fail, () => {
201
+ const rows = subscription.getSnapshot().data;
202
+ if (rows !== void 0) render({
203
+ type: "snapshot",
204
+ rows
205
+ });
206
+ }))], signal);
207
+ const consumeChanges = async (subscription, render, signal) => consumeSubscription(subscription, (fail) => [subscription.onReset((rows) => attempt(fail, () => render({
208
+ type: "reset",
209
+ rows
210
+ }))), subscription.onBatch((changes, batch) => attempt(fail, () => render({
211
+ type: "batch",
212
+ txids: batch.txids,
213
+ changes
214
+ })))], signal);
215
+ const consumeSubscription = (subscription, listenForData, signal) => new Promise((resolve, reject) => {
216
+ let settled = false;
217
+ let detachData = [];
218
+ let detachState = () => void 0;
219
+ const onAbort = () => finish(resolve);
220
+ const finish = (settle, error) => {
221
+ if (settled) return;
222
+ settled = true;
223
+ for (const detach of detachData) detach();
224
+ detachState();
225
+ signal?.removeEventListener("abort", onAbort);
226
+ settle(error);
227
+ };
228
+ const fail = (error) => finish(reject, error);
229
+ const onState = (state) => {
230
+ if (state.status === "error") fail(state.error);
231
+ else if (state.status === "closed") finish(resolve);
232
+ };
233
+ detachData = listenForData(fail);
234
+ detachState = subscription.onStateChange(onState);
235
+ signal?.addEventListener("abort", onAbort, { once: true });
236
+ onState(subscription.getState());
237
+ if (signal?.aborted) onAbort();
238
+ });
239
+ const attempt = (fail, action) => {
240
+ try {
241
+ action();
242
+ } catch (error) {
243
+ fail(error);
244
+ }
245
+ };
246
+ //#endregion
247
+ export { builder, command, describe, handler, subscribe, subscribe_exports as t };
@@ -161,6 +161,52 @@ const setToggle = (parsed, key) => {
161
161
  change: `set ${toggle.path}: false → true`
162
162
  };
163
163
  };
164
+ const realtimeValue = (edit) => edit.enabled && edit.allowedOrigins !== void 0 ? `{ allowedOrigins: ${JSON.stringify(edit.allowedOrigins)} }` : String(edit.enabled);
165
+ /** Set the complete Realtime desired state while preserving an existing settings object. */
166
+ const setRealtime = (parsed, edit) => {
167
+ const desired = realtimeValue(edit);
168
+ const declaration = `realtime: ${desired},`;
169
+ const snippet = [
170
+ "preview: {",
171
+ `${parsed.unit}${declaration}`,
172
+ "},"
173
+ ];
174
+ const previewEntry = findEntry(parsed.root, "preview");
175
+ if (!previewEntry) return {
176
+ source: insertEntry(parsed, parsed.root, snippet, snippet, entryAfter(parsed, "preview")),
177
+ change: `added preview.realtime: ${desired}`
178
+ };
179
+ const preview = objectOf(parsed, previewEntry);
180
+ if (!preview) return unsupported("preview is set to an expression, not an object literal.", snippet);
181
+ const entry = findEntry(preview, "realtime");
182
+ if (!entry) return {
183
+ source: insertEntry(parsed, preview, [declaration], snippet),
184
+ change: `added preview.realtime: ${desired}`
185
+ };
186
+ const current = parsed.source.slice(entry.valueStart, entry.valueEnd);
187
+ if (entry.keyed && current === desired) return { source: parsed.source };
188
+ const object = objectOf(parsed, entry);
189
+ if (edit.enabled && edit.allowedOrigins === void 0 && object) return { source: parsed.source };
190
+ if (edit.enabled && edit.allowedOrigins !== void 0 && object) {
191
+ const origins = JSON.stringify(edit.allowedOrigins);
192
+ const allowedOrigins = findEntry(object, "allowedOrigins");
193
+ if (!allowedOrigins) return {
194
+ source: insertEntry(parsed, object, [`allowedOrigins: ${origins},`], snippet),
195
+ change: `added preview.realtime.allowedOrigins: ${origins}`
196
+ };
197
+ if (!allowedOrigins.keyed) return unsupported("preview.realtime.allowedOrigins is a computed property.", snippet);
198
+ if (parsed.source.slice(allowedOrigins.valueStart, allowedOrigins.valueEnd) === origins) return { source: parsed.source };
199
+ return {
200
+ source: parsed.source.slice(0, allowedOrigins.valueStart) + origins + parsed.source.slice(allowedOrigins.valueEnd),
201
+ change: `set preview.realtime.allowedOrigins to ${origins}`
202
+ };
203
+ }
204
+ if (!entry.keyed || current !== "true" && current !== "false" && !object) return unsupported("preview.realtime is set to an expression, not a boolean or object literal.", snippet);
205
+ return {
206
+ source: parsed.source.slice(0, entry.valueStart) + desired + parsed.source.slice(entry.valueEnd),
207
+ change: `set preview.realtime to ${desired}`
208
+ };
209
+ };
164
210
  /**
165
211
  * Whether `dataApi` verifies a third-party IdP, in which case it does not need Neon Auth. An
166
212
  * `authProvider` that is neither literal is refused rather than assumed to be Neon's.
@@ -205,6 +251,9 @@ const apply = (source, edit) => {
205
251
  steps.push(step);
206
252
  };
207
253
  switch (edit.kind) {
254
+ case "realtime":
255
+ run((parsed) => setRealtime(parsed, edit));
256
+ break;
208
257
  case "service":
209
258
  if (edit.service === "auth") run((parsed) => setToggle(parsed, "auth"));
210
259
  else if (edit.service === "ai-gateway") run((parsed) => setToggle(parsed, "aiGateway"));
@@ -1,5 +1,6 @@
1
1
  import { authoredAiGateway, authoredBuckets, authoredFunctions } from "@neon/config";
2
2
  //#region src/config_services.ts
3
+ const isClaimableService = (service) => service !== "realtime";
3
4
  const isToggleEnabled = (toggle) => {
4
5
  if (toggle === void 0) return false;
5
6
  if (typeof toggle === "boolean") return toggle;
@@ -13,8 +14,10 @@ const declaredNeonServices = (config) => {
13
14
  if (Object.keys(authoredBuckets(config) ?? {}).length > 0) services.push("object-storage");
14
15
  if (Object.keys(authoredFunctions(config) ?? {}).length > 0) services.push("functions");
15
16
  if (isToggleEnabled(authoredAiGateway(config))) services.push("ai-gateway");
17
+ if (config.preview?.realtime !== void 0 && config.preview.realtime !== false) services.push("realtime");
16
18
  return services;
17
19
  };
20
+ const declaredClaimableNeonServices = (config) => declaredNeonServices(config).filter(isClaimableService);
18
21
  const dataApiSettingsToSnake = (settings) => {
19
22
  const out = {};
20
23
  if (settings.dbAggregatesEnabled !== void 0) out.db_aggregates_enabled = settings.dbAggregatesEnabled;
@@ -51,4 +54,4 @@ const claimableDataApiCreateBody = (config) => {
51
54
  return withSettings({ auth_provider: "neon_auth" });
52
55
  };
53
56
  //#endregion
54
- export { claimableDataApiCreateBody, declaredNeonServices };
57
+ export { claimableDataApiCreateBody, declaredClaimableNeonServices, declaredNeonServices, isClaimableService };
@@ -76,11 +76,12 @@ const renderGaServices = (services) => {
76
76
  const renderNeonConfig = (services) => {
77
77
  const auth = services.includes("auth") || services.includes("data-api");
78
78
  const dataApi = services.includes("data-api") ? " dataApi: true,\n" : "";
79
+ const realtimeDeclaration = services.includes("realtime") ? " preview: {\n realtime: true,\n },\n" : "";
79
80
  return `import { defineConfig } from "${CONFIG_PACKAGE}/v1";
80
81
 
81
82
  export default defineConfig({
82
83
  // Declare your Neon services here
83
- auth: ${auth},
84
+ ${realtimeDeclaration} auth: ${auth},
84
85
  ${dataApi}${renderGaServices(services)} // Branch policy: per-branch tuning
85
86
  branch: (branch) => {
86
87
  if (branch.isDefault) {
package/dist/dev/env.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-L2DPzDAK.js";
1
+ import { a as resolveDevEnv, i as dropClaimableUnsupported, n as MissingBranchContextError, o as resolveNeonEnvVars, r as ServiceNotOnBranchError, t as DevEnvMismatchError } from "../_chunks/env-4WGRW2nw.js";
2
2
  export { DevEnvMismatchError, MissingBranchContextError, ServiceNotOnBranchError, dropClaimableUnsupported, resolveDevEnv, resolveNeonEnvVars };
@@ -1,5 +1,5 @@
1
1
  import { NEON_SERVICES } from "./neon_services.js";
2
- import { f as parseFunctionBaseUrlKey, t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "./_chunks/env-XsUZj_Ay.js";
2
+ import { f as parseFunctionBaseUrlKey, t as NEON_ENV_VAR_KEYS, u as isFunctionBaseUrlKey } from "./_chunks/env-BpP3Dqpe.js";
3
3
  //#region src/env_services.ts
4
4
  const ENV_PULL_SERVICES = NEON_SERVICES;
5
5
  const ENV_PULL_KEYS = [
@@ -7,6 +7,7 @@ const ENV_PULL_KEYS = [
7
7
  NEON_ENV_VAR_KEYS.branch.name,
8
8
  ...Object.values(NEON_ENV_VAR_KEYS.auth),
9
9
  ...Object.values(NEON_ENV_VAR_KEYS.dataApi),
10
+ ...Object.values(NEON_ENV_VAR_KEYS.realtime),
10
11
  ...Object.values(NEON_ENV_VAR_KEYS.storage),
11
12
  ...Object.values(NEON_ENV_VAR_KEYS.aiGateway)
12
13
  ];
@@ -16,6 +17,7 @@ const SERVICE_ENV_KEYS = {
16
17
  postgres: Object.values(NEON_ENV_VAR_KEYS.postgres),
17
18
  auth: Object.values(NEON_ENV_VAR_KEYS.auth),
18
19
  "data-api": Object.values(NEON_ENV_VAR_KEYS.dataApi),
20
+ realtime: Object.values(NEON_ENV_VAR_KEYS.realtime),
19
21
  "object-storage": Object.values(NEON_ENV_VAR_KEYS.storage),
20
22
  "ai-gateway": Object.values(NEON_ENV_VAR_KEYS.aiGateway),
21
23
  functions: []
@@ -44,6 +46,8 @@ const ENV_KEY_SERVICE = {
44
46
  NEON_AUTH_BASE_URL: "auth",
45
47
  NEON_AUTH_JWKS_URL: "auth",
46
48
  NEON_DATA_API_URL: "data-api",
49
+ NEON_REALTIME_URL: "realtime",
50
+ NEON_REALTIME_SECRET: "realtime",
47
51
  AWS_ACCESS_KEY_ID: "object-storage",
48
52
  AWS_SECRET_ACCESS_KEY: "object-storage",
49
53
  AWS_ENDPOINT_URL_S3: "object-storage",
package/dist/index.js CHANGED
@@ -12,11 +12,13 @@ import { getCliName } from "./utils/cli_name.js";
12
12
  import { ensureAuth } from "./commands/auth.js";
13
13
  import { recoverFrom401 } from "./auth_recovery.js";
14
14
  import { UsageError, failOnUsageError, isBareParentCommand, showHelp, showUsageErrorHelp } from "./help.js";
15
+ import { stagingFeaturesEnabledForArgv } from "./utils/staging.js";
15
16
  import { lazyCommands, preloadCommand } from "./commands/manifest.js";
16
17
  import { rewriteUnknownAgentArg } from "./init/plan.js";
17
18
  import { recoverFromSSO } from "./sso_recovery.js";
18
19
  import { notifyIfUpdateAvailable } from "./update_notifier.js";
19
20
  import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
21
+ import { collapseRepeatedFlags, declaredOptions } from "./utils/repeated_flags.js";
20
22
  import yargs from "yargs";
21
23
  import { hideBin } from "yargs/helpers";
22
24
  //#region src/index.ts
@@ -28,6 +30,7 @@ const NO_SUBCOMMANDS_VERBS = [
28
30
  "cs",
29
31
  "connection-string",
30
32
  "psql",
33
+ "subscribe",
31
34
  "set-context",
32
35
  "checkout",
33
36
  "link",
@@ -47,7 +50,10 @@ const NO_SUBCOMMANDS_VERBS = [
47
50
  "status"
48
51
  ];
49
52
  let builder = yargs(hideBin(process.argv));
50
- builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command> [options]").parserConfiguration({ "populate--": true }).help().option("output", {
53
+ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command> [options]").parserConfiguration({
54
+ "populate--": true,
55
+ "parse-positional-numbers": false
56
+ }).help().option("output", {
51
57
  alias: "o",
52
58
  group: "Global options:",
53
59
  describe: "Set output format",
@@ -120,6 +126,8 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
120
126
  default: true
121
127
  }
122
128
  }).middleware((args) => {
129
+ collapseRepeatedFlags(args, declaredOptions(builder));
130
+ }, true).middleware((args) => {
123
131
  fillInArgs(args);
124
132
  }, true).middleware(resolveApiKeyFromEnv, true).middleware(initAnalyticsClientMiddleware, true).help(false).group("help", "Global options:").option("help", {
125
133
  describe: "Show help",
@@ -129,7 +137,7 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
129
137
  if (args.help) await showHelp(builder);
130
138
  }, true).middleware(async (args) => {
131
139
  if (args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0]) || args._.length > 1 && isBareParentCommand(builder, args._)) await showHelp(builder);
132
- }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(lazyCommands).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
140
+ }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(lazyCommands({ staging: stagingFeaturesEnabledForArgv(hideBin(process.argv)) })).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(failOnUsageError);
133
141
  async function handleError(msg, err, canRetry) {
134
142
  if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
135
143
  await showHelp(builder);
package/dist/init/run.js CHANGED
@@ -9,6 +9,7 @@ import { getCliName } from "../utils/cli_name.js";
9
9
  import { servicesFlagValue } from "../neon_services.js";
10
10
  import { CONFIG_INIT_SERVICES, parseConfigInitServices } from "../config_template.js";
11
11
  import { hasNeonConfigFile, neonConfigFilename } from "../project.js";
12
+ import { assertRealtimeAvailable } from "../utils/staging.js";
12
13
  import { getAgentDisplayName } from "../mcp/agents.js";
13
14
  import { mcpInstallableAgents } from "../mcp/targets.js";
14
15
  import { missingPluginsCommands, pluginsInstallableAgents } from "../plugins/targets.js";
@@ -183,7 +184,7 @@ const runInit = async (props) => {
183
184
  };
184
185
  const servicesFlag = servicesFlagValue(props.services);
185
186
  if (props.config === false && servicesFlag !== void 0) throw new Error(INIT_CONFIG_SERVICES_CONFLICT);
186
- if (servicesFlag !== void 0) parseConfigInitServices(servicesFlag);
187
+ if (servicesFlag !== void 0 && parseConfigInitServices(servicesFlag).includes("realtime")) assertRealtimeAvailable(props.apiHost);
187
188
  const linkInputs = {
188
189
  ...props.orgId ? { orgId: props.orgId } : {},
189
190
  ...props.projectId ? { projectId: props.projectId } : {},
@@ -23,7 +23,8 @@ const NEON_SERVICES = [
23
23
  "data-api",
24
24
  "functions",
25
25
  "object-storage",
26
- "ai-gateway"
26
+ "ai-gateway",
27
+ "realtime"
27
28
  ];
28
29
  /** How output names each service. */
29
30
  const NEON_SERVICE_LABELS = {
@@ -32,7 +33,8 @@ const NEON_SERVICE_LABELS = {
32
33
  "data-api": "Data API",
33
34
  "object-storage": "Object Storage",
34
35
  functions: "Functions",
35
- "ai-gateway": "AI Gateway"
36
+ "ai-gateway": "AI Gateway",
37
+ realtime: "Realtime"
36
38
  };
37
39
  /**
38
40
  * Spellings that used to be canonical, and the service they now mean. Accepted so a scripted
@@ -73,7 +75,7 @@ const parseServices = (raw, options) => {
73
75
  const unsupported = resolved.filter((name) => !allowed.some((service) => service === name));
74
76
  if (unsupported.length > 0) throw new Error(`${unsupportedMessage(unsupported, flag)} ${supported}`);
75
77
  for (const [used, canonical] of deprecated) onDeprecated?.(used, canonical);
76
- return NEON_SERVICES.filter((service) => allowed.includes(service) && resolved.includes(service));
78
+ return allowed.filter((service) => resolved.includes(service));
77
79
  };
78
80
  /**
79
81
  * The sentences explaining why a selection was refused. A real Neon service this command
@@ -95,6 +95,11 @@ const projectCreateRequest = {
95
95
  description: "The project name. If not specified, the name will be identical to the generated project ID",
96
96
  demandOption: false
97
97
  },
98
+ "project.realtime.allowed_origins": {
99
+ type: "array",
100
+ description: "The browser origins allowed to connect, each `http` or `https` with a host and optional port\nand no path. An empty list, or `*` alone, allows any origin. Omitted keeps the current or\ninherited value.\n",
101
+ demandOption: false
102
+ },
98
103
  "project.branch.name": {
99
104
  type: "string",
100
105
  description: "The default branch name. If not specified, the default branch name, `main`, will be used.\n",
@@ -284,6 +289,11 @@ const branchCreateRequest = {
284
289
  description: "Whether the branch is protected. Protected branches (and their computes) cannot be deleted, archived, or reset, and block deletion of the project. Can be gated by `protected_branches_only` in the IP allowlist. Paid plans only.\n",
285
290
  demandOption: false
286
291
  },
292
+ "branch.realtime.allowed_origins": {
293
+ type: "array",
294
+ description: "The browser origins allowed to connect, each `http` or `https` with a host and optional port\nand no path. An empty list, or `*` alone, allows any origin. Omitted keeps the current or\ninherited value.\n",
295
+ demandOption: false
296
+ },
287
297
  "branch.archived": {
288
298
  type: "boolean",
289
299
  description: "Whether to create the branch in the archived state. When omitted, the branch is created as a normal (non-archived) branch.\n",
@@ -23,6 +23,26 @@ const reserveClosedPort = () => new Promise((resolve, reject) => {
23
23
  });
24
24
  });
25
25
  });
26
+ /** Tokens yargs would turn into numbers. `1.50` and `1e3` do not survive the round trip. */
27
+ const NUMERIC_PSQL_ARGS = [
28
+ "-c",
29
+ "1",
30
+ "-p",
31
+ "5432",
32
+ "-v",
33
+ "1.50",
34
+ "1e3"
35
+ ];
36
+ /** The arguments `mocks/bin/psql` received after the connection URI. */
37
+ const forwardedPsqlArgs = (stdout) => {
38
+ const match = /\{"psql-cli-args":(\[.*?\])\}/.exec(stdout);
39
+ if (!match) throw new Error(`mocks/bin/psql did not run. stdout: ${stdout}`);
40
+ const args = JSON.parse(match[1]);
41
+ if (!Array.isArray(args)) throw new Error(`psql-cli-args is not an array: ${match[1]}`);
42
+ return args.slice(1);
43
+ };
44
+ /** The CLI rejects `--output` given twice with different values, so a test's own wins. */
45
+ const passesOutput = (args) => args.some((arg) => arg === "-o" || arg === "--output" || arg.startsWith("--output="));
26
46
  const test = test$1.extend({
27
47
  runMockServer: async ({}, use) => {
28
48
  let startedServer;
@@ -62,8 +82,7 @@ const test = test$1.extend({
62
82
  const cp = fork(join(process.cwd(), "./dist/index.js"), [
63
83
  "--api-host",
64
84
  apiHost,
65
- "--output",
66
- options.output ?? (options.outputTable ? "table" : "yaml"),
85
+ ...passesOutput(args) ? [] : ["--output", options.output ?? (options.outputTable ? "table" : "yaml")],
67
86
  "--no-analytics",
68
87
  ...options.apiKey === false ? [] : ["--api-key", options.apiKey ?? "test-key"],
69
88
  ...args
@@ -110,4 +129,4 @@ const test = test$1.extend({
110
129
  }
111
130
  });
112
131
  //#endregion
113
- export { test };
132
+ export { NUMERIC_PSQL_ARGS, forwardedPsqlArgs, passesOutput, test };
@@ -78,6 +78,7 @@ const renderBranchGroups = (byBranch, paint) => {
78
78
  const serviceLabel = (identifier, details) => {
79
79
  if (identifier === "auth") return "Neon Auth";
80
80
  if (identifier === "dataApi") return "Data API";
81
+ if (identifier === "realtime") return "Realtime";
81
82
  if (identifier.startsWith("bucket:")) return `bucket ${identifier.slice(7)}`;
82
83
  if (identifier.startsWith("function:")) return `function ${identifier.slice(9)}`;
83
84
  if (identifier.startsWith("domain:")) {
@@ -1,6 +1,7 @@
1
1
  import { log } from "../log.js";
2
2
  import { loadProjectConfig } from "../project.js";
3
- import { s as fetchEnvKeys } from "../_chunks/env-XsUZj_Ay.js";
3
+ import { s as fetchEnvKeys } from "../_chunks/env-BpP3Dqpe.js";
4
+ import { assertPolicyAvailable } from "./staging.js";
4
5
  import { runHook } from "@neon/config-runtime";
5
6
  //#region src/utils/hooks.ts
6
7
  /**
@@ -149,6 +150,7 @@ const runAfter = async (label, run) => {
149
150
  * postgres only, since nothing else is declared enabled).
150
151
  */
151
152
  const resolveHookEnv = async (config, options) => {
153
+ assertPolicyAvailable(config, options.apiHost);
152
154
  try {
153
155
  const resolved = await fetchEnvKeys(config, options, null);
154
156
  return shapeHookEnv(resolved);