neon 4.15.0 → 4.16.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
@@ -767,27 +767,34 @@ The target directory must be empty unless you pass `--force` (a lone `.git` is i
767
767
 
768
768
  ## Set up a project (`init`)
769
769
 
770
- `neon init` sets up this directory for Neon.
770
+ `neon init` sets up this directory for Neon as one flow: agents, a linked project, and optionally `neon.ts`.
771
771
 
772
- An empty directory (nothing except `.git`) runs `neon bootstrap .` and stops. With `-y` that is `neon bootstrap . --default`. Bootstrap handles scaffolding, agent tooling, and linking. Interactive bootstrap prints a NEON banner, asks every setup question, then runs the work — dependency install last, except when a `neon.ts` needs deps before `link`.
772
+ An empty directory (nothing except `.git`) asks how to set up the directory. Pick a starter template, or skip scaffolding and only install agent tooling, link a project, and optionally write `neon.ts`. That skip is only on `init` — `neon bootstrap` always scaffolds.
773
773
 
774
- An existing app installs agent tooling, then `neon link` unless `.neon` already has a projectId, then `neon config init`. Interactive `config init` opens the services picker; `-y` uses `--services none` (starter policy).
774
+ `-y` in an empty directory scaffolds the default template (`bootstrap --default`) and does not add a later `config init` step. `--skip-template` skips scaffolding even with `-y`. `--template <id>` scaffolds that template; combine it with `-y` to skip the remaining bootstrap pickers.
775
+
776
+ An existing app (or skip-template) installs agent tooling, then `neon link --no-config` unless `.neon` already has a projectId, then asks whether to create `neon.ts`. Saying no skips the services picker and does not write the file. `--no-config` does the same without asking. `-y` writes `config init --services none` unless `--no-config`. `--services` implies creating `neon.ts`.
775
777
 
776
778
  In an interactive terminal it offers one of: the Neon plugin (`neon plugins`), skills and MCP separately (`neon skills`, then `neon mcp`), or skip agent setup. It never runs plugin and skills+MCP together.
777
779
 
778
780
  ```bash
779
781
  $ neon init
782
+ $ neon init --skip-template
780
783
  $ neon init -y
781
784
  $ neon init --agent cursor --agent claude-code
782
785
  ```
783
786
 
784
787
  Without a TTY, pass `-y`. `--agent` skips agent selection but does not replace `-y` for link or templates.
785
788
 
786
- `-y` skips the template picker and the agent-setup offer. Empty dir: `bootstrap --default`. Existing app: plugin when Cursor, Claude Code, or Codex is in project folders, else the host CLI agent; otherwise skills and MCP. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `-y` in a terminal to pick. VS Code, GitHub Copilot CLI, and Grok only take the plugin user-level (`neon plugins --global`), so `-y` uses skills and MCP for those.
789
+ `-y` skips the template picker and the agent-setup offer. Empty dir: `bootstrap --default`. `--skip-template` or an existing app: plugin when Cursor, Claude Code, or Codex is in project folders, else the host CLI agent; otherwise skills and MCP. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `-y` in a terminal to pick. VS Code, GitHub Copilot CLI, and Grok only take the plugin user-level (`neon plugins --global`), so `-y` uses skills and MCP for those.
787
790
 
788
791
  `--agent` / `-a` (repeatable) names coding agents and skips agent selection, interactive or with `-y`. Init forwards those names to `plugins`, or to `skills` and `mcp`, not both.
789
792
 
790
- `-y` forwards `-y` to `plugins` or `skills`/`mcp`, `--default` to `bootstrap`, `--yes` to `link`, and `--services none` to `config init`. `--agent` is forwarded with them. `mcp -y` is the global install. `link --yes` only skips the "already linked" confirmation; it still asks for a project unless one is already linked.
793
+ `--project-id`, `--org-id`, `--project-name`, `--region-id`, and `--branch` are forwarded to `link`, including the link step inside nested bootstrap when a template is scaffolded. They are not filled from `.neon`; a linked directory is not relinked unless you pass one of those flags.
794
+
795
+ `--config`, `--no-config`, and `--services` apply on the existing-app and `--skip-template` path. A template's own `neon.ts` is left as the template shipped it. Passing those flags while scaffolding prints a warning and still copies the template as shipped. After a new `neon.ts` on a pinned branch, init runs `env pull`.
796
+
797
+ `-y` forwards `-y` to `plugins` or `skills`/`mcp`, `--default` to nested bootstrap, `--yes --no-config` to `link`, and `--services none` to `config init`. `--agent` is forwarded with them. `mcp -y` is the global install. `link --yes` only skips the "already linked" confirmation; it still asks for a project unless one is already linked.
791
798
 
792
799
  A failed step stops the rest. `--profile` and `--config-dir` are forwarded to each child. `--output json` and `--output yaml` are refused; the commands init runs print their own output.
793
800
 
@@ -950,6 +957,40 @@ neon snapshots schedule set --branch main --schedule '[{"frequency":"weekly","da
950
957
 
951
958
  All sub-commands honor the [global options](#global-options), including `--output json|yaml|table`.
952
959
 
960
+ ## Function triggers (`triggers`)
961
+
962
+ `neon triggers` (alias `neon trigger`) manages **scheduled function triggers** on a branch — cron jobs that invoke a Neon Function. Beta. The only trigger type is `schedule`; cron is a five-field UTC expression.
963
+
964
+ Every sub-command resolves the project through the standard chain (`--project-id`, then the `.neon` context file, then a single-project auto-detect) and the branch through `--branch <id|name>`, the `.neon` pin, or the project's default branch.
965
+
966
+ ```bash
967
+ neon triggers create --function-slug uptime --name uptime-check --cron '*/15 * * * *'
968
+ neon triggers list
969
+ neon triggers get trigger-test-123
970
+ neon triggers update trigger-test-123 --cron '0 3 * * *'
971
+ neon triggers enable trigger-test-123
972
+ neon triggers disable trigger-test-123
973
+ neon triggers delete trigger-test-123
974
+ ```
975
+
976
+ `enable` / `disable` are wrappers over `update --enabled`. List shows `inherited` when the effective config was authored on an ancestor branch.
977
+
978
+ ## Branch credentials (`credentials`)
979
+
980
+ `neon credentials` (alias `neon credential`) lists, issues, reveals, rotates, and revokes **branch-scoped credentials** — the tokens behind Object Storage (`AWS_*`) and the AI Gateway (`NEON_AI_GATEWAY_TOKEN`). Beta. In regions where those services exist, a new project already has default credentials named `Default AI gateway credential` and `Default object storage credential`; `list` then `reveal` recovers their secrets without minting another token.
981
+
982
+ ```bash
983
+ neon credentials list
984
+ neon credentials create --name app --scope storage:read --scope storage:write
985
+ neon credentials reveal nak_live_…
986
+ neon credentials rotate nak_live_…
987
+ neon credentials revoke nak_live_…
988
+ ```
989
+
990
+ `create` always issues a customer-managed (`user`) credential. `--scope` is repeatable; `--help` lists the values. `reveal` and `rotate` print `api_token` and `s3_secret_access_key`. Rotation keeps the same `token_id` (it is the `AWS_ACCESS_KEY_ID`) and is not idempotent: a retry after a lost response mints another secret.
991
+
992
+ All sub-commands honor the [global options](#global-options), including `--output json|yaml|table`.
993
+
953
994
  ## Database diagnostics (`inspect`)
954
995
 
955
996
  `neon inspect db stalled-queries` takes a read-only snapshot of active queries that have run for more than 30 seconds and groups parallel workers with their leader. Oldest group first. Table output shows duration, wait event, blocking pids, role, query group, and query. `--output json` adds timestamps, query IDs, pids, database, and the rest of the row. A blocking pid can belong to an idle-in-transaction backend this command does not list; `neon inspect db locks` shows lock holders.
@@ -1289,6 +1330,8 @@ Id Name Project Created At Last Used At Last
1289
1330
  | logs | `query`, `fields`, `field-values` | Query branch logs (Beta) |
1290
1331
  | inspect | `db stalled-queries` | Inspect Postgres diagnostics |
1291
1332
  | snapshots | `list`, `get`, `create`, `update`, `delete`, `restore`, `finalize`, `schedule get`, `schedule set` | Manage snapshots |
1333
+ | triggers | `list`, `get`, `create`, `update`, `enable`, `disable`, `delete` | Manage function triggers |
1334
+ | credentials | `list`, `create`, `reveal`, `rotate`, `revoke` | Manage branch credentials |
1292
1335
  | [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
1293
1336
  | [psql](https://neon.com/docs/reference/cli-psql) | | Connect to a database via psql |
1294
1337
  | set-context | | Deprecated; use `link` |
package/dist/analytics.js CHANGED
@@ -156,13 +156,20 @@ const closeAnalytics = async (opts) => {
156
156
  log.debug("Flushed CLI analytics");
157
157
  }
158
158
  };
159
+ const getErrorAnalyticsEventContext = (_args) => ({
160
+ version: pkg_default.version,
161
+ ci: isCi(),
162
+ agent: getCliAgent(process.env)
163
+ });
164
+ const CREDENTIAL_TOKEN_ID = /\bnak_live_[0-9a-f]+\b/gi;
165
+ const redactCredentialTokenIds = (text) => text.replace(CREDENTIAL_TOKEN_ID, "nak_live_<redacted>");
159
166
  const getErrorAnalyticsEventProperties = (err, errCode, context) => {
160
167
  const apiError = isNeonApiError(err) ? err : void 0;
161
168
  const requestId = apiError?.headers?.["x-neon-ret-request-id"];
162
169
  return {
163
170
  ...context,
164
- message: err.message,
165
- stack: err.stack,
171
+ message: redactCredentialTokenIds(err.message),
172
+ stack: err.stack ? redactCredentialTokenIds(err.stack) : err.stack,
166
173
  errCode,
167
174
  statusCode: apiError?.status,
168
175
  requestId
@@ -188,15 +195,10 @@ const trackEvent = (event, properties) => {
188
195
  });
189
196
  log.debug("Sent CLI event: %s", event);
190
197
  };
191
- const getErrorAnalyticsEventContext = (_args) => ({
192
- version: pkg_default.version,
193
- ci: isCi(),
194
- agent: getCliAgent(process.env)
195
- });
196
198
  const analyticsCommand = (args) => {
197
199
  const command = args._.join(" ");
198
- if (args._[0] !== "ask" || typeof args.prompt !== "string") return command;
199
- return `${command} ${args.prompt}`;
200
+ const raw = args._[0] !== "ask" || typeof args.prompt !== "string" ? command : `${command} ${args.prompt}`;
201
+ return redactCredentialTokenIds(raw);
200
202
  };
201
203
  const getAnalyticsEventProperties = (args) => ({
202
204
  version: pkg_default.version,
package/dist/api.js CHANGED
@@ -872,6 +872,85 @@ const getApiClient = ({ apiKey, apiHost, requestTimeoutMs = REQUEST_TIMEOUT_MS }
872
872
  auth_user_id: authUserId
873
873
  },
874
874
  body: data
875
+ })),
876
+ listProjectBranchTriggers: (projectId, branchId) => call(() => raw.listProjectBranchTriggers({
877
+ client,
878
+ path: {
879
+ project_id: projectId,
880
+ branch_id: branchId
881
+ }
882
+ })),
883
+ getProjectBranchTrigger: (projectId, branchId, triggerId) => call(() => raw.getProjectBranchTrigger({
884
+ client,
885
+ path: {
886
+ project_id: projectId,
887
+ branch_id: branchId,
888
+ trigger_id: triggerId
889
+ }
890
+ })),
891
+ createProjectBranchTrigger: (projectId, branchId, data) => call(() => raw.createProjectBranchTrigger({
892
+ client,
893
+ path: {
894
+ project_id: projectId,
895
+ branch_id: branchId
896
+ },
897
+ body: data
898
+ })),
899
+ updateProjectBranchTrigger: (projectId, branchId, triggerId, data) => call(() => raw.updateProjectBranchTrigger({
900
+ client,
901
+ path: {
902
+ project_id: projectId,
903
+ branch_id: branchId,
904
+ trigger_id: triggerId
905
+ },
906
+ body: data
907
+ })),
908
+ deleteProjectBranchTrigger: (projectId, branchId, triggerId) => call(() => raw.deleteProjectBranchTrigger({
909
+ client,
910
+ path: {
911
+ project_id: projectId,
912
+ branch_id: branchId,
913
+ trigger_id: triggerId
914
+ }
915
+ })),
916
+ listCredentials: (projectId, branchId) => call(() => raw.listCredentials({
917
+ client,
918
+ path: {
919
+ project_id: projectId,
920
+ branch_id: branchId
921
+ }
922
+ })),
923
+ createCredential: (projectId, branchId, data) => call(() => raw.createCredential({
924
+ client,
925
+ path: {
926
+ project_id: projectId,
927
+ branch_id: branchId
928
+ },
929
+ body: data
930
+ })),
931
+ revokeCredential: (projectId, branchId, tokenId) => call(() => raw.revokeCredential({
932
+ client,
933
+ path: {
934
+ project_id: projectId,
935
+ branch_id: branchId,
936
+ token_id: tokenId
937
+ }
938
+ })),
939
+ revealCredential: (projectId, branchId, tokenId) => call(() => raw.revealCredential({
940
+ client,
941
+ path: {
942
+ project_id: projectId,
943
+ branch_id: branchId,
944
+ token_id: tokenId
945
+ }
946
+ })),
947
+ rotateCredential: (projectId, branchId, tokenId) => call(() => raw.rotateCredential({
948
+ client,
949
+ path: {
950
+ project_id: projectId,
951
+ branch_id: branchId,
952
+ token_id: tokenId
953
+ }
875
954
  }))
876
955
  };
877
956
  };
@@ -8,14 +8,14 @@ import { FALLBACK_TEMPLATES, ensureTargetUsable, fetchTemplates, findTemplate, s
8
8
  import { spawnCliChild } from "../init/child.js";
9
9
  import { agentSetupDoneLabel, formatInitDone, printInitBanner, printInitDone, shouldPrintInitBanner } from "../init/chrome.js";
10
10
  import { coerceAgentFlag } from "../utils/agent_flag.js";
11
- import { assertNamedAgentTooling, chooseYesAgentTooling, initPluginAgents, initSkillsMcpAgents, postScaffoldActions, projectContextFile, resolveNamedAgents } from "../init/plan.js";
11
+ import { assertNamedAgentTooling, chooseYesAgentTooling, initPluginAgents, initSkillsMcpAgents, planLinkStep, postScaffoldActions, projectContextFile, resolveNamedAgents } from "../init/plan.js";
12
+ import { formatTemplateTitle } from "../init/template_title.js";
12
13
  import { pickAgentSetupInteractively } from "../init/wizard.js";
13
14
  import { runAgentTooling, runInitSteps } from "../init/tooling.js";
14
15
  import { helpCsv, helpEpilogue } from "../utils/help_text.js";
15
16
  import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
16
17
  import { existsSync } from "node:fs";
17
18
  import { join, relative, resolve } from "node:path";
18
- import chalk from "chalk";
19
19
  import prompts from "prompts";
20
20
  //#region src/commands/bootstrap.ts
21
21
  var bootstrap_exports = /* @__PURE__ */ __exportAll({
@@ -102,20 +102,19 @@ const handler = async (props) => {
102
102
  }
103
103
  return;
104
104
  }
105
- if (shouldPrintInitBanner(props.default)) printInitBanner();
105
+ if (props.printBanner !== false && shouldPrintInitBanner(props.default)) printInitBanner();
106
106
  const named = resolveNamedAgents(props.agent ?? []);
107
107
  if (props.agentSetup !== false) assertNamedAgentTooling(named, "bootstrap", {
108
108
  ...props.directory !== void 0 && props.directory.length > 0 ? { directory: props.directory } : {},
109
109
  ...props.default ? { yes: true } : {}
110
110
  });
111
- const templates = await resolveTemplateList(props);
112
111
  const interactive = !props.default && Boolean(process.stdout.isTTY) && !isCi();
113
- const template = await resolveSelectedTemplate(props, interactive, templates);
112
+ const template = props.selectedTemplate ?? await resolveSelectedTemplate(props, interactive, await resolveTemplateList(props));
114
113
  const targetDir = await resolveTargetDir(props, interactive, template);
115
114
  ensureTargetUsable(targetDir, props.force);
116
115
  await scaffold(template, targetDir);
117
116
  printScaffolded(template, targetDir);
118
- await runPostScaffoldSteps(props, targetDir, interactive, template, named);
117
+ return runPostScaffoldSteps(props, targetDir, interactive, template, named);
119
118
  };
120
119
  /**
121
120
  * The template list to choose from. When --template is given we try the
@@ -123,18 +122,6 @@ const handler = async (props) => {
123
122
  * remote manifest if the id isn't one of the defaults.
124
123
  */
125
124
  const resolveTemplateList = async (props) => props.template && findTemplate(FALLBACK_TEMPLATES, props.template) ? FALLBACK_TEMPLATES : fetchTemplates();
126
- /**
127
- * The picker label for a template: the title first, then the Neon services it
128
- * uses as a dim, italic suffix, e.g. "Hono API … Postgres · Functions". The
129
- * suffix is styled with chalk.dim (and italic) only — never a foreground color —
130
- * so it survives the cyan/underline `prompts` paints over the focused row: dim
131
- * and italic reset with their own SGRs, leaving the row's color and underline
132
- * intact. Descriptions are intentionally omitted to keep the picker uncluttered.
133
- */
134
- const formatTemplateTitle = (template) => {
135
- if (!template.services || template.services.length === 0) return template.title;
136
- return `${template.title} ${chalk.dim.italic(template.services.join(" · "))}`;
137
- };
138
125
  const resolveSelectedTemplate = async (props, interactive, templates) => {
139
126
  if (props.template) {
140
127
  const template = findTemplate(templates, props.template);
@@ -188,20 +175,19 @@ const scaffold = async (template, targetDir) => {
188
175
  log.info("Scaffolded %d files into %s.", filesWritten, targetDir);
189
176
  return filesWritten;
190
177
  };
178
+ const nestedResult = (template, targetDir, hasNeonConfig, outcome) => ({
179
+ templateTitle: template.title,
180
+ targetDir,
181
+ hasNeonConfig,
182
+ ...outcome
183
+ });
191
184
  const runPostScaffoldSteps = async (props, targetDir, interactive, template, named) => {
192
185
  const inferred = inferPackageManager(targetDir);
193
186
  const defaultPm = resolvePackageManager(targetDir);
194
187
  const neonConfig = hasNeonConfig(targetDir);
195
- if (props.default) {
196
- await runDefaultSteps(props, targetDir, defaultPm, neonConfig, template, named);
197
- return;
198
- }
188
+ if (props.default) return runDefaultSteps(props, targetDir, defaultPm, neonConfig, template, named);
199
189
  if (!interactive) {
200
- printDoneSummary({
201
- heading: "Project scaffolded.",
202
- template,
203
- targetDir,
204
- pm: defaultPm,
190
+ const outcome = {
205
191
  installed: false,
206
192
  installFailed: false,
207
193
  gitFailed: false,
@@ -209,10 +195,18 @@ const runPostScaffoldSteps = async (props, targetDir, interactive, template, nam
209
195
  agentSetup: "skip",
210
196
  linked: false,
211
197
  skippedLinkForDeps: false,
212
- suggestLink: true,
213
198
  agentsRan: false
199
+ };
200
+ finishPostScaffold({
201
+ heading: "Project scaffolded.",
202
+ template,
203
+ targetDir,
204
+ pm: defaultPm,
205
+ ...outcome,
206
+ suggestLink: true,
207
+ skipPrint: props.skipDoneSummary === true
214
208
  });
215
- return;
209
+ return nestedResult(template, targetDir, neonConfig, outcome);
216
210
  }
217
211
  let pm = defaultPm;
218
212
  const wantInstall = props.install && await confirm(installPrompt(inferred));
@@ -239,8 +233,10 @@ const runPostScaffoldSteps = async (props, targetDir, interactive, template, nam
239
233
  targetDir,
240
234
  pm,
241
235
  ...outcome,
242
- suggestLink: !outcome.linked
236
+ suggestLink: !outcome.linked,
237
+ skipPrint: props.skipDoneSummary === true
243
238
  });
239
+ return nestedResult(template, targetDir, neonConfig, outcome);
244
240
  };
245
241
  const installPrompt = (inferred) => inferred ? `Install dependencies with ${inferred}?` : "Install dependencies?";
246
242
  /** `link --yes` still asks for a project unless one is already linked. */
@@ -265,11 +261,16 @@ const runDefaultSteps = async (props, targetDir, pm, neonConfig, template, named
265
261
  targetDir,
266
262
  pm,
267
263
  ...outcome,
268
- suggestLink: !outcome.linked
264
+ suggestLink: !outcome.linked,
265
+ skipPrint: props.skipDoneSummary === true
269
266
  });
267
+ return nestedResult(template, targetDir, neonConfig, outcome);
270
268
  };
271
269
  const executePostScaffold = async (props, targetDir, choices) => {
272
- const kids = bootstrapChildren(props, targetDir);
270
+ const kids = {
271
+ ...bootstrapChildren(props, targetDir),
272
+ ...props.narrate ? { narrate: props.narrate } : {}
273
+ };
273
274
  let installed = false;
274
275
  let installFailed = false;
275
276
  let gitFailed = false;
@@ -321,7 +322,10 @@ const executePostScaffold = async (props, targetDir, choices) => {
321
322
  logSkippedLink(choices.pm);
322
323
  continue;
323
324
  }
324
- await runInitSteps([choices.yes ? ["link", "--yes"] : ["link"]], {
325
+ await runInitSteps([props.linkNoConfig === true ? planLinkStep({
326
+ yes: choices.yes,
327
+ extra: props.linkExtra
328
+ }) : choices.yes ? ["link", "--yes"] : ["link"]], {
325
329
  cwd: targetDir,
326
330
  ...kids
327
331
  });
@@ -450,7 +454,7 @@ const printDoneSummary = (input) => {
450
454
  }));
451
455
  };
452
456
  const finishPostScaffold = (input) => {
453
- printDoneSummary(input);
457
+ if (!input.skipPrint) printDoneSummary(input);
454
458
  if (input.installFailed || input.gitFailed) throw new Error("Setup did not finish.");
455
459
  };
456
460
  const isCurrentDir = (targetDir) => relative(process.cwd(), targetDir) === "";
@@ -0,0 +1,146 @@
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
+ import { log } from "../log.js";
3
+ import { isNeonApiError, messageFromBody, retryOnLock } from "../api.js";
4
+ import { writer } from "../writer.js";
5
+ import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
6
+ //#region src/commands/credentials.ts
7
+ var credentials_exports = /* @__PURE__ */ __exportAll({
8
+ aliases: () => aliases,
9
+ builder: () => builder,
10
+ command: () => command,
11
+ create: () => create,
12
+ describe: () => describe,
13
+ handler: () => handler,
14
+ list: () => list,
15
+ reveal: () => reveal,
16
+ revoke: () => revoke,
17
+ rotate: () => rotate
18
+ });
19
+ const CREDENTIAL_SCOPES = [
20
+ "storage:read",
21
+ "storage:write",
22
+ "ai_gateway:invoke",
23
+ "functions:invoke"
24
+ ];
25
+ const LIST_FIELDS = [
26
+ "token_id",
27
+ "name",
28
+ "principal_type",
29
+ "scopes",
30
+ "created_at"
31
+ ];
32
+ const SECRET_META_FIELDS = [
33
+ "token_id",
34
+ "name",
35
+ "scopes"
36
+ ];
37
+ const renderColumns = { scopes: (c) => (c.scopes ?? []).join(", ") };
38
+ /** Translate a token-id 404 into a resource-specific message; re-throw everything else. */
39
+ async function withCredentialNotFound(tokenId, branchId, run) {
40
+ try {
41
+ return await run();
42
+ } catch (err) {
43
+ if (isNeonApiError(err) && err.status === 404) {
44
+ if ((messageFromBody(err.data) ?? "").includes("platform credentials not available")) throw err;
45
+ throw new Error(`Credential ${tokenId} not found on branch ${branchId}.`);
46
+ }
47
+ throw err;
48
+ }
49
+ }
50
+ /**
51
+ * Secrets are shown only once (create/rotate) or on demand (reveal). Table
52
+ * output keeps them off the grid so they stay one selectable line; json/yaml
53
+ * keep them on the object for scripts.
54
+ */
55
+ const reportSecrets = (props, data, warning) => {
56
+ const out = writer(props);
57
+ if (props.output === "table") {
58
+ out.write(data, { fields: SECRET_META_FIELDS });
59
+ out.end();
60
+ const apiToken = "api_token" in data && typeof data.api_token === "string" ? data.api_token : "";
61
+ const secretKey = "s3_secret_access_key" in data && typeof data.s3_secret_access_key === "string" ? data.s3_secret_access_key : "";
62
+ out.text(`api_token: ${apiToken}\n`);
63
+ out.text(`s3_secret_access_key: ${secretKey}\n`);
64
+ } else out.end(data, { fields: [
65
+ "token_id",
66
+ "name",
67
+ "scopes",
68
+ "api_token",
69
+ "s3_secret_access_key"
70
+ ] });
71
+ log.warning(warning);
72
+ };
73
+ const command = "credentials";
74
+ const describe = "Manage branch credentials";
75
+ const aliases = ["credential"];
76
+ const builder = (argv) => argv.usage("$0 credentials <sub-command> [options]").options({
77
+ "project-id": {
78
+ describe: "Project ID",
79
+ type: "string"
80
+ },
81
+ branch: {
82
+ describe: "Branch ID or name",
83
+ type: "string"
84
+ }
85
+ }).middleware(fillSingleProject).command("list", "List credentials on the branch", (yargs) => yargs, (args) => list(args)).command("create", "Issue a scoped credential on the branch", (yargs) => yargs.options({
86
+ name: {
87
+ describe: "Label for the credential",
88
+ type: "string"
89
+ },
90
+ scope: {
91
+ describe: "Capability to grant. Repeatable. Values: storage:read, storage:write, ai_gateway:invoke, functions:invoke",
92
+ type: "string",
93
+ array: true,
94
+ choices: CREDENTIAL_SCOPES,
95
+ demandOption: true
96
+ }
97
+ }), (args) => create(args)).command("reveal <tokenId>", "Show a credential's api_token and s3_secret_access_key", (yargs) => yargs.positional("tokenId", {
98
+ describe: "Credential token id (nak_live_<32hex>)",
99
+ type: "string"
100
+ }), (args) => reveal(args)).command("rotate <tokenId>", "Replace a credential's secrets in place. The token id is unchanged.", (yargs) => yargs.positional("tokenId", {
101
+ describe: "Credential token id (nak_live_<32hex>)",
102
+ type: "string"
103
+ }), (args) => rotate(args)).command("revoke <tokenId>", "Revoke a credential", (yargs) => yargs.positional("tokenId", {
104
+ describe: "Credential token id (nak_live_<32hex>)",
105
+ type: "string"
106
+ }), (args) => revoke(args));
107
+ const handler = (args) => {
108
+ return args;
109
+ };
110
+ const list = async (props) => {
111
+ const branchId = await branchIdFromProps(props);
112
+ const { data } = await props.apiClient.listCredentials(props.projectId, branchId);
113
+ writer(props).end(data.credentials, {
114
+ fields: LIST_FIELDS,
115
+ renderColumns,
116
+ emptyMessage: "No credentials found on this branch."
117
+ });
118
+ };
119
+ const create = async (props) => {
120
+ const branchId = await branchIdFromProps(props);
121
+ const scopes = (Array.isArray(props.scope) ? props.scope : [props.scope]).filter((scope) => CREDENTIAL_SCOPES.some((allowed) => allowed === scope));
122
+ if (scopes.length === 0) throw new Error("Pass at least one --scope. Values: storage:read, storage:write, ai_gateway:invoke, functions:invoke");
123
+ const { data } = await retryOnLock(() => props.apiClient.createCredential(props.projectId, branchId, {
124
+ scopes,
125
+ principal_type: "user",
126
+ ...props.name !== void 0 ? { name: props.name } : {}
127
+ }));
128
+ reportSecrets(props, data, "Store these secrets now: they are not shown again unless you run neon credentials reveal.");
129
+ };
130
+ const reveal = async (props) => {
131
+ const branchId = await branchIdFromProps(props);
132
+ const { data } = await withCredentialNotFound(props.tokenId, branchId, () => props.apiClient.revealCredential(props.projectId, branchId, props.tokenId));
133
+ reportSecrets(props, data, "These are live secrets. Treat them like a password.");
134
+ };
135
+ const rotate = async (props) => {
136
+ const branchId = await branchIdFromProps(props);
137
+ const { data } = await withCredentialNotFound(props.tokenId, branchId, () => retryOnLock(() => props.apiClient.rotateCredential(props.projectId, branchId, props.tokenId)));
138
+ reportSecrets(props, data, "Store the new secrets now: a retry mints another pair and does not recover a lost response. A replica may briefly accept the previous secret.");
139
+ };
140
+ const revoke = async (props) => {
141
+ const branchId = await branchIdFromProps(props);
142
+ await withCredentialNotFound(props.tokenId, branchId, () => retryOnLock(() => props.apiClient.revokeCredential(props.projectId, branchId, props.tokenId)));
143
+ log.info(`Credential ${props.tokenId} revoked`);
144
+ };
145
+ //#endregion
146
+ export { aliases, builder, command, create, describe, handler, list, reveal, revoke, rotate, credentials_exports as t };
@@ -12,6 +12,7 @@ import { t as link_exports } from "./link.js";
12
12
  import { t as checkout_exports } from "./checkout.js";
13
13
  import { t as claim_exports } from "./claim.js";
14
14
  import { t as connection_string_exports } from "./connection_string.js";
15
+ import { t as credentials_exports } from "./credentials.js";
15
16
  import { t as data_api_exports } from "./data_api.js";
16
17
  import { t as databases_exports } from "./databases.js";
17
18
  import { t as deploy_exports } from "./deploy.js";
@@ -35,6 +36,7 @@ import { t as set_context_exports } from "./set_context.js";
35
36
  import { t as skills_exports } from "./skills.js";
36
37
  import { t as snapshots_exports } from "./snapshots.js";
37
38
  import { t as status_exports } from "./status.js";
39
+ import { t as triggers_exports } from "./triggers.js";
38
40
  import { t as user_exports } from "./user.js";
39
41
  import { t as vpc_endpoints_exports } from "./vpc_endpoints.js";
40
42
  //#region src/commands/index.ts
@@ -70,6 +72,8 @@ var commands_default = [
70
72
  ask_exports,
71
73
  data_api_exports,
72
74
  functions_exports,
75
+ triggers_exports,
76
+ credentials_exports,
73
77
  dev_exports,
74
78
  diff_exports,
75
79
  config_exports,