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 +48 -5
- package/dist/analytics.js +11 -9
- package/dist/api.js +79 -0
- package/dist/commands/bootstrap.js +38 -34
- package/dist/commands/credentials.js +146 -0
- package/dist/commands/index.js +4 -0
- package/dist/commands/init.js +233 -23
- package/dist/commands/link.js +27 -16
- package/dist/commands/triggers.js +182 -0
- package/dist/context.js +1 -0
- package/dist/init/child.js +2 -1
- package/dist/init/choices.js +46 -0
- package/dist/init/chrome.js +25 -2
- package/dist/init/plan.js +42 -7
- package/dist/init/template_title.js +15 -0
- package/dist/init/tooling.js +5 -1
- package/dist/init/wizard.js +58 -6
- package/package.json +1 -1
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`)
|
|
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
|
-
|
|
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`.
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
return
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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([
|
|
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 };
|
package/dist/commands/index.js
CHANGED
|
@@ -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,
|