neon 2.45.0 → 2.46.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 +62 -1
- package/dist/_shared/paths.js +3 -4
- package/dist/commands/bootstrap.js +12 -11
- package/dist/commands/checkout.js +7 -6
- package/dist/commands/config.js +2 -1
- package/dist/commands/data_api.js +4 -3
- package/dist/commands/env.js +5 -4
- package/dist/commands/functions.js +3 -2
- package/dist/commands/init.js +82 -37
- package/dist/commands/ip_allow.js +3 -2
- package/dist/commands/link.js +17 -16
- package/dist/commands/projects.js +3 -2
- package/dist/commands/set_context.js +5 -4
- package/dist/current_branch_fast_path.js +2 -1
- package/dist/dev/env.js +8 -7
- package/dist/index.js +2 -2
- package/dist/init/agents.js +127 -0
- package/dist/init/auth.js +77 -0
- package/dist/init/bootstrap.js +448 -0
- package/dist/init/build_config.js +2 -0
- package/dist/init/detect_agent.js +108 -0
- package/dist/init/editors.js +62 -0
- package/dist/init/enrich_output.js +71 -0
- package/dist/init/extension.js +191 -0
- package/dist/init/inspect.js +287 -0
- package/dist/init/interactive.js +651 -0
- package/dist/init/neonctl.js +184 -0
- package/dist/init/orchestrate.js +190 -0
- package/dist/init/phases/auth.js +209 -0
- package/dist/init/phases/cleanup.js +27 -0
- package/dist/init/phases/db.js +283 -0
- package/dist/init/phases/getting_started.js +228 -0
- package/dist/init/phases/mcp.js +227 -0
- package/dist/init/phases/migrations.js +251 -0
- package/dist/init/phases/neon_auth.js +135 -0
- package/dist/init/phases/setup.js +729 -0
- package/dist/init/phases/skills.js +89 -0
- package/dist/init/phases/status.js +70 -0
- package/dist/init/resolve_context.js +107 -0
- package/dist/init/route_command.js +100 -0
- package/dist/init/skills.js +248 -0
- package/dist/init/types.js +1 -0
- package/dist/init/vsix.js +111 -0
- package/dist/psql/command/cmd_meta.js +2 -2
- package/dist/psql/core/mainloop.js +1 -1
- package/dist/psql/core/startup.js +1 -1
- package/dist/psql/core/syncVars.js +3 -3
- package/dist/psql/index.js +1 -1
- package/dist/utils/cli_name.js +14 -0
- package/dist/utils/esbuild.js +1 -1
- package/dist/utils/write_sync.js +39 -0
- package/package.json +18 -12
package/README.md
CHANGED
|
@@ -593,6 +593,66 @@ $ neon bootstrap . --template hono
|
|
|
593
593
|
|
|
594
594
|
The target directory must be empty unless you pass `--force` (a lone `.git` is ignored, so a freshly `git init`ed folder is fine). Symlinks and executable bits in the template are preserved.
|
|
595
595
|
|
|
596
|
+
## Set up a project for your coding agent (`init`)
|
|
597
|
+
|
|
598
|
+
`neon init` wires an existing project up to Neon: it signs you in, installs the Neon MCP server and agent skills into your editor, adds the Neon Local Connect extension for VS Code and Cursor, creates or picks a project, writes `DATABASE_URL` into `.env`, and offers to scaffold migrations.
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
$ neon init
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
Run in a terminal it prompts you through those steps. This is what the retired `neon-init` package used to do; `npx neon init` replaces it.
|
|
605
|
+
|
|
606
|
+
Two side effects worth knowing before you run it. It **installs or upgrades `neonctl` globally**, with whichever package manager invoked it — the flow drives Neon by shelling out to the CLI rather than calling the API in-process. And it **writes `.neon`** in the project directory, the same context file `neon link` and `neon checkout` use.
|
|
607
|
+
|
|
608
|
+
### Agent mode
|
|
609
|
+
|
|
610
|
+
`--agent` turns the same flow into a state machine an AI coding assistant drives. It prints **one JSON object on stdout** and nothing else: a phase response carrying a `status` and a `nextAction` telling the agent what to do next — usually another `neon init` invocation, spelled out as a `command`. The two read-only steps, `status` and `finalize`, return a snapshot instead.
|
|
611
|
+
|
|
612
|
+
```bash
|
|
613
|
+
$ neon init --agent --data '{"step":"status"}'
|
|
614
|
+
{
|
|
615
|
+
"auth": { "authenticated": true },
|
|
616
|
+
"tooling": { "mcpServer": { "configured": true, "scope": "global" }, "skills": { "installed": false, "scope": null } },
|
|
617
|
+
"project": { "databaseUrl": false },
|
|
618
|
+
"migrations": { "tool": "prisma", "hasMigrations": false },
|
|
619
|
+
"recommendations": [
|
|
620
|
+
{ "priority": "high", "message": "No DATABASE_URL found in .env", "command": "neon init --agent --data '{\"step\":\"db\"}'" },
|
|
621
|
+
{ "priority": "medium", "message": "Neon agent skills not detected in this project", "command": "neon init --agent --data '{\"step\":\"skills\",\"install\":true}'" },
|
|
622
|
+
{ "priority": "medium", "message": "prisma detected but no migrations found", "command": "neon init --agent --data '{\"step\":\"migrations\"}'" }
|
|
623
|
+
]
|
|
624
|
+
}
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
`--agent` is implied when stdin is not a TTY and a known agent is detected from the environment (Claude Code, Codex, Cline, Cursor, VS Code, Windsurf).
|
|
628
|
+
|
|
629
|
+
`--data` takes a JSON object whose `step` selects the phase: `auth`, `db`, `setup`, `getting-started`, `mcp`, `skills`, `migrations`, `neon-auth`, `status`, or `finalize`. Remaining keys are that phase's options. Without `--data`, the orchestrator picks the next phase itself. An unrecognised `step` is refused with the full list.
|
|
630
|
+
|
|
631
|
+
**Failures are JSON too**, so an agent never has to distinguish "it broke" from "it returned nothing":
|
|
632
|
+
|
|
633
|
+
```bash
|
|
634
|
+
$ neon init --agent --data '{not json'
|
|
635
|
+
{
|
|
636
|
+
"success": false,
|
|
637
|
+
"error": "Invalid JSON in --data flag at position 1. Expected a JSON object."
|
|
638
|
+
}
|
|
639
|
+
$ echo $?
|
|
640
|
+
1
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
That message reports where parsing stopped and nothing more. `--data` carries whatever you put in it, and the JSON parser's own message quotes a window of the input, so echoing either would put a connection string or an API key on stdout.
|
|
644
|
+
|
|
645
|
+
**One exception to "JSON on stdout".** Credentials are resolved before any command runs, so a failure in that step — an unknown `--profile` or `NEON_PROFILE`, `--api-key` and `--profile` together, or a `credentials.json` that cannot be read — prints `ERROR: …` on stderr, leaves stdout empty, and exits 1. Treat a non-zero exit with empty stdout as a credential problem and read stderr.
|
|
646
|
+
|
|
647
|
+
| Option | |
|
|
648
|
+
| --- | --- |
|
|
649
|
+
| `--agent`, `-a` | Emit the JSON state machine instead of prompting |
|
|
650
|
+
| `--data <json>` | Route to one phase, with that phase's options |
|
|
651
|
+
| `--skip-migrations` | Leave the migrations phase out of the flow |
|
|
652
|
+
| `--preview` | Enable preview features (scaffolding a project from a template) |
|
|
653
|
+
|
|
654
|
+
`neon init` refuses `--profile`; see [Which credential an invocation uses](#which-credential-an-invocation-uses).
|
|
655
|
+
|
|
596
656
|
## Snapshots (`snapshots`)
|
|
597
657
|
|
|
598
658
|
`neon snapshots` (alias `neon snapshot`) manages **snapshots** — point-in-time backups of a branch that you can list, rename, expire, restore into a branch, or schedule automatically. Snapshots are a Beta Neon feature and were previously only available in the Console and REST API; this command group brings them to the CLI.
|
|
@@ -776,7 +836,7 @@ When both are only environment variables the key wins, which keeps a CI pipeline
|
|
|
776
836
|
|
|
777
837
|
`neon auth` and the `profile` subcommands are outside all of this, because they read the same flags to mean something else: `neon auth --profile work` names where to write a credential, and `neon profile create work --api-key …` names one to store.
|
|
778
838
|
|
|
779
|
-
`neon init` does not support `--profile` yet. It
|
|
839
|
+
`neon init` does not support `--profile` yet. It runs its own auth flow, which reads the default credentials directly and re-invokes the CLI as a subprocess without passing a profile down, so passing the flag fails instead of quietly running as the default account. `--api-key` and `NEON_API_KEY` reach `neon init` no better and are **not** refused. The flow reads the stored credential and nothing else, so a supplied key is ignored: with a credential on disk `neon init` runs silently as *that* account, and with none it sends you to a browser sign-in. The silent case is the one to watch — it is the same failure `--profile` is refused for, without the refusal. Until that is fixed, sign in as the account you want first, or use another command.
|
|
780
840
|
|
|
781
841
|
## API keys (`api-keys`)
|
|
782
842
|
|
|
@@ -876,6 +936,7 @@ API keys in org-7
|
|
|
876
936
|
| config | `init`, `status`, `plan`, `apply` | Drive a branch from `neon.ts` |
|
|
877
937
|
| deploy | | Alias for `config apply` |
|
|
878
938
|
| bootstrap | | Scaffold a project from a template |
|
|
939
|
+
| init | | Set up a project for a coding agent |
|
|
879
940
|
| bucket | `create`, `list`, `delete`, `object list`, `object get`, `object put`, `object delete` (incl. `--recursive`) | Manage buckets and their objects |
|
|
880
941
|
| [completion](https://neon.com/docs/reference/cli-completion) | | Generate a completion script |
|
|
881
942
|
|
package/dist/_shared/paths.js
CHANGED
|
@@ -4,13 +4,12 @@
|
|
|
4
4
|
**Deliberately impure.** It reads environment variables and touches the filesystem, which
|
|
5
5
|
* `@neon/config` — the package this used to be a subpath of — must never do from its root
|
|
6
6
|
* export. It lives here instead of there precisely so that a policy-facing package does not
|
|
7
|
-
* carry implementor-only code
|
|
8
|
-
* the same resolution as everything else.
|
|
7
|
+
* carry implementor-only code.
|
|
9
8
|
*
|
|
10
9
|
* It exists because three separate readers each grew their own answer to "where is the
|
|
11
10
|
* config directory", and all three disagreed: `packages/cli` honoured `XDG_CONFIG_HOME` but
|
|
12
|
-
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and
|
|
13
|
-
*
|
|
11
|
+
* not `NEONCTL_CONFIG_DIR`, `packages/env` honoured the env var but not XDG, and the init
|
|
12
|
+
* flow hardcoded `~/.config/neonctl`. With `XDG_CONFIG_HOME` set, the CLI wrote
|
|
14
13
|
* credentials somewhere the other two never looked.
|
|
15
14
|
*
|
|
16
15
|
* ## The directory
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join, relative, resolve } from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { BootstrapInputError, ensureTargetUsable, FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, templateIds, } from "neon-init/bootstrap";
|
|
5
4
|
import prompts from "prompts";
|
|
6
5
|
import { credentialInputs } from "../_shared/auth_selection.js";
|
|
7
6
|
import { isCi } from "../env.js";
|
|
7
|
+
import { BootstrapInputError, ensureTargetUsable, FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, templateIds, } from "../init/bootstrap.js";
|
|
8
8
|
import { log } from "../log.js";
|
|
9
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
9
10
|
import { detectPackageManager, installedPackageManagers, runCommand, } from "../utils/package_manager.js";
|
|
10
11
|
// The directory positional is optional: omitting it in an interactive terminal
|
|
11
12
|
// prompts for one. In a non-interactive context a missing directory is an error.
|
|
@@ -55,7 +56,7 @@ export const builder = (argv) => argv
|
|
|
55
56
|
default: true,
|
|
56
57
|
},
|
|
57
58
|
link: {
|
|
58
|
-
describe:
|
|
59
|
+
describe: `Run \`${getCliName()} link\` in the scaffolded directory after installing. In interactive mode this is offered as a prompt; use --no-link to skip without being asked.`,
|
|
59
60
|
type: "boolean",
|
|
60
61
|
default: true,
|
|
61
62
|
},
|
|
@@ -160,7 +161,7 @@ const resolveTargetDir = async (props, interactive, template) => {
|
|
|
160
161
|
return resolve(process.cwd(), defaultDirName(template));
|
|
161
162
|
}
|
|
162
163
|
if (!interactive) {
|
|
163
|
-
throw new Error(
|
|
164
|
+
throw new Error(`No target directory given. Pass one, e.g. \`${getCliName()} bootstrap my-app\` (or "." for the current directory).`);
|
|
164
165
|
}
|
|
165
166
|
const { value } = await prompts({
|
|
166
167
|
onState: onPromptState,
|
|
@@ -179,7 +180,7 @@ const resolveTargetDir = async (props, interactive, template) => {
|
|
|
179
180
|
const defaultDirName = (template) => template.source.subdir.split("/").pop() || template.id;
|
|
180
181
|
/**
|
|
181
182
|
* Download and materialize the template into `targetDir`. The actual
|
|
182
|
-
* download/extract/write lives in
|
|
183
|
+
* download/extract/write lives in `src/init/bootstrap.ts`, shared with `neon init`
|
|
183
184
|
* (exec-bit and symlink fidelity, graceful symlink fallback); here we just
|
|
184
185
|
* frame it with progress logging. Returns the number of files written.
|
|
185
186
|
*/
|
|
@@ -238,11 +239,11 @@ const runPostScaffoldSteps = async (props, targetDir, interactive) => {
|
|
|
238
239
|
// and tell the user how to finish by hand.
|
|
239
240
|
if (props.link) {
|
|
240
241
|
if (!installed && hasNeonConfig(targetDir)) {
|
|
241
|
-
log.info(
|
|
242
|
+
log.info(`Skipping the Neon link step: \`${getCliName()} link\` reads this project's neon.ts ` +
|
|
242
243
|
`to pull env vars, which needs its dependencies. Run \`${pm} install\`, ` +
|
|
243
|
-
|
|
244
|
+
`then \`${getCliName()} link\`.`);
|
|
244
245
|
}
|
|
245
|
-
else if (await confirm(
|
|
246
|
+
else if (await confirm(`Link this project to a Neon project now? (runs ${getCliName()} link)`)) {
|
|
246
247
|
await runNeonLink(props, targetDir);
|
|
247
248
|
// link prints its own summary (and pulls env), so end with just the run hint.
|
|
248
249
|
printNextSteps(targetDir, pm, { installed, suggestLink: false });
|
|
@@ -366,7 +367,7 @@ const printNextSteps = (targetDir, pm, opts) => {
|
|
|
366
367
|
log.info(" %s install", pm);
|
|
367
368
|
}
|
|
368
369
|
if (opts.suggestLink) {
|
|
369
|
-
log.info(
|
|
370
|
+
log.info(` ${getCliName()} link`);
|
|
370
371
|
}
|
|
371
372
|
log.info(" See the README to run it.");
|
|
372
373
|
log.info("");
|
|
@@ -400,7 +401,7 @@ const runAgent = async (props) => {
|
|
|
400
401
|
description: template.description,
|
|
401
402
|
...(template.services ? { services: template.services } : {}),
|
|
402
403
|
})),
|
|
403
|
-
next_command_template:
|
|
404
|
+
next_command_template: `${getCliName()} bootstrap --agent ${props.directory ? shellArg(props.directory) : "<directory>"} --template <template_id>`,
|
|
404
405
|
});
|
|
405
406
|
return;
|
|
406
407
|
}
|
|
@@ -413,7 +414,7 @@ const runAgent = async (props) => {
|
|
|
413
414
|
emitAgent({
|
|
414
415
|
status: "needs_directory",
|
|
415
416
|
instruction: 'Ask the user which directory to scaffold into (use "." for the current directory), then re-run the next_command_template with it.',
|
|
416
|
-
next_command_template:
|
|
417
|
+
next_command_template: `${getCliName()} bootstrap --agent <directory> --template ${shellArg(template.id)}`,
|
|
417
418
|
});
|
|
418
419
|
return;
|
|
419
420
|
}
|
|
@@ -441,7 +442,7 @@ const runAgent = async (props) => {
|
|
|
441
442
|
{
|
|
442
443
|
action: "link_neon_project",
|
|
443
444
|
instruction: "Ask the user whether to link the project to a Neon project now. This runs the link state machine — follow its JSON output for the next step.",
|
|
444
|
-
command: `${runIn}
|
|
445
|
+
command: `${runIn}${getCliName()} link --agent`,
|
|
445
446
|
},
|
|
446
447
|
],
|
|
447
448
|
message: `Scaffolded "${template.title}" (${filesWritten} files) into ${dir}. Offer the next_steps to the user: install dependencies, initialize git, then link a Neon project.`,
|
|
@@ -5,6 +5,7 @@ import { applyContext, contextBranch, readContextFile } from "../context.js";
|
|
|
5
5
|
import { isCi } from "../env.js";
|
|
6
6
|
import { log } from "../log.js";
|
|
7
7
|
import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
|
|
8
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
8
9
|
import { fillSingleProject } from "../utils/enrichers.js";
|
|
9
10
|
import { looksLikeBranchId } from "../utils/formats.js";
|
|
10
11
|
import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout, } from "./config.js";
|
|
@@ -104,8 +105,8 @@ export const handler = async (props) => {
|
|
|
104
105
|
if (failure) {
|
|
105
106
|
throw new Error([
|
|
106
107
|
`Branch ${branchName} (${branchId}) was created and checked out, but applying neon.ts to it failed: ${failure}`,
|
|
107
|
-
|
|
108
|
-
`Fix the cause above, then run
|
|
108
|
+
`The branch is usable but does not match the policy, and \`${getCliName()} checkout\` never reconciles a branch that already exists.`,
|
|
109
|
+
`Fix the cause above, then run \`${getCliName()} deploy --update-existing\` 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: \`${getCliName()} branches delete ${branchName}\` then \`${getCliName()} checkout ${branchName}\`.`,
|
|
109
110
|
].join("\n"));
|
|
110
111
|
}
|
|
111
112
|
};
|
|
@@ -129,7 +130,7 @@ const resolveBranchId = async (props, projectId) => {
|
|
|
129
130
|
if (!props.id) {
|
|
130
131
|
const picked = await pickBranchInteractively(branches, {
|
|
131
132
|
message: "Which branch would you like to check out?",
|
|
132
|
-
nonInteractiveMessage:
|
|
133
|
+
nonInteractiveMessage: `No branch specified. Pass a branch name or id (e.g. \`${getCliName()} checkout main\`), ` +
|
|
133
134
|
"or run interactively to pick one from a list.",
|
|
134
135
|
});
|
|
135
136
|
if (picked.kind === "existing") {
|
|
@@ -266,7 +267,7 @@ const resolveProjectId = async (props) => {
|
|
|
266
267
|
}
|
|
267
268
|
const missingProjectMessage = "Could not determine which Neon project to check out a branch from. " +
|
|
268
269
|
"Provide one via the --project-id flag " +
|
|
269
|
-
|
|
270
|
+
`or a .neon file (created by \`${getCliName()} link\` / \`${getCliName()} set-context\`).`;
|
|
270
271
|
if (isCi() || !process.stdout.isTTY) {
|
|
271
272
|
throw new Error(missingProjectMessage);
|
|
272
273
|
}
|
|
@@ -274,7 +275,7 @@ const resolveProjectId = async (props) => {
|
|
|
274
275
|
const { runLink } = await prompts({
|
|
275
276
|
type: "confirm",
|
|
276
277
|
name: "runLink",
|
|
277
|
-
message:
|
|
278
|
+
message: `Run \`${getCliName()} link\` in the current folder to pick a project now?`,
|
|
278
279
|
initial: true,
|
|
279
280
|
});
|
|
280
281
|
if (!runLink) {
|
|
@@ -289,7 +290,7 @@ const resolveProjectId = async (props) => {
|
|
|
289
290
|
});
|
|
290
291
|
const linked = readContextFile(props.contextFile);
|
|
291
292
|
if (!linked.projectId) {
|
|
292
|
-
throw new Error(
|
|
293
|
+
throw new Error(`Linking did not produce a project id. Re-run \`${getCliName()} checkout\` once the directory is linked.`);
|
|
293
294
|
}
|
|
294
295
|
// Carry the freshly-linked org id forward so the merge below keeps it.
|
|
295
296
|
if (linked.orgId) {
|
package/dist/commands/config.js
CHANGED
|
@@ -12,6 +12,7 @@ import { loadEnvFileIntoProcess } from "../env_file.js";
|
|
|
12
12
|
import { log } from "../log.js";
|
|
13
13
|
import { assertAiGatewayProvisionable, warnAiGateway, } from "../utils/ai_gateway_notice.js";
|
|
14
14
|
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
15
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
15
16
|
import { renderAppliedChanges, renderBranchSettingConflicts, } from "../utils/config_diff.js";
|
|
16
17
|
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
17
18
|
import { bundleEntry } from "../utils/esbuild.js";
|
|
@@ -354,7 +355,7 @@ export const status = async (props) => {
|
|
|
354
355
|
else {
|
|
355
356
|
// No branch pinned: hint on stderr and exit non-zero (grep-style) so a prompt's
|
|
356
357
|
// `when` hides the segment cleanly instead of rendering a bare icon.
|
|
357
|
-
log.info(
|
|
358
|
+
log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
358
359
|
process.exitCode = 1;
|
|
359
360
|
}
|
|
360
361
|
return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isNeonApiError, retryOnLock } from "../api.js";
|
|
2
2
|
import { log } from "../log.js";
|
|
3
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
3
4
|
import { branchIdFromProps, fillSingleProject, resolveSingleDatabase, } from "../utils/enrichers.js";
|
|
4
5
|
import { writer } from "../writer.js";
|
|
5
6
|
const SETTINGS_FIELDS = [
|
|
@@ -221,7 +222,7 @@ const update = async (props) => {
|
|
|
221
222
|
}
|
|
222
223
|
catch (err) {
|
|
223
224
|
if (isNeonApiError(err) && err.status === 404) {
|
|
224
|
-
throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run
|
|
225
|
+
throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`${getCliName()} data-api create\` first.`);
|
|
225
226
|
}
|
|
226
227
|
throw err;
|
|
227
228
|
}
|
|
@@ -238,7 +239,7 @@ const update = async (props) => {
|
|
|
238
239
|
}
|
|
239
240
|
catch (err) {
|
|
240
241
|
if (isNeonApiError(err) && err.status === 404) {
|
|
241
|
-
throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run
|
|
242
|
+
throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`${getCliName()} data-api create\` first.`);
|
|
242
243
|
}
|
|
243
244
|
throw err;
|
|
244
245
|
}
|
|
@@ -257,7 +258,7 @@ const refreshSchema = async (props) => {
|
|
|
257
258
|
}
|
|
258
259
|
catch (err) {
|
|
259
260
|
if (isNeonApiError(err) && err.status === 404) {
|
|
260
|
-
throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run
|
|
261
|
+
throw new Error(`Data API is not provisioned for ${database} on branch ${branchId}. Run \`${getCliName()} data-api create\` first.`);
|
|
261
262
|
}
|
|
262
263
|
throw err;
|
|
263
264
|
}
|
package/dist/commands/env.js
CHANGED
|
@@ -7,6 +7,7 @@ import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
|
7
7
|
import { log } from "../log.js";
|
|
8
8
|
import { warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
9
9
|
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
10
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
10
11
|
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
11
12
|
export const command = "env";
|
|
12
13
|
export const describe = "Manage a branch's Neon env variables locally";
|
|
@@ -15,7 +16,7 @@ export const describe = "Manage a branch's Neon env variables locally";
|
|
|
15
16
|
* `--no-env-pull`. Names the two ways to get the branch's vars without an on-disk file written
|
|
16
17
|
* eagerly: an explicit `neonctl env pull`, or runtime injection via `neon-env run`.
|
|
17
18
|
*/
|
|
18
|
-
export const ENV_PULL_SKIPPED_HINT =
|
|
19
|
+
export const ENV_PULL_SKIPPED_HINT = `Skipped env pull (--no-env-pull). Run \`${getCliName()} env pull\` to write this branch’s env vars ` +
|
|
19
20
|
"(DATABASE_URL, …) into a local .env, or inject them at runtime with `neon-env run -- <your dev command>`.";
|
|
20
21
|
export const builder = (argv) => argv
|
|
21
22
|
.usage("$0 env <sub-command> [options]")
|
|
@@ -165,7 +166,7 @@ export const autoPullEnvAfterPin = async (props) => {
|
|
|
165
166
|
catch (err) {
|
|
166
167
|
const message = err instanceof Error ? err.message : String(err);
|
|
167
168
|
log.warning("Branch pinned, but pulling its Neon env vars failed: %s\n" +
|
|
168
|
-
|
|
169
|
+
`Run \`${getCliName()} env pull\` once resolved (e.g. \`${getCliName()} deploy\` if a declared service ` +
|
|
169
170
|
"is missing), or inject them at runtime with `neon-env run -- <your dev command>`.", message);
|
|
170
171
|
return { status: "failed", message };
|
|
171
172
|
}
|
|
@@ -187,10 +188,10 @@ export const renderAgentPullNote = (result) => {
|
|
|
187
188
|
case "empty":
|
|
188
189
|
return " No Neon env vars to pull for this branch yet.";
|
|
189
190
|
case "skipped":
|
|
190
|
-
return (
|
|
191
|
+
return (` Skipped env pull (--no-env-pull); run \`${getCliName()} env pull\` later, ` +
|
|
191
192
|
"or inject env at runtime with `neon-env run -- <your dev command>`.");
|
|
192
193
|
case "failed":
|
|
193
|
-
return ` Could not pull env vars (${result.message}); run
|
|
194
|
+
return ` Could not pull env vars (${result.message}); run \`${getCliName()} env pull\` once resolved.`;
|
|
194
195
|
}
|
|
195
196
|
};
|
|
196
197
|
/**
|
|
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { isNeonApiError, retryOnLock } from "../api.js";
|
|
4
4
|
import { createDeployment, deleteFunction, getFunction, listFunctions, } from "../functions_api.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
6
7
|
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
7
8
|
import { bundleEntry } from "../utils/esbuild.js";
|
|
8
9
|
import { zipBundle } from "../utils/zip.js";
|
|
@@ -146,7 +147,7 @@ const parseEnv = (entries) => {
|
|
|
146
147
|
}
|
|
147
148
|
return JSON.stringify(map);
|
|
148
149
|
};
|
|
149
|
-
const statusHint = (slug, projectId, branchId) => `Check status with:
|
|
150
|
+
const statusHint = (slug, projectId, branchId) => `Check status with: ${getCliName()} function get ${slug} --project-id ${projectId} --branch ${branchId}`;
|
|
150
151
|
// Emit the resolved deployment together with the function's invocation_url, so the
|
|
151
152
|
// deploy output shows where the function is reachable (not just the deployment id).
|
|
152
153
|
const emitDeployResult = (props, deployment, fn) => {
|
|
@@ -172,7 +173,7 @@ const deploy = async (props) => {
|
|
|
172
173
|
props.runtime !== undefined;
|
|
173
174
|
if (!hasOption) {
|
|
174
175
|
throw new Error("Provide at least one option to deploy, e.g. --src or --env. " +
|
|
175
|
-
|
|
176
|
+
`See: ${getCliName()} function deploy --help.`);
|
|
176
177
|
}
|
|
177
178
|
// Cheap, offline validation first - fail before any network round-trip.
|
|
178
179
|
if (!SLUG_PATTERN.test(props.slug)) {
|
package/dist/commands/init.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import { detectAgent, enrichResponse, interactiveInit, orchestrate, routeDataStep, } from "neon-init";
|
|
2
1
|
import { credentialInputs } from "../_shared/auth_selection.js";
|
|
3
|
-
import { sendError } from "../analytics.js";
|
|
4
|
-
import {
|
|
2
|
+
import { closeAnalytics, sendError } from "../analytics.js";
|
|
3
|
+
import { detectAgent } from "../init/detect_agent.js";
|
|
4
|
+
import { enrichResponse } from "../init/enrich_output.js";
|
|
5
|
+
import { interactiveInit } from "../init/interactive.js";
|
|
6
|
+
import { orchestrate } from "../init/orchestrate.js";
|
|
7
|
+
import { routeDataStep } from "../init/route_command.js";
|
|
8
|
+
import { STDOUT_FD, writeAllSync } from "../utils/write_sync.js";
|
|
5
9
|
export const command = "init";
|
|
6
10
|
export const describe = "Initialize a project with Neon using your AI coding assistant";
|
|
7
11
|
export const builder = (yargs) => yargs
|
|
@@ -29,62 +33,103 @@ export const builder = (yargs) => yargs
|
|
|
29
33
|
describe: "Enable preview features (e.g. project bootstrapping from templates).",
|
|
30
34
|
})
|
|
31
35
|
.strict(false);
|
|
36
|
+
/**
|
|
37
|
+
* The agent-facing half of `neon init` speaks JSON, and it speaks it on **stdout**:
|
|
38
|
+
* one object, no prefix, nothing else. `log.info` would prefix every line with
|
|
39
|
+
* `INFO: ` and send it to stderr, which is right for a diagnostic and wrong for the
|
|
40
|
+
* payload an agent is expected to parse.
|
|
41
|
+
*/
|
|
42
|
+
const writeAgentResponse = (result) => {
|
|
43
|
+
writeAllSync(STDOUT_FD, `${JSON.stringify(enrichResponse(result), null, 2)}\n`);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* A failure has to arrive in the shape the caller asked for. An agent parses stdout and
|
|
47
|
+
* has no branch for "empty stdout, exit 1" — it cannot tell a broken credentials file
|
|
48
|
+
* from a phase that legitimately produced nothing — so the error goes out as JSON too.
|
|
49
|
+
*/
|
|
50
|
+
const writeAgentFailure = (error) => {
|
|
51
|
+
writeAllSync(STDOUT_FD, `${JSON.stringify({ success: false, error: error.message }, null, 2)}\n`);
|
|
52
|
+
};
|
|
53
|
+
/** ` at position 12`, or nothing when the parser did not report one. */
|
|
54
|
+
const parsePosition = (parseError) => {
|
|
55
|
+
const message = parseError instanceof Error ? parseError.message : "";
|
|
56
|
+
const at = message.match(/at position (\d+)/);
|
|
57
|
+
return at === null ? "" : ` at position ${at[1]}`;
|
|
58
|
+
};
|
|
32
59
|
export const handler = async (argv) => {
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// default account would be worse than refusing, because naming an account is the entire
|
|
37
|
-
// job of the thing being ignored.
|
|
60
|
+
// Auto-detect agent from environment. When --agent is explicitly passed,
|
|
61
|
+
// always detect (the user asked for agent mode). Otherwise, require
|
|
62
|
+
// non-TTY stdin to distinguish agent from human in terminal.
|
|
38
63
|
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
64
|
+
// Resolved before anything can fail, so that every failure this handler sees —
|
|
65
|
+
// including the profile refusal — is reported in the shape the caller can read.
|
|
66
|
+
// Failures raised by `ensureAuth` are not among them: it resolves credentials
|
|
67
|
+
// above its own `init` skip, so an unknown profile, a contradictory
|
|
68
|
+
// `--api-key`/`--profile` pair, and a damaged credentials file all report on
|
|
69
|
+
// stderr before this runs.
|
|
70
|
+
const agent = (argv.agent || !process.stdin.isTTY ? detectAgent() : null) ||
|
|
71
|
+
undefined;
|
|
72
|
+
const isAgentMode = argv.agent || agent !== undefined;
|
|
49
73
|
try {
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
74
|
+
// The init flow reads the default credentials directly and re-invokes the CLI as a
|
|
75
|
+
// subprocess. It has no way to be told which profile to use, so honouring a selection
|
|
76
|
+
// here is not possible yet — and silently running as the default account would be
|
|
77
|
+
// worse than refusing, because naming an account is the entire job of the thing being
|
|
78
|
+
// ignored.
|
|
79
|
+
//
|
|
80
|
+
// `NEON_PROFILE` counts just as much as the flag. Checking only the flag left the case
|
|
81
|
+
// that is easier to hit by accident: a profile exported once into a shell then
|
|
82
|
+
// silently disregarded by every `neon init` run in it.
|
|
83
|
+
const selectedProfile = argv.profile?.trim() || credentialInputs().profileEnv.trim();
|
|
84
|
+
if (selectedProfile) {
|
|
85
|
+
const how = argv.profile?.trim()
|
|
86
|
+
? "--profile was passed, so"
|
|
87
|
+
: "NEON_PROFILE is set, so";
|
|
88
|
+
throw new Error(`${how} \`neon init\` would run as the default account instead of "${selectedProfile}", and it does not support profile selection yet. Run it without one, or set the project up with \`neon --profile ${selectedProfile} link\`.`);
|
|
89
|
+
}
|
|
56
90
|
// --data with a "step" field routes to the appropriate phase
|
|
57
91
|
if (argv.data && isAgentMode) {
|
|
58
92
|
let data;
|
|
59
93
|
try {
|
|
60
94
|
data = JSON.parse(argv.data);
|
|
61
95
|
}
|
|
62
|
-
catch {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
catch (parseError) {
|
|
97
|
+
// Neither the payload nor the parser's message may appear here. `--data`
|
|
98
|
+
// carries whatever the caller put in it — a connection string, an API key —
|
|
99
|
+
// and V8 quotes a window of the input around the syntax error, so both would
|
|
100
|
+
// travel into the error message, onto stdout, and into `sendError`'s
|
|
101
|
+
// analytics payload. `shared/cli-core/src/credentials.ts` discards the same
|
|
102
|
+
// message for the same reason. The position is a number and says enough.
|
|
103
|
+
throw new Error(`Invalid JSON in --data flag${parsePosition(parseError)}. Expected a JSON object.`);
|
|
66
104
|
}
|
|
67
105
|
if (typeof data.step === "string") {
|
|
68
|
-
|
|
69
|
-
log.info(JSON.stringify(enrichResponse(result), null, 2));
|
|
106
|
+
writeAgentResponse(await routeDataStep(data, agent));
|
|
70
107
|
return;
|
|
71
108
|
}
|
|
72
109
|
}
|
|
73
110
|
if (isAgentMode) {
|
|
74
|
-
|
|
111
|
+
writeAgentResponse(await orchestrate({
|
|
75
112
|
agent,
|
|
76
113
|
skipMigrations: argv.skipMigrations,
|
|
77
114
|
preview: argv.preview,
|
|
78
|
-
});
|
|
79
|
-
log.info(JSON.stringify(enrichResponse(result), null, 2));
|
|
115
|
+
}));
|
|
80
116
|
}
|
|
81
117
|
else {
|
|
82
118
|
await interactiveInit({ preview: argv.preview });
|
|
83
119
|
}
|
|
84
120
|
}
|
|
85
|
-
catch {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
121
|
+
catch (error) {
|
|
122
|
+
const cause = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
if (isAgentMode) {
|
|
124
|
+
// Agent mode answers and exits here, so nothing else will report this. Attribute
|
|
125
|
+
// it to init and flush before exiting — `process.exit` drops in-flight events.
|
|
126
|
+
sendError(cause, "NEON_INIT_FAILED");
|
|
127
|
+
writeAgentFailure(cause);
|
|
128
|
+
await closeAnalytics();
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
// A human gets the top-level handler's single `ERROR: <message>` line on stderr, and
|
|
132
|
+
// its `sendError`. Reporting here as well would file one failure as two events.
|
|
133
|
+
throw cause;
|
|
89
134
|
}
|
|
90
135
|
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { log } from "../log.js";
|
|
2
2
|
import { projectUpdateRequest } from "../parameters.gen.js";
|
|
3
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
3
4
|
import { fillSingleProject } from "../utils/enrichers.js";
|
|
4
5
|
import { writer } from "../writer.js";
|
|
5
6
|
const IP_ALLOW_FIELDS = [
|
|
@@ -68,7 +69,7 @@ const list = async (props) => {
|
|
|
68
69
|
const add = async (props) => {
|
|
69
70
|
if (props.ips.length <= 0) {
|
|
70
71
|
throw new Error(`Enter individual IP addresses, define ranges with a dash, or use CIDR notation for more flexibility.
|
|
71
|
-
Example:
|
|
72
|
+
Example: ${getCliName()} ip-allow add 192.168.1.1, 192.168.1.20-192.168.1.50, 192.168.1.0/24 --project-id <id>`);
|
|
72
73
|
}
|
|
73
74
|
const project = {};
|
|
74
75
|
const { data } = await props.apiClient.getProject(props.projectId);
|
|
@@ -90,7 +91,7 @@ const add = async (props) => {
|
|
|
90
91
|
};
|
|
91
92
|
const remove = async (props) => {
|
|
92
93
|
if (props.ips.length <= 0) {
|
|
93
|
-
throw new Error(`Remove individual IP addresses and ranges. Example:
|
|
94
|
+
throw new Error(`Remove individual IP addresses and ranges. Example: ${getCliName()} ip-allow remove 192.168.1.1 --project-id <id>`);
|
|
94
95
|
}
|
|
95
96
|
const project = {};
|
|
96
97
|
const { data } = await props.apiClient.getProject(props.projectId);
|