neon 4.17.4 → 4.18.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.
package/README.md CHANGED
@@ -736,6 +736,8 @@ neon function domains delete docs.example.com
736
736
 
737
737
  Register prints the record, then an INFO line `CNAME <domain> to <cname_target>`. The domain goes live after that CNAME resolves and a certificate is issued on the first request.
738
738
 
739
+ A `neon.ts` `customDomains` list is applied by `neon config plan`, `neon config apply`, and `neon checkout` only on the project's default branch. Other branches still deploy the function. Set `customDomains` in the `branch` closure to register a hostname on a child. See [`@neon/config` Default-branch-only fields](../config/README.md#default-branch-only-fields).
740
+
739
741
  When a package cannot be bundled — a native addon with no esbuild loader, or an optional peer dependency a library references on an untaken code path — list it in that function's `externalPackages` and the bundler leaves the import alone. `neon dev` honours the same list. It does not make the package resolvable in the deployed archive (there is no `node_modules` next to the bundle), so it only unblocks an import that is never evaluated — a dependency the handler actually calls has to be bundled, and a natively-backed one cannot be. See [`@neon/config`](../config/README.md#unbundleable-dependencies-externalpackages).
740
742
 
741
743
  ## Scaffold a project (`bootstrap`)
package/dist/analytics.js CHANGED
@@ -195,6 +195,30 @@ const trackEvent = (event, properties) => {
195
195
  });
196
196
  log.debug("Sent CLI event: %s", event);
197
197
  };
198
+ let commandSuccessExtras = {};
199
+ const recordCommandSuccessExtras = (patch) => {
200
+ commandSuccessExtras = {
201
+ ...commandSuccessExtras,
202
+ ...patch
203
+ };
204
+ };
205
+ const recordScaffoldedTemplate = (templateId) => {
206
+ recordCommandSuccessExtras({ template: templateId });
207
+ };
208
+ const takeCommandSuccessExtras = () => {
209
+ const extras = commandSuccessExtras;
210
+ commandSuccessExtras = {};
211
+ return extras;
212
+ };
213
+ const commandSuccessProperties = (args) => ({
214
+ ...getAnalyticsEventProperties(args),
215
+ projectId: args.projectId,
216
+ branchId: args.branchId,
217
+ accountId: args.accountId,
218
+ authMethod: args.authMethod,
219
+ authData: args.authData,
220
+ ...takeCommandSuccessExtras()
221
+ });
198
222
  const analyticsCommand = (args) => {
199
223
  const command = args._.join(" ");
200
224
  const raw = args._[0] !== "ask" || typeof args.prompt !== "string" ? command : `${command} ${args.prompt}`;
@@ -209,4 +233,4 @@ const getAnalyticsEventProperties = (args) => ({
209
233
  githubEnvVars: getGithubEnvVars(process.env)
210
234
  });
211
235
  //#endregion
212
- export { analyticsMiddleware, analyticsUserId, closeAnalytics, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, storedCredentialAttribution, telemetryCredential, trackEvent };
236
+ export { analyticsMiddleware, analyticsUserId, closeAnalytics, commandSuccessProperties, getAnalyticsEventProperties, getErrorAnalyticsEventProperties, initAnalyticsClientMiddleware, recordCommandSuccessExtras, recordScaffoldedTemplate, sendError, storedCredentialAttribution, takeCommandSuccessExtras, telemetryCredential, trackEvent };
@@ -1,6 +1,7 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
+ import { recordCommandSuccessExtras, recordScaffoldedTemplate } from "../analytics.js";
4
5
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
5
6
  import { getCliName } from "../utils/cli_name.js";
6
7
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
@@ -114,8 +115,11 @@ const handler = async (props) => {
114
115
  const targetDir = await resolveTargetDir(props, interactive, template);
115
116
  ensureTargetUsable(targetDir, props.force);
116
117
  await scaffold(template, targetDir);
118
+ recordScaffoldedTemplate(template.id);
117
119
  printScaffolded(template, targetDir);
118
- return runPostScaffoldSteps(props, targetDir, interactive, template, named);
120
+ const result = await runPostScaffoldSteps(props, targetDir, interactive, template, named);
121
+ recordCommandSuccessExtras({ agent_setup: result.agentSetup });
122
+ return result;
119
123
  };
120
124
  /**
121
125
  * The template list to choose from. When --template is given we try the
@@ -12,7 +12,7 @@ import { loadEnvFileIntoProcess } from "../env_file.js";
12
12
  import { writer } from "../writer.js";
13
13
  import { assertAiGatewayProvisionable, warnAiGateway } from "../utils/ai_gateway_notice.js";
14
14
  import { announceTargetBranch } from "../utils/branch_notice.js";
15
- import { renderAppliedChanges, renderBranchSettingConflicts } from "../utils/config_diff.js";
15
+ import { renderAppliedChanges, renderBranchSettingConflicts, renderCustomDomainFollowup } from "../utils/config_diff.js";
16
16
  import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
17
17
  import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
18
18
  import { bundleEntry } from "../utils/esbuild.js";
@@ -70,6 +70,7 @@ const neonctlBundler = async (fn) => {
70
70
  assertZipWithinLimits(fn.slug, zip, entries);
71
71
  return zip;
72
72
  };
73
+ const reasonAllowsUpdateExisting = (reason) => reason.includes("Pass `updateExisting: true`");
73
74
  const INSPECT_FIELDS = [
74
75
  "project",
75
76
  "branch",
@@ -381,6 +382,7 @@ const planCmd = async (props) => {
381
382
  });
382
383
  const services = utilizedServices(config);
383
384
  reportPushResult(props, result, "plan", services);
385
+ if (result.conflicts.some((conflict) => conflict.field === "customDomain" && !reasonAllowsUpdateExisting(conflict.reason))) process.exitCode = 1;
384
386
  if (services.includes("AI Gateway")) await warnAiGateway({
385
387
  apiClient: props.apiClient,
386
388
  projectId: props.projectId,
@@ -411,7 +413,8 @@ const applyCmd = async (props) => {
411
413
  } catch (err) {
412
414
  if (err instanceof PushConflictError) {
413
415
  reportConflicts(props, err.conflicts);
414
- throw new Error("Branch settings conflict with the policy. Re-run with --update-existing to apply the changes shown above.");
416
+ const overrideable = err.conflicts.every((c) => reasonAllowsUpdateExisting(c.reason));
417
+ 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.");
415
418
  }
416
419
  throw err;
417
420
  }
@@ -472,9 +475,16 @@ const reportPushResult = (props, result, mode, services) => {
472
475
  }
473
476
  const color = props.color !== false;
474
477
  const out = writer(props);
475
- const noChanges = appliedChanges.length === 0;
478
+ const noChanges = appliedChanges.length === 0 && result.conflicts.length === 0;
476
479
  const appliedText = renderAppliedChanges(appliedChanges, mode === "plan" ? "Planned changes" : "Applied changes", { color });
477
480
  if (appliedText) out.text(`${appliedText}\n`);
481
+ if (result.conflicts.length > 0) {
482
+ const conflictText = renderBranchSettingConflicts(result.conflicts, { color });
483
+ if (conflictText) out.text(`\n${conflictText}\n`);
484
+ for (const conflict of result.conflicts) if (!reasonAllowsUpdateExisting(conflict.reason)) out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
485
+ }
486
+ const followup = renderCustomDomainFollowup(result, { color });
487
+ if (followup) out.text(`\n${followup}\n`);
478
488
  if (functionUrlBySlug.size > 0) {
479
489
  const heading = mode === "plan" ? "Function URLs (after apply)" : "Function URLs";
480
490
  out.text(`\n${isCi() ? heading : chalk.bold(heading)}\n`);
@@ -498,7 +508,7 @@ const reportConflicts = (props, conflicts) => {
498
508
  const out = writer(props);
499
509
  const text = renderBranchSettingConflicts([...conflicts], { color: props.color !== false });
500
510
  if (text) out.text(`${text}\n`);
501
- for (const conflict of conflicts) if (!/updateExisting/i.test(conflict.reason)) out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
511
+ for (const conflict of conflicts) if (!reasonAllowsUpdateExisting(conflict.reason)) out.text(` ! ${conflict.field}: ${conflict.reason}\n`);
502
512
  };
503
513
  /**
504
514
  * Block provisioning the AI Gateway on a Free plan from the `checkout` policy paths, which
@@ -565,11 +575,20 @@ const applyPolicyOnCreate = async (props) => {
565
575
  */
566
576
  const logPolicyResult = (result, opts) => {
567
577
  const changes = result.applied.filter((c) => c.action !== "noop");
568
- if (changes.length === 0) {
578
+ if (changes.length === 0 && result.conflicts.length === 0) {
569
579
  log.info("neon.ts applied — no changes were needed.");
580
+ const followup = renderCustomDomainFollowup(result, opts);
581
+ if (followup) log.info("%s", followup);
570
582
  return;
571
583
  }
572
- log.info("%s", renderAppliedChanges(changes, `neon.ts applied — ${changes.length} change${changes.length === 1 ? "" : "s"}:`, opts));
584
+ if (changes.length > 0) log.info("%s", renderAppliedChanges(changes, `neon.ts applied — ${changes.length} change${changes.length === 1 ? "" : "s"}:`, opts));
585
+ if (result.conflicts.length > 0) {
586
+ const conflictText = renderBranchSettingConflicts(result.conflicts, opts);
587
+ if (conflictText) log.info("%s", conflictText);
588
+ for (const conflict of result.conflicts) if (!reasonAllowsUpdateExisting(conflict.reason)) log.info(" ! %s: %s", conflict.field, conflict.reason);
589
+ }
590
+ const followup = renderCustomDomainFollowup(result, opts);
591
+ if (followup) log.info("%s", followup);
573
592
  };
574
593
  /**
575
594
  * Create a branch **from** the local `neon.ts` policy. Returns `null` when there is no
@@ -1,6 +1,7 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { contextBranch, readContextFile } from "../context.js";
4
+ import { recordCommandSuccessExtras } from "../analytics.js";
4
5
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
5
6
  import { getCliName } from "../utils/cli_name.js";
6
7
  import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
@@ -184,6 +185,12 @@ const noteTemplateKeepsShippedConfig = (config, services) => {
184
185
  if (config === void 0 && services === void 0) return;
185
186
  log.warning(INIT_TEMPLATE_KEEPS_CONFIG);
186
187
  };
188
+ const recordInitSuccess = (kind, agentSetup) => {
189
+ recordCommandSuccessExtras({
190
+ init_kind: kind,
191
+ ...agentSetup !== void 0 ? { agent_setup: agentSetup } : {}
192
+ });
193
+ };
187
194
  const handler = async (props) => {
188
195
  if (props.output === "json" || props.output === "yaml") throw new Error(`\`${getCliName()} init\` does not support --output. The commands it runs print their own output.`);
189
196
  const cwd = props.cwd ?? process.cwd();
@@ -228,6 +235,7 @@ const handler = async (props) => {
228
235
  ...templateChoice.kind === "template" ? { id: templateChoice.id } : {}
229
236
  }, linkInputs));
230
237
  printNestedBootstrapDone(result, cwd, templateChoice.kind === "template" ? templateChoice.id : "default");
238
+ recordInitSuccess("empty-template", result?.agentSetup);
231
239
  return;
232
240
  }
233
241
  if (templateChoice.kind === "ask") {
@@ -242,6 +250,7 @@ const handler = async (props) => {
242
250
  useDefault: false
243
251
  }, linkInputs));
244
252
  printNestedBootstrapDone(result, cwd, picked.template.id);
253
+ recordInitSuccess("empty-template", result?.agentSetup);
245
254
  return;
246
255
  }
247
256
  }
@@ -351,6 +360,7 @@ Run \`${getCliName()} env pull\` once resolved.`, message);
351
360
  ],
352
361
  next: []
353
362
  }));
363
+ recordInitSuccess(templateChoice.kind === "existing" ? "existing" : "empty-skip", agentSetup);
354
364
  };
355
365
  //#endregion
356
366
  export { builder, command, describe, handler, initChildEnv, init_exports as t };
@@ -1,6 +1,7 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { readContextFile } from "../context.js";
4
+ import { recordCommandSuccessExtras } from "../analytics.js";
4
5
  import { getCliName } from "../utils/cli_name.js";
5
6
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
6
7
  import { writer } from "../writer.js";
@@ -217,6 +218,7 @@ const handler = async (props) => {
217
218
  if (minted) log.info("Minted API key %s (id %d, %s). Revoke with: %s", minted.name, minted.id, minted.projectId ? "project" : "account", mintedKeyRevokeCommand(minted));
218
219
  if (plan.auth === "oauth") log.info("The agent will prompt for Neon sign-in on first use.");
219
220
  if (failedAgents.length > 0) throw new Error(`Failed to write Neon MCP config for: ${failedAgents.join(", ")}.`);
221
+ recordCommandSuccessExtras({ scope: plan.scope });
220
222
  };
221
223
  //#endregion
222
224
  export { builder, command, describe, handler, mcp_exports as t };
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
+ import { recordCommandSuccessExtras } from "../analytics.js";
3
4
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
4
5
  import { writer } from "../writer.js";
5
6
  import { noPassthrough } from "../utils/flags.js";
@@ -124,6 +125,7 @@ const handler = async (props) => {
124
125
  });
125
126
  out.end();
126
127
  if (failed.length === 0) {
128
+ recordCommandSuccessExtras({ scope: plan.scope });
127
129
  log.info(plan.scope === "project" ? "Installed the Neon plugin (project)." : "Installed the Neon plugin (user).");
128
130
  return;
129
131
  }
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
+ import { recordCommandSuccessExtras } from "../analytics.js";
3
4
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
4
5
  import { writer } from "../writer.js";
5
6
  import { noPassthrough } from "../utils/flags.js";
@@ -160,6 +161,7 @@ const handler = async (props) => {
160
161
  });
161
162
  out.end();
162
163
  if (failed.length === 0) {
164
+ recordCommandSuccessExtras({ scope: plan.scope });
163
165
  log.info(plan.scope === "project" ? "Wrote skills in this directory." : "Wrote user-level skills.");
164
166
  return;
165
167
  }
@@ -27,17 +27,112 @@ async function withTriggerNotFound(triggerId, branchId, run) {
27
27
  throw err;
28
28
  }
29
29
  }
30
+ const TRIGGER_COMMON_FIELDS = [
31
+ "trigger_id",
32
+ "name",
33
+ "function_slug",
34
+ "function_path",
35
+ "enabled",
36
+ "inherited"
37
+ ];
30
38
  const TRIGGER_FIELDS = [
31
39
  "trigger_id",
32
40
  "name",
41
+ "type",
33
42
  "function_slug",
34
43
  "function_path",
35
44
  "schedule",
45
+ "storage",
36
46
  "enabled",
37
47
  "inherited",
38
48
  "next_run_at"
39
49
  ];
40
- const renderColumns = { schedule: (t) => t.schedule?.cron ?? "" };
50
+ function storageMatch(config) {
51
+ return config.prefix === void 0 ? config.bucket_name : `${config.bucket_name} ${config.prefix}`;
52
+ }
53
+ function triggerTableRow(t) {
54
+ if (t.type === "schedule") return {
55
+ trigger_id: t.trigger_id,
56
+ name: t.name,
57
+ type: t.type,
58
+ function_slug: t.function_slug,
59
+ function_path: t.function_path,
60
+ schedule: t.schedule.cron,
61
+ storage: "",
62
+ enabled: t.enabled,
63
+ inherited: t.inherited,
64
+ next_run_at: t.next_run_at ?? ""
65
+ };
66
+ return {
67
+ trigger_id: t.trigger_id,
68
+ name: t.name,
69
+ type: t.type,
70
+ function_slug: t.function_slug,
71
+ function_path: t.function_path,
72
+ schedule: "",
73
+ storage: storageMatch(t.storage_object_created),
74
+ enabled: t.enabled,
75
+ inherited: t.inherited,
76
+ next_run_at: ""
77
+ };
78
+ }
79
+ function writeTrigger(props, data, extra) {
80
+ if (props.output !== "table") {
81
+ writer(props).end(data, {
82
+ fields: TRIGGER_COMMON_FIELDS,
83
+ ...extra
84
+ });
85
+ return;
86
+ }
87
+ const rows = Array.isArray(data) ? data.map(triggerTableRow) : triggerTableRow(data);
88
+ writer(props).end(rows, {
89
+ fields: TRIGGER_FIELDS,
90
+ ...extra
91
+ });
92
+ }
93
+ function enabledPatch(type, enabled) {
94
+ if (type === "schedule") return {
95
+ type: "schedule",
96
+ enabled
97
+ };
98
+ return {
99
+ type: "storage_object_created",
100
+ enabled
101
+ };
102
+ }
103
+ function updatePatch(type, props) {
104
+ if (type === "schedule") {
105
+ const body = { type: "schedule" };
106
+ if (props["function-slug"] !== void 0) body.function_slug = props["function-slug"];
107
+ if (props.name !== void 0) body.name = props.name;
108
+ if (props.cron !== void 0) body.schedule = { cron: props.cron };
109
+ if (props["function-path"] !== void 0) body.function_path = props["function-path"];
110
+ if (props.enabled !== void 0) body.enabled = props.enabled;
111
+ return body;
112
+ }
113
+ const body = { type: "storage_object_created" };
114
+ if (props["function-slug"] !== void 0) body.function_slug = props["function-slug"];
115
+ if (props.name !== void 0) body.name = props.name;
116
+ if (props["function-path"] !== void 0) body.function_path = props["function-path"];
117
+ if (props.enabled !== void 0) body.enabled = props.enabled;
118
+ return body;
119
+ }
120
+ function noFieldsToUpdateMessage(type) {
121
+ const flags = type === "schedule" ? [
122
+ "--function-slug",
123
+ "--name",
124
+ "--cron",
125
+ "--function-path",
126
+ "--enabled"
127
+ ] : [
128
+ "--function-slug",
129
+ "--name",
130
+ "--function-path",
131
+ "--enabled"
132
+ ];
133
+ const last = flags[flags.length - 1];
134
+ return `No fields to update. Pass at least one of ${flags.slice(0, -1).join(", ")}, or ${last}.`;
135
+ }
41
136
  const command = "triggers";
42
137
  const describe = "Manage function triggers";
43
138
  const aliases = ["trigger"];
@@ -117,19 +212,12 @@ const handler = (args) => {
117
212
  const list = async (props) => {
118
213
  const branchId = await branchIdFromProps(props);
119
214
  const { data } = await props.apiClient.listProjectBranchTriggers(props.projectId, branchId);
120
- writer(props).end(data.triggers, {
121
- fields: TRIGGER_FIELDS,
122
- renderColumns,
123
- emptyMessage: "No triggers found on this branch."
124
- });
215
+ writeTrigger(props, data.triggers, { emptyMessage: "No triggers found on this branch." });
125
216
  };
126
217
  const get = async (props) => {
127
218
  const branchId = await branchIdFromProps(props);
128
219
  const { data } = await withTriggerNotFound(props.id, branchId, () => props.apiClient.getProjectBranchTrigger(props.projectId, branchId, props.id));
129
- writer(props).end(data.trigger, {
130
- fields: TRIGGER_FIELDS,
131
- renderColumns
132
- });
220
+ writeTrigger(props, data.trigger);
133
221
  };
134
222
  const create = async (props) => {
135
223
  const branchId = await branchIdFromProps(props);
@@ -142,36 +230,23 @@ const create = async (props) => {
142
230
  if (props["function-path"] !== void 0) body.function_path = props["function-path"];
143
231
  if (props.enabled !== void 0) body.enabled = props.enabled;
144
232
  const { data } = await retryOnLock(() => props.apiClient.createProjectBranchTrigger(props.projectId, branchId, body));
145
- writer(props).end(data.trigger, {
146
- fields: TRIGGER_FIELDS,
147
- renderColumns
148
- });
233
+ writeTrigger(props, data.trigger);
149
234
  };
150
235
  const update = async (props) => {
151
236
  const branchId = await branchIdFromProps(props);
152
- const body = { type: "schedule" };
153
- if (props["function-slug"] !== void 0) body.function_slug = props["function-slug"];
154
- if (props.name !== void 0) body.name = props.name;
155
- if (props.cron !== void 0) body.schedule = { cron: props.cron };
156
- if (props["function-path"] !== void 0) body.function_path = props["function-path"];
157
- if (props.enabled !== void 0) body.enabled = props.enabled;
158
- if (Object.keys(body).filter((k) => k !== "type").length === 0) throw new Error("No fields to update. Pass at least one of --function-slug, --name, --cron, --function-path, or --enabled.");
237
+ const { data: existing } = await withTriggerNotFound(props.id, branchId, () => props.apiClient.getProjectBranchTrigger(props.projectId, branchId, props.id));
238
+ const trigger = existing.trigger;
239
+ if (props.cron !== void 0 && trigger.type !== "schedule") throw new Error(`Trigger ${props.id} is type ${trigger.type}; --cron applies to schedule triggers.`);
240
+ const body = updatePatch(trigger.type, props);
241
+ if (Object.keys(body).filter((k) => k !== "type").length === 0) throw new Error(noFieldsToUpdateMessage(trigger.type));
159
242
  const { data } = await withTriggerNotFound(props.id, branchId, () => retryOnLock(() => props.apiClient.updateProjectBranchTrigger(props.projectId, branchId, props.id, body)));
160
- writer(props).end(data.trigger, {
161
- fields: TRIGGER_FIELDS,
162
- renderColumns
163
- });
243
+ writeTrigger(props, data.trigger);
164
244
  };
165
245
  const setEnabled = async (props, enabled) => {
166
246
  const branchId = await branchIdFromProps(props);
167
- const { data } = await withTriggerNotFound(props.id, branchId, () => retryOnLock(() => props.apiClient.updateProjectBranchTrigger(props.projectId, branchId, props.id, {
168
- type: "schedule",
169
- enabled
170
- })));
171
- writer(props).end(data.trigger, {
172
- fields: TRIGGER_FIELDS,
173
- renderColumns
174
- });
247
+ const { data: existing } = await withTriggerNotFound(props.id, branchId, () => props.apiClient.getProjectBranchTrigger(props.projectId, branchId, props.id));
248
+ const { data } = await withTriggerNotFound(props.id, branchId, () => retryOnLock(() => props.apiClient.updateProjectBranchTrigger(props.projectId, branchId, props.id, enabledPatch(existing.trigger.type, enabled))));
249
+ writeTrigger(props, data.trigger);
175
250
  };
176
251
  const deleteTrigger = async (props) => {
177
252
  const branchId = await branchIdFromProps(props);
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ 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, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent } from "./analytics.js";
8
+ import { analyticsMiddleware, closeAnalytics, commandSuccessProperties, initAnalyticsClientMiddleware, sendError, takeCommandSuccessExtras, trackEvent } from "./analytics.js";
9
9
  import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.js";
10
10
  import { AuthRefreshError, defaultClientID } from "./auth.js";
11
11
  import { ensureAuth } from "./commands/auth.js";
@@ -187,14 +187,7 @@ async function handleError(msg, err, canRetry) {
187
187
  const MAX_ATTEMPTS = 2;
188
188
  while (attempts < MAX_ATTEMPTS) try {
189
189
  const args = await builder.argv;
190
- trackEvent("cli_command_success", {
191
- ...getAnalyticsEventProperties(args),
192
- projectId: args.projectId,
193
- branchId: args.branchId,
194
- accountId: args.accountId,
195
- authMethod: args.authMethod,
196
- authData: args.authData
197
- });
190
+ trackEvent("cli_command_success", commandSuccessProperties(args));
198
191
  if (args._.length === 0 || args.help) {
199
192
  await showHelp(builder);
200
193
  process.exit(0);
@@ -202,6 +195,7 @@ async function handleError(msg, err, canRetry) {
202
195
  await closeAnalytics();
203
196
  break;
204
197
  } catch (err) {
198
+ takeCommandSuccessExtras();
205
199
  attempts++;
206
200
  if (!await handleError("", err, attempts < MAX_ATTEMPTS) || attempts >= MAX_ATTEMPTS) {
207
201
  await closeAnalytics();
@@ -75,11 +75,19 @@ const renderBranchGroups = (byBranch, paint) => {
75
75
  return lines;
76
76
  };
77
77
  /** Friendly label for a service change identifier (`bucket:x` → `bucket x`). */
78
- const serviceLabel = (identifier) => {
78
+ const serviceLabel = (identifier, details) => {
79
79
  if (identifier === "auth") return "Neon Auth";
80
80
  if (identifier === "dataApi") return "Data API";
81
81
  if (identifier.startsWith("bucket:")) return `bucket ${identifier.slice(7)}`;
82
82
  if (identifier.startsWith("function:")) return `function ${identifier.slice(9)}`;
83
+ if (identifier.startsWith("domain:")) {
84
+ const domain = identifier.slice(7);
85
+ const previous = typeof details?.previousSlug === "string" ? details.previousSlug : void 0;
86
+ const slug = typeof details?.slug === "string" ? details.slug : void 0;
87
+ if (previous !== void 0 && slug !== void 0) return `domain ${domain}: ${previous} → ${slug}`;
88
+ if (slug !== void 0) return `domain ${domain} -> ${slug}`;
89
+ return `domain ${domain}`;
90
+ }
83
91
  return identifier;
84
92
  };
85
93
  /**
@@ -110,7 +118,7 @@ const renderAppliedChanges = (changes, title, opts) => {
110
118
  const lines = [paint.title(title)];
111
119
  const services = changes.filter((c) => c.kind === "service").sort((a, b) => a.identifier.localeCompare(b.identifier));
112
120
  for (const service of services) {
113
- const label = serviceLabel(service.identifier);
121
+ const label = serviceLabel(service.identifier, service.details);
114
122
  const line = service.action === "create" ? `+ ${label}` : service.action === "delete" ? `- ${label}` : `~ ${label}`;
115
123
  lines.push(` ${service.action === "create" ? paint.added(line) : service.action === "delete" ? paint.removed(line) : paint.arrow(line)}`);
116
124
  }
@@ -122,22 +130,39 @@ const renderAppliedChanges = (changes, title, opts) => {
122
130
  lines.push(...renderBranchGroups(byBranch, paint));
123
131
  return lines.join("\n");
124
132
  };
125
- /**
126
- * Render branch-setting **conflicts** (drift the policy wants to change but that
127
- * needs `--update-existing`) as a `git diff`-style before→after report: grouped
128
- * per branch, sorted by field, `current → desired` with the old value in red and
129
- * the new in green. Conflicts already carry both sides, so this is the fullest
130
- * form of the diff. Returns "" when there are no conflicts.
131
- */
133
+ const isOverrideableConflict = (conflict) => conflict.reason.includes("Pass `updateExisting: true`");
134
+ const CUSTOM_DOMAIN_REASON = /^custom domain "([^"]+)"/;
135
+ const labeledConflictField = (conflict) => {
136
+ if (conflict.field !== "customDomain") return conflict.field;
137
+ const match = CUSTOM_DOMAIN_REASON.exec(conflict.reason);
138
+ return match?.[1] !== void 0 ? `customDomain ${match[1]}` : conflict.field;
139
+ };
132
140
  const renderBranchSettingConflicts = (conflicts, opts) => {
133
141
  if (conflicts.length === 0) return "";
134
142
  const paint = palette(opts.color);
135
143
  const byBranch = /* @__PURE__ */ new Map();
136
144
  for (const conflict of conflicts) {
137
145
  const existing = byBranch.get(conflict.identifier) ?? [];
138
- byBranch.set(conflict.identifier, [...existing, ...expandField(conflict.field, conflict.current, conflict.desired)]);
146
+ byBranch.set(conflict.identifier, [...existing, ...expandField(labeledConflictField(conflict), conflict.current, conflict.desired)]);
147
+ }
148
+ const heading = conflicts.every(isOverrideableConflict) ? "Branch settings differ (re-run with --update-existing to apply)" : "Branch settings differ";
149
+ return [paint.title(heading), ...renderBranchGroups(byBranch, paint)].join("\n");
150
+ };
151
+ const renderCustomDomainFollowup = (result, opts) => {
152
+ const paint = palette(opts.color);
153
+ const lines = [];
154
+ const cnames = (result.customDomains ?? []).filter((d) => d.cnameTarget !== void 0 && d.cnameTarget !== "");
155
+ if (cnames.length > 0) {
156
+ lines.push(paint.title("Custom domains"));
157
+ for (const d of cnames) lines.push(` CNAME ${d.domain} -> ${d.cnameTarget}`);
158
+ lines.push(" Point each hostname at the CNAME target. Neon does not create DNS records.");
139
159
  }
140
- return [paint.title("Branch settings differ (re-run with --update-existing to apply)"), ...renderBranchGroups(byBranch, paint)].join("\n");
160
+ if (result.warnings.length > 0) {
161
+ if (lines.length > 0) lines.push("");
162
+ lines.push(paint.title("Warnings"));
163
+ for (const warning of result.warnings) lines.push(` ${warning}`);
164
+ }
165
+ return lines.join("\n");
141
166
  };
142
167
  //#endregion
143
- export { renderAppliedChanges, renderBranchSettingConflicts };
168
+ export { renderAppliedChanges, renderBranchSettingConflicts, renderCustomDomainFollowup };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.17.4",
3
+ "version": "4.18.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config": "1.4.3",
67
- "@neon/sdk": "5.0.0",
68
- "@neon/config-runtime": "1.3.3"
66
+ "@neon/config-runtime": "1.4.1",
67
+ "@neon/sdk": "6.0.0",
68
+ "@neon/config": "1.5.1"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -97,9 +97,9 @@
97
97
  "tsx": "4.22.3",
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
- "@neon-internals/env-core": "0.0.15",
101
100
  "@neon-internals/cli-core": "0.0.0",
102
101
  "@neon/e2e-harness": "0.0.0",
102
+ "@neon-internals/env-core": "0.0.17",
103
103
  "@neon/functions": "0.10.0"
104
104
  },
105
105
  "publishConfig": {