neon 4.14.6 → 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
@@ -440,8 +440,8 @@ The branch argument is **optional**: run `neon checkout` with no branch in an in
440
440
 
441
441
  Branch **id vs name** is detected automatically (a `br-…` value is treated as an id):
442
442
 
443
- - **id** — matched strictly by id. A non-existent id is a hard "not found" error (ids are server-assigned, so checkout never creates one).
444
- - **name** — matched by name. If the name doesn't exist, in an interactive terminal `checkout` offers to **create** it (equivalent to `neon branch create --name <name>`: branched from the project's default branch with a read-write compute), then checks it out. In a non-interactive context a missing name is the usual "not found" error.
443
+ - **id** — matched strictly by id. A non-existent id is a hard "not found" error (ids are server-assigned, so checkout never creates one, including with `--create`).
444
+ - **name** — matched by name. If the name doesn't exist, pass `--create` to create it (equivalent to `neon branch create --name <name>`: branched from the project's default branch with a read-write compute, or from `neon.ts` when that file is present), then check it out. `--create` is a no-op when the name already exists. Without `--create`, an interactive terminal offers to create the branch; in CI or with no TTY the error names `--create`.
445
445
 
446
446
  The project is resolved through the standard neon chain, each entry winning over the next:
447
447
 
@@ -457,11 +457,15 @@ The resolved branch is then written (by name) to the same `.neon` file `link` us
457
457
  $ neon checkout main --project-id polished-snowflake-12345678
458
458
  INFO: Checked out branch br-main-branch-87654321 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
459
459
 
460
+ $ neon checkout dev --create --project-id polished-snowflake-12345678
461
+ INFO: Created branch dev (br-dev-branch-12345678).
462
+ INFO: Checked out branch br-dev-branch-12345678 on project polished-snowflake-12345678. Updated /path/to/cwd/.neon.
463
+
460
464
  $ cat .neon
461
465
  {
462
466
  "orgId": "org-abc123",
463
467
  "projectId": "polished-snowflake-12345678",
464
- "branch": "main"
468
+ "branch": "dev"
465
469
  }
466
470
  ```
467
471
 
@@ -763,27 +767,34 @@ The target directory must be empty unless you pass `--force` (a lone `.git` is i
763
767
 
764
768
  ## Set up a project (`init`)
765
769
 
766
- `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
+
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.
767
773
 
768
- 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`.
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.
769
775
 
770
- 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).
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`.
771
777
 
772
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.
773
779
 
774
780
  ```bash
775
781
  $ neon init
782
+ $ neon init --skip-template
776
783
  $ neon init -y
777
784
  $ neon init --agent cursor --agent claude-code
778
785
  ```
779
786
 
780
787
  Without a TTY, pass `-y`. `--agent` skips agent selection but does not replace `-y` for link or templates.
781
788
 
782
- `-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.
783
790
 
784
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.
785
792
 
786
- `-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.
787
798
 
788
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.
789
800
 
@@ -946,6 +957,40 @@ neon snapshots schedule set --branch main --schedule '[{"frequency":"weekly","da
946
957
 
947
958
  All sub-commands honor the [global options](#global-options), including `--output json|yaml|table`.
948
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
+
949
994
  ## Database diagnostics (`inspect`)
950
995
 
951
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.
@@ -1285,6 +1330,8 @@ Id Name Project Created At Last Used At Last
1285
1330
  | logs | `query`, `fields`, `field-values` | Query branch logs (Beta) |
1286
1331
  | inspect | `db stalled-queries` | Inspect Postgres diagnostics |
1287
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 |
1288
1335
  | [connection-string](https://neon.com/docs/reference/cli-connection-string) | | Get connection string |
1289
1336
  | [psql](https://neon.com/docs/reference/cli-psql) | | Connect to a database via psql |
1290
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) === "";
@@ -28,7 +28,7 @@ const formatCheckoutPolicyFailure = (opts) => {
28
28
  return [
29
29
  `Branch ${opts.branchName} (${opts.branchId}) was created and checked out, but applying neon.ts to it failed: ${opts.failure}`,
30
30
  `The branch is usable but does not match the policy, and \`${cli} checkout\` never reconciles a branch that already exists.`,
31
- `Fix the cause above, then run \`${cli} deploy --update-existing${envArg}\` to apply the policy to it — or, if your policy only configures new branches (keyed on \`!branch.exists\`), delete the branch and check it out again: \`${cli} branches delete ${opts.branchName}\` then \`${cli} checkout ${opts.branchName}${envArg}\`.`
31
+ `Fix the cause above, then run \`${cli} deploy --update-existing${envArg}\` to apply the policy to it — or, if your policy only configures new branches (keyed on \`!branch.exists\`), delete the branch and check it out again: \`${cli} branches delete ${opts.branchName}\` then \`${cli} checkout ${opts.branchName} --create${envArg}\`.`
32
32
  ].join("\n");
33
33
  };
34
34
  const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").positional("id", {
@@ -44,6 +44,11 @@ const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").position
44
44
  type: "boolean",
45
45
  default: true
46
46
  },
47
+ create: {
48
+ describe: "Create the named branch if it does not exist, then check it out",
49
+ type: "boolean",
50
+ default: false
51
+ },
47
52
  ...envFlag,
48
53
  env: {
49
54
  ...envFlag.env,
@@ -52,12 +57,14 @@ const builder = (argv) => argv.usage("$0 checkout [id|name] [options]").position
52
57
  }).example([
53
58
  ["$0 checkout", "Pick a branch interactively from the project in the closest .neon file"],
54
59
  ["$0 checkout main", "Pin the branch named \"main\" in the closest .neon file"],
55
- ["$0 checkout feat --env .env.local", "Create feat from neon.ts, loading Function env from .env.local first"],
60
+ ["$0 checkout dev --create", "Create and pin a missing branch named dev"],
61
+ ["$0 checkout feat --create --env .env.local", "Create feat from neon.ts, loading Function env from .env.local first"],
56
62
  ["$0 checkout br-cool-snow-12345678 --project-id project-id-123", "Pin a branch by id for an explicit project"]
57
63
  ]);
58
64
  const handler = async (props) => {
59
65
  const previousBranch = contextBranch(readContextFile(props.contextFile));
60
66
  if (previousBranch) log.info("%s Currently on branch %s", chalk.dim("→"), chalk.cyan.bold(previousBranch));
67
+ if (props.create && !props.id?.trim()) throw new Error(`No branch specified. Pass a branch name with --create (e.g. \`${getCliName()} checkout dev --create\`).`);
61
68
  const projectId = await resolveProjectId(props);
62
69
  const { branchId, branchName, created, policyApplied, policyFailure } = await resolveBranchId(props, projectId);
63
70
  const orgId = await resolveOrgId(props, projectId);
@@ -137,8 +144,10 @@ const resolveBranchId = async (props, projectId) => {
137
144
  created: false,
138
145
  policyApplied: false
139
146
  };
140
- if (isCi() || !process.stdout.isTTY) throw new Error(notFoundMessage(ref, branches));
141
- log.error(notFoundMessage(ref, branches));
147
+ if (props.create) return createCheckoutBranch(props, projectId, ref, branches);
148
+ const missingName = notFoundMessage(ref, branches, { suggestCreate: true });
149
+ if (isCi() || !process.stdout.isTTY) throw new Error(missingName);
150
+ log.error(missingName);
142
151
  const { create } = await prompts({
143
152
  type: "confirm",
144
153
  name: "create",
@@ -178,7 +187,7 @@ const createCheckoutBranch = async (props, projectId, name, branches) => {
178
187
  policyApplied: false
179
188
  };
180
189
  };
181
- const notFoundMessage = (ref, branches) => `Branch ${ref} not found.\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`;
190
+ const notFoundMessage = (ref, branches, opts) => `Branch ${ref} not found.${opts?.suggestCreate ? " Pass --create to create it." : ""}\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`;
182
191
  /**
183
192
  * Resolve the org id to heal into the context file.
184
193
  *
@@ -2,7 +2,7 @@ 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
4
  import { getApiClient } from "../api.js";
5
- import { contextBranch, readContextFile } from "../context.js";
5
+ import { contextBranch, ensureDirectoryGitignored, readContextFile } from "../context.js";
6
6
  import { getCliName } from "../utils/cli_name.js";
7
7
  import { toNeonConfigView } from "../config_format.js";
8
8
  import { declaredNeonServices } from "../config_services.js";
@@ -229,6 +229,7 @@ const initCmd = async (props) => {
229
229
  const args = installArgs(pm, missing);
230
230
  if (props.install === false) log.info("Install the Neon config packages to use neon.ts: %s", formatInstallCommand(pm, missing));
231
231
  else {
232
+ ensureDirectoryGitignored(join(cwd, "node_modules"));
232
233
  log.info("Installing %s with %s…", missing.join(", "), pm);
233
234
  if (!await run(pm, args, cwd)) log.warning("Could not install the config packages automatically. Run by hand: %s", formatInstallCommand(pm, missing));
234
235
  }
@@ -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,