neon 4.21.1 → 5.0.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 +35 -27
- package/dist/commands/bootstrap.js +3 -3
- package/dist/commands/checkout.js +2 -2
- package/dist/commands/config.js +1 -1
- package/dist/commands/diff.js +2 -2
- package/dist/commands/env.js +1 -1
- package/dist/commands/init.js +2 -2
- package/dist/commands/link.js +205 -116
- package/dist/commands/logs.js +1 -1
- package/dist/commands/schema_diff.js +3 -5
- package/dist/commands/set_context.js +1 -1
- package/dist/plugins/run.js +1 -0
- package/dist/skills/catalog.js +5 -0
- package/dist/utils/enrichers.js +26 -8
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -298,15 +298,15 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
|
|
|
298
298
|
|
|
299
299
|
## Linking a project
|
|
300
300
|
|
|
301
|
-
`neon link` is a Vercel-style command that binds the current directory to a Neon project. It picks (or creates) an organization and a project and writes a `.neon` file (`{ "orgId", "projectId", "branch" }`) that subsequent commands run in this directory (or any sub-directory) pick up automatically.
|
|
301
|
+
`neon link` is a Vercel-style command that binds the current directory to a Neon project. It picks (or creates) an organization and a project and writes a `.neon` file (`{ "orgId", "projectId", "branch" }`) that subsequent commands run in this directory (or any sub-directory) pick up automatically. Personal-account projects omit `orgId`.
|
|
302
302
|
|
|
303
|
-
`link` resolves what it can and **verifies every identifier you pass** before writing, so a `.neon`
|
|
303
|
+
`link` resolves what it can and **verifies every identifier you pass** before writing, so a successful `.neon` always has a project and a branch (and an organization when the project has one):
|
|
304
304
|
|
|
305
|
-
- **org** is inferred from the project (so `--project-id` alone is enough); it's omitted only when the project has no organization (personal account).
|
|
306
|
-
- **project** is taken from `--project-id` (or chosen interactively).
|
|
307
|
-
- **branch** is taken from `--branch`,
|
|
305
|
+
- **org** is inferred from the project (so `--project-id` alone is enough); it's omitted only when the project has no organization (personal account). `--org-id` that the project does not confirm is an error.
|
|
306
|
+
- **project** is taken from `--project-id` (or chosen interactively). `--org-id` without a project opens the project picker in a TTY. Without a TTY it needs `-y` (select the only project, or print IDs) or `--project-id`.
|
|
307
|
+
- **branch** is taken from `--branch`, a still-valid pin for the same project, or the project's branch list: one branch is pinned automatically; several prompt in a TTY, or `-y` pins the default. Zero branches, a stale pin, or several branches with no TTY and no `-y` fail without writing.
|
|
308
308
|
|
|
309
|
-
When a branch
|
|
309
|
+
When a branch is pinned, `link` also runs [`env pull`](#env-pull) so the branch's Neon env vars (`DATABASE_URL`, …) land in a local `.env`. Pass `--no-env-pull` to skip the pull (for example when injecting env at runtime with `neon-env run` or `neon dev`).
|
|
310
310
|
|
|
311
311
|
> **Migrating from `set-context`?** `set-context` is **deprecated** in favor of `link` (see [below](#set-context-is-deprecated)). It still works exactly as before for now (a raw write), it just prints a deprecation warning. The `.neon` `branchId` field is also superseded by `branch` (which stores the branch **name** when known); old `branchId` files are still read and are upgraded to `branch` the next time `link`/`checkout` writes the context.
|
|
312
312
|
|
|
@@ -339,19 +339,24 @@ $ neon link
|
|
|
339
339
|
```
|
|
340
340
|
|
|
341
341
|
`link --project-id …` skips org and project. One branch is pinned with no prompt. Several branches
|
|
342
|
-
in a TTY show the branch prompt; `-y` pins the default; no TTY
|
|
343
|
-
`neon checkout`:
|
|
342
|
+
in a TTY show the branch prompt; `-y` pins the default; no TTY without `-y` or `--branch` exits 1:
|
|
344
343
|
|
|
345
344
|
```bash
|
|
346
345
|
$ neon link --project-id polished-snowflake-12345678
|
|
347
346
|
? Which branch would you like to link? › [default] main (br-main-branch-87654321)
|
|
348
347
|
```
|
|
349
348
|
|
|
350
|
-
**Non-interactive (flags or `--params` JSON)** — for scripts and
|
|
349
|
+
**Non-interactive (`-y`, flags, or `--params` JSON)** — for scripts, CI, and agents:
|
|
351
350
|
|
|
352
351
|
```bash
|
|
352
|
+
# One organization and one project: link them. Several: print IDs and exit 1.
|
|
353
|
+
neon link -y
|
|
354
|
+
|
|
355
|
+
# After choosing an organization from that list, discover its project the same way
|
|
356
|
+
neon link -y --org-id org-abc123
|
|
357
|
+
|
|
353
358
|
# Link to an existing project (org is inferred). Pins the only branch;
|
|
354
|
-
# several branches prompt in a TTY, or
|
|
359
|
+
# several branches prompt in a TTY, or -y pins the default.
|
|
355
360
|
neon link --project-id polished-snowflake-12345678
|
|
356
361
|
|
|
357
362
|
# Same, pin the project's default branch when several exist
|
|
@@ -369,21 +374,21 @@ neon link --org-id org-abc123 --project-name my-app --region-id aws-us-east-2
|
|
|
369
374
|
# Same payload, one JSON blob
|
|
370
375
|
neon link --params '{"orgId":"org-abc123","projectName":"my-app","regionId":"aws-us-east-2"}'
|
|
371
376
|
|
|
372
|
-
# Record just the default org (preserves any existing project/branch)
|
|
373
|
-
neon link --org-id org-abc123
|
|
374
|
-
|
|
375
377
|
# Forget the current context
|
|
376
378
|
neon link --clear
|
|
377
379
|
|
|
378
380
|
# Offline write — no API calls, no verification (see --no-checks below)
|
|
379
|
-
neon link --no-checks --org-id org-abc123 --project-id polished-snowflake-12345678
|
|
381
|
+
neon link --no-checks --org-id org-abc123 --project-id polished-snowflake-12345678 --branch main
|
|
380
382
|
```
|
|
381
383
|
|
|
382
384
|
Every supplied identifier is checked before anything is written, with actionable errors — e.g. `Project '…' not found`, `You don't have access to project '…'`, `Organization '…' not found, or your API key doesn't have access to it`, `Project '…' belongs to organization 'A', not 'B'`, or `Branch '…' not found in project '…'. Available branches: …`.
|
|
383
385
|
|
|
384
|
-
**Agents and scripts (
|
|
386
|
+
**Agents and scripts:** `neon link -y` selects the only organization and project. If several exist, it prints their IDs (human table, or `--output json` / `--output yaml`) and names the flag to pass. `--org-id` alone, without `-y` and without a TTY, is not a completed link — pass `--project-id` or `-y`. `neon link --help` prints the explicit-flag recipe.
|
|
385
387
|
|
|
386
388
|
```bash
|
|
389
|
+
neon link -y
|
|
390
|
+
neon link -y --org-id <org-id>
|
|
391
|
+
neon link -y --project-id <project-id>
|
|
387
392
|
neon orgs list --output json
|
|
388
393
|
neon projects list --org-id <org-id> --output json
|
|
389
394
|
neon link --project-id <project-id> [--branch <name> | -y]
|
|
@@ -397,7 +402,7 @@ Organization-scoped API keys cannot list user organizations (`orgs list`) or cal
|
|
|
397
402
|
- If no projects exist yet, interactive `link` errors pointing at `--org-id`.
|
|
398
403
|
- When the regions endpoint is not allowed, interactive create falls back to a built-in static region list. Non-interactive create already requires `--region-id`.
|
|
399
404
|
|
|
400
|
-
**Offline writes (`--no-checks`)** — write the `.neon` with no API calls at all: no org inference, no existence/access verification, no env pull. Because nothing can be resolved offline, it requires
|
|
405
|
+
**Offline writes (`--no-checks`)** — write the `.neon` with no API calls at all: no org inference, no existence/access verification, no env pull. Because nothing can be resolved offline, it requires `--org-id`, `--project-id`, and `--branch` (stored verbatim). Handy for scripted/CI setups or re-creating a `.neon` from values you already trust:
|
|
401
406
|
|
|
402
407
|
```bash
|
|
403
408
|
neon link --no-checks --org-id org-abc123 --project-id polished-snowflake-12345678 --branch main
|
|
@@ -411,13 +416,13 @@ How today's `set-context` uses map onto `link`:
|
|
|
411
416
|
|
|
412
417
|
| `set-context` (deprecated) | Recommended `link` equivalent |
|
|
413
418
|
| --------------------------------------- | ----------------------------------------------------------------------------- |
|
|
414
|
-
| `neon set-context --project-id <id>` | `neon link --project-id <id>` (infers org + verifies; pins the only branch) |
|
|
415
|
-
| `neon set-context --org-id <id>` |
|
|
416
|
-
| `neon set-context --branch-id <id>` | `neon link --branch <name\|id>`
|
|
419
|
+
| `neon set-context --project-id <id>` | `neon link --project-id <id>` (infers org + verifies; pins the only branch, or `-y` for the default) |
|
|
420
|
+
| `neon set-context --org-id <id>` | Not a completed link. Pass `--project-id` / `-y`, or an `--org-id` flag on the org-scoped command |
|
|
421
|
+
| `neon set-context --branch-id <id>` | `neon link --branch <name\|id>` or [`neon checkout <branch>`](#checkout) |
|
|
417
422
|
| `neon set-context` (clear) | `neon link --clear` |
|
|
418
|
-
| a raw local write (no network) | `neon link --no-checks --org-id <id> --project-id <id>`
|
|
423
|
+
| a raw local write (no network) | `neon link --no-checks --org-id <id> --project-id <id> --branch <name>` |
|
|
419
424
|
|
|
420
|
-
The key difference: `link` resolves and **verifies** before writing
|
|
425
|
+
The key difference: `link` resolves and **verifies** before writing a complete context, whereas `set-context` writes whatever you give it verbatim. The closest like-for-like replacement for the old raw write is `link --no-checks` with org, project, and branch.
|
|
421
426
|
|
|
422
427
|
### open
|
|
423
428
|
|
|
@@ -434,7 +439,7 @@ A branch pinned in `.neon` does not change the destination. `.neon` stores the b
|
|
|
434
439
|
|
|
435
440
|
### checkout
|
|
436
441
|
|
|
437
|
-
`checkout [id|name]`
|
|
442
|
+
`checkout [id|name]` switches the pinned branch in a linked folder. It resolves the branch (by name or id) against the project, then **heals** the `.neon` file: it always (re)writes `projectId`, `branch`, and `orgId` (when the project has one), so a `.neon` that was missing fields or drifted ends up complete and consistent. The branch is stored as its **name** when known (matching `link`). When `orgId` isn't already known (from `--org-id` or the existing `.neon`), it's looked up from the project itself.
|
|
438
443
|
|
|
439
444
|
The branch argument is **optional**: run `neon checkout` with no branch in an interactive terminal to fetch the project's branches and pick one from a list. In a non-interactive context (CI or no TTY), a branch must be passed explicitly.
|
|
440
445
|
|
|
@@ -749,7 +754,7 @@ When a package cannot be bundled — a native addon with no esbuild loader, or a
|
|
|
749
754
|
|
|
750
755
|
`neon bootstrap` copies a Neon starter template into a new (or current) directory — conceptually like `degit`, but it only pulls from a small set of templates we maintain in the public [`neondatabase/examples`](https://github.com/neondatabase/examples) repo. The template copy needs no Neon login: it downloads files from GitHub.
|
|
751
756
|
|
|
752
|
-
After scaffolding, an interactive terminal asks about dependency install and git, then finishes agent setup before asking whether to link a Neon project. Dependency install is last, except when the template has a `neon.ts` and you chose to link — then install runs first so the link flow can pull env. `--default` / `-y` skips the template, install, git, and agent pickers, then installs agent tooling for project folders, else the host CLI agent. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `--default` / `-y` in a terminal to pick. `--agent` / `-a` names coding agents, skips agent selection, and is forwarded to `plugins`, or to `skills` and `mcp`, not both.
|
|
757
|
+
After scaffolding, an interactive terminal asks about dependency install and git, then finishes agent setup before asking whether to link a Neon project. Dependency install is last, except when the template has a `neon.ts` and you chose to link — then install runs first so the link flow can pull env. `--default` / `-y` skips the template, install, git, and agent pickers, then installs agent tooling for project folders, else the host CLI agent. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `--default` / `-y` in a terminal to pick. `--agent` / `-a` names coding agents, skips agent selection, and is forwarded to `plugins`, or to `skills` and `mcp`, not both. Linking uses `neon link -y`: the only organization and project are selected, or their IDs are printed and the command exits. `--no-agent-setup` and `--no-link` skip those steps. Non-interactive without `--default` prints next steps and does not install, set up agents, or link.
|
|
753
758
|
|
|
754
759
|
Pass a target directory (or `.` for the current one). In an interactive terminal you pick the template from a list; in CI / non-interactive contexts pass `--template <id>`.
|
|
755
760
|
|
|
@@ -802,7 +807,7 @@ Without a TTY, pass `-y`. `--agent` skips agent selection but does not replace `
|
|
|
802
807
|
|
|
803
808
|
`--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`.
|
|
804
809
|
|
|
805
|
-
`-y` forwards `-y` to `plugins` or `skills`/`mcp`, uses
|
|
810
|
+
`-y` forwards `-y` to `plugins` or `skills`/`mcp`, uses `link -y` (auto-select or print IDs and exit), passes `--default` to nested bootstrap, and forwards `--services none` to `config init`. `--agent` is forwarded to agent setup. `mcp -y` is the global install. Default linking skips the "already linked" confirmation.
|
|
806
811
|
|
|
807
812
|
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.
|
|
808
813
|
|
|
@@ -863,6 +868,9 @@ $ neon skills -y -s neon -s neon-ai-gateway
|
|
|
863
868
|
# Named skills into a named agent.
|
|
864
869
|
$ neon skills -s neon -s neon-ai-gateway --agent cursor
|
|
865
870
|
|
|
871
|
+
# One skill, no prompts.
|
|
872
|
+
$ neon skills -s neon-auth --agent cursor -y
|
|
873
|
+
|
|
866
874
|
# User-level skills.
|
|
867
875
|
$ neon skills --global
|
|
868
876
|
|
|
@@ -876,7 +884,7 @@ On a TTY the command asks which agents and which skills, then shows a summary to
|
|
|
876
884
|
|
|
877
885
|
`-y` skips those questions and installs the default skills into detected agents: project-folder markers such as `.cursor`, else the agent driving the CLI. `--agent` / `-a` names coding agents and skips the agent picker. `--skill` / `-s` names specific skills and skips the skill picker; it does not select agents. `--global -y` uses installed apps, else the host CLI agent. Without a TTY, pass `-y`, or `--skill <name>` (add `--agent <name>` to name agents). If `-y` finds no agent, it exits: pass `--agent <name>`, run from a supported agent, or omit `-y` in a terminal to pick.
|
|
878
886
|
|
|
879
|
-
`--skill` names by source repo: `neondatabase/agent-skills` (`neon`, `neon-ai-gateway`, `neon-functions`, `neon-object-storage`, `neon-postgres`, `neon-postgres-branches`, `neon-postgres-egress-optimizer`); `neondatabase/neon-for-agent-platforms` (`neon-postgres-agent-platforms`).
|
|
887
|
+
`--skill` names by source repo: `neondatabase/agent-skills` (`neon`, `neon-ai-gateway`, `neon-auth`, `neon-functions`, `neon-object-storage`, `neon-postgres`, `neon-postgres-branches`, `neon-postgres-egress-optimizer`); `neondatabase/neon-for-agent-platforms` (`neon-postgres-agent-platforms`).
|
|
880
888
|
|
|
881
889
|
Supported agents match `neon mcp`, minus agents that cannot install skills: `antigravity`, `cline`, `cline-cli`, `claude-code`, `claude-desktop`, `codex`, `cursor`, `gemini-cli`, `goose`, `github-copilot-cli`, `grok-build`, `opencode`, `vscode`, `windsurf`, `zed`. `mcporter` is a known MCP name that is then skipped. `neon skills --help` lists the same skill and agent values, and that `-y` leaves out `neon-postgres-agent-platforms`.
|
|
882
890
|
|
|
@@ -917,7 +925,7 @@ Default scope is `project`. `--global` is `user`. On macOS and Linux, Cursor and
|
|
|
917
925
|
|
|
918
926
|
Supported agents with a plugins mapping: `claude-code`, `claude-desktop`, `codex`, `cursor`, `github-copilot-cli`, `grok-build`, `vscode`. `claude-desktop` installs as Claude Code; detecting both produces one install and lists both names in the table. `mcporter` is a known MCP name that is then skipped.
|
|
919
927
|
|
|
920
|
-
The plugins CLI installs every plugin it finds in the Neon plugin package. Today that is `neon-postgres` from `neondatabase/agent-skills`. It includes the Neon MCP server (`https://mcp.neon.tech/mcp`) and these skills: `neon`, `neon-ai-gateway`, `neon-functions`, `neon-object-storage`, `neon-postgres`, `neon-postgres-branches`, `neon-postgres-egress-optimizer`. It does not include `neon-postgres-agent-platforms`.
|
|
928
|
+
The plugins CLI installs every plugin it finds in the Neon plugin package. Today that is `neon-postgres` from `neondatabase/agent-skills`. It includes the Neon MCP server (`https://mcp.neon.tech/mcp`) and these skills: `neon`, `neon-ai-gateway`, `neon-auth`, `neon-functions`, `neon-object-storage`, `neon-postgres`, `neon-postgres-branches`, `neon-postgres-egress-optimizer`. It does not include `neon-postgres-agent-platforms`.
|
|
921
929
|
|
|
922
930
|
`neon plugins --help` lists the plugin, those contents, and the supported agent names.
|
|
923
931
|
|
|
@@ -1012,7 +1020,7 @@ neon inspect db stalled-queries --output json
|
|
|
1012
1020
|
|
|
1013
1021
|
## Logs (`logs`)
|
|
1014
1022
|
|
|
1015
|
-
`neon logs` reads the log records the services on a branch emit — Neon Functions, object storage, and Postgres computes. **Logs
|
|
1023
|
+
`neon logs` reads the log records the services on a branch emit — Neon Functions, object storage, and Postgres computes. **Logs are currently available only for projects in `aws-us-east-2`, `aws-us-east-1`, `aws-eu-central-1`, and `aws-ap-southeast-1`.**
|
|
1016
1024
|
|
|
1017
1025
|
Every sub-command resolves the project through the standard chain (`--project-id`, then the `.neon` context file, then a single-project auto-detect), and takes `--branch <id|name>`, defaulting to the project's default branch. `logs query` searches the previous hour by default; `logs field-values` searches the previous six hours. The maximum time window is seven days.
|
|
1018
1026
|
|
|
@@ -49,7 +49,7 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
|
|
|
49
49
|
},
|
|
50
50
|
default: {
|
|
51
51
|
alias: "y",
|
|
52
|
-
describe: "Quick start: scaffold the default template (or --template), then install, git, agent tooling (project folders, else the host CLI agent; if none, pass --agent or omit --default in a terminal), and link with defaults.
|
|
52
|
+
describe: "Quick start: scaffold the default template (or --template), then install, git, agent tooling (project folders, else the host CLI agent; if none, pass --agent or omit --default in a terminal), and link with defaults. If several organizations or projects exist, link prints IDs and exits",
|
|
53
53
|
type: "boolean",
|
|
54
54
|
default: false
|
|
55
55
|
},
|
|
@@ -80,7 +80,7 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
|
|
|
80
80
|
describe: "Coding agent to install into (repeatable). Forwarded to plugins, or to skills and mcp. Skips agent selection. Values listed below",
|
|
81
81
|
coerce: coerceAgentFlag
|
|
82
82
|
}
|
|
83
|
-
}).example("$0 bootstrap my-app", "Create ./my-app from an interactively chosen template").example("$0 bootstrap . --template hono", "Scaffold the Hono template into the current directory").example("$0 bootstrap my-app --default", "Skip the pickers;
|
|
83
|
+
}).example("$0 bootstrap my-app", "Create ./my-app from an interactively chosen template").example("$0 bootstrap . --template hono", "Scaffold the Hono template into the current directory").example("$0 bootstrap my-app --default", "Skip the pickers; if several orgs or projects exist, link prints IDs and exits").example("$0 bootstrap --list-templates --output json", "Print the template catalog as JSON").example("$0 bootstrap my-app --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with --default.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).strict();
|
|
84
84
|
const handler = async (props) => {
|
|
85
85
|
if (props.listTemplates) {
|
|
86
86
|
const templates = await fetchTemplates();
|
|
@@ -244,7 +244,7 @@ const runPostScaffoldSteps = async (props, targetDir, interactive, template, nam
|
|
|
244
244
|
};
|
|
245
245
|
const installPrompt = (inferred) => inferred ? `Install dependencies with ${inferred}?` : "Install dependencies?";
|
|
246
246
|
const runDefaultSteps = async (props, targetDir, pm, neonConfig, template, named) => {
|
|
247
|
-
log.info("Quick start (--default): skipping the template, install, git, and agent pickers.
|
|
247
|
+
log.info("Quick start (--default): skipping the template, install, git, and agent pickers. If several organizations or projects exist, link prints IDs and exits.");
|
|
248
248
|
const wantGit = props.git && !isGitRepo(targetDir);
|
|
249
249
|
const agentSetup = props.agentSetup === false ? "skip" : named.length > 0 ? chooseYesAgentTooling(named).setup : "skills-mcp";
|
|
250
250
|
const outcome = await executePostScaffold(props, targetDir, {
|
|
@@ -6,7 +6,7 @@ import { applyContext, contextBranch, readContextFile } from "../context.js";
|
|
|
6
6
|
import { getCliName } from "../utils/cli_name.js";
|
|
7
7
|
import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
|
|
8
8
|
import { looksLikeBranchId } from "../utils/formats.js";
|
|
9
|
-
import { fillSingleProject } from "../utils/enrichers.js";
|
|
9
|
+
import { fillSingleProject, listAllProjectBranches } from "../utils/enrichers.js";
|
|
10
10
|
import { autoPullEnvAfterPin } from "./env.js";
|
|
11
11
|
import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout, envFlag } from "./config.js";
|
|
12
12
|
import { handler as handler$1 } from "./link.js";
|
|
@@ -110,7 +110,7 @@ const applyPolicyOrDescribeFailure = async (props) => {
|
|
|
110
110
|
}
|
|
111
111
|
};
|
|
112
112
|
const resolveBranchId = async (props, projectId) => {
|
|
113
|
-
const branches =
|
|
113
|
+
const branches = await listAllProjectBranches(props.apiClient, projectId);
|
|
114
114
|
if (!props.id) {
|
|
115
115
|
const picked = await pickBranchInteractively(branches, {
|
|
116
116
|
message: "Which branch would you like to check out?",
|
package/dist/commands/config.js
CHANGED
|
@@ -10,10 +10,10 @@ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOpt
|
|
|
10
10
|
import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE, FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView } from "../config_template.js";
|
|
11
11
|
import { loadEnvFileIntoProcess } from "../env_file.js";
|
|
12
12
|
import { writer } from "../writer.js";
|
|
13
|
+
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
13
14
|
import { assertAiGatewayProvisionable, warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
14
15
|
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
15
16
|
import { renderAppliedChanges, renderBranchSettingConflicts, renderCustomDomainFollowup } from "../utils/config_diff.js";
|
|
16
|
-
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
17
17
|
import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
|
|
18
18
|
import { bundleEntry } from "../utils/esbuild.js";
|
|
19
19
|
import { pickServicesInteractively } from "../utils/service_picker.js";
|
package/dist/commands/diff.js
CHANGED
|
@@ -3,7 +3,7 @@ import { log } from "../log.js";
|
|
|
3
3
|
import { isNeonApiError } from "../api.js";
|
|
4
4
|
import { writer } from "../writer.js";
|
|
5
5
|
import { looksLikeBranchId } from "../utils/formats.js";
|
|
6
|
-
import { fillSingleProject } from "../utils/enrichers.js";
|
|
6
|
+
import { fillSingleProject, listAllProjectBranches } from "../utils/enrichers.js";
|
|
7
7
|
import { renderDatabaseSchemaDiff, renderSchemaDiffReport } from "../utils/git_diff.js";
|
|
8
8
|
import chalk from "chalk";
|
|
9
9
|
//#region src/commands/diff.ts
|
|
@@ -43,7 +43,7 @@ const builder = (argv) => argv.usage("$0 diff [compare-branch] [options]").posit
|
|
|
43
43
|
["$0 diff main --db neondb", "Diff only the neondb database against main"]
|
|
44
44
|
]);
|
|
45
45
|
const handler = async (props) => {
|
|
46
|
-
const branches =
|
|
46
|
+
const branches = await listAllProjectBranches(props.apiClient, props.projectId);
|
|
47
47
|
const after = resolveAfterBranch(branches, props.branch);
|
|
48
48
|
const before = resolveBeforeBranch(branches, after, props.compareBranch);
|
|
49
49
|
if (before.branchId === after.branchId) throw new Error(`Nothing to compare: both sides resolve to branch ${after.branchName} (${after.branchId}).`);
|
package/dist/commands/env.js
CHANGED
|
@@ -6,9 +6,9 @@ import { getCliName } from "../utils/cli_name.js";
|
|
|
6
6
|
import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
|
|
7
7
|
import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
8
8
|
import { a as envKeysForSelection, b as isFunctionBaseUrlKey, c as ownedEnvServiceKeys, f as NEON_ENV_VAR_KEYS, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, o as envPullFlagValue, r as ENV_PULL_KEY_HELP } from "../_chunks/env_services-DlXECBPi.js";
|
|
9
|
+
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
9
10
|
import { warnAiGateway } from "../utils/ai_gateway_notice.js";
|
|
10
11
|
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
11
|
-
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
12
12
|
import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-DRpFX-_d.js";
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import chalk from "chalk";
|
package/dist/commands/init.js
CHANGED
|
@@ -36,7 +36,7 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
|
|
|
36
36
|
alias: "y",
|
|
37
37
|
type: "boolean",
|
|
38
38
|
default: false,
|
|
39
|
-
describe: "Empty dir: scaffold the default template. --skip-template: plugin, or skills and MCP, for project folders, else the host CLI agent. Exits if none. Then link with defaults and create the bare neon.ts policy.
|
|
39
|
+
describe: "Empty dir: scaffold the default template. --skip-template: plugin, or skills and MCP, for project folders, else the host CLI agent. Exits if none. Then link with defaults and create the bare neon.ts policy. If several organizations or projects exist, link prints IDs and exits"
|
|
40
40
|
}).option("skip-template", {
|
|
41
41
|
type: "boolean",
|
|
42
42
|
default: false,
|
|
@@ -86,7 +86,7 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
|
|
|
86
86
|
alias: "o",
|
|
87
87
|
hidden: true,
|
|
88
88
|
describe: "Not supported; the commands init runs print their own output"
|
|
89
|
-
}).example("$0 init", "Set up this directory (template picker if empty)").example("$0 init --skip-template", "Empty dir: agents, link, and neon.ts — no template files").example("$0 init -y", "Default template, or existing-app defaults").example("$0 init --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("Empty directory: pick a starter template, or skip scaffolding and only set up agents, a Neon project, and neon.ts. That skip is not on `neon bootstrap`.", "Interactive agent setup: plugin (recommended), skills and MCP separately, or skip agent setup. Never both plugin and skills+MCP.", "neon.ts is optional when you skip the template or set up an existing app. Saying no skips the services picker and does not write the file. Scaffolding a template keeps that template's neon.ts.", "-y installs the 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. Then link unless already linked.
|
|
89
|
+
}).example("$0 init", "Set up this directory (template picker if empty)").example("$0 init --skip-template", "Empty dir: agents, link, and neon.ts — no template files").example("$0 init -y", "Default template, or existing-app defaults").example("$0 init --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("Empty directory: pick a starter template, or skip scaffolding and only set up agents, a Neon project, and neon.ts. That skip is not on `neon bootstrap`.", "Interactive agent setup: plugin (recommended), skills and MCP separately, or skip agent setup. Never both plugin and skills+MCP.", "neon.ts is optional when you skip the template or set up an existing app. Saying no skips the services picker and does not write the file. Scaffolding a template keeps that template's neon.ts.", "-y installs the 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. Then link unless already linked. If several organizations or projects exist, link prints IDs and exits.", "--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with -y.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).check((argv) => {
|
|
90
90
|
if (argv.data !== void 0) throw new Error(removedProtocol());
|
|
91
91
|
if (argv.help !== true && (argv.output === "json" || argv.output === "yaml")) throw new Error(`\`${getCliName()} init\` does not support --output. The commands it runs print their own output.`);
|
|
92
92
|
return true;
|
package/dist/commands/link.js
CHANGED
|
@@ -2,10 +2,15 @@ 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 { isNeonApiError, messageFromBody } from "../api.js";
|
|
5
|
+
import { r as defaultDir } from "../_chunks/paths-DMq0Lt7a.js";
|
|
6
|
+
import "../config.js";
|
|
5
7
|
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile } from "../context.js";
|
|
6
8
|
import { getCliName } from "../utils/cli_name.js";
|
|
7
9
|
import { helpEpilogue } from "../utils/help_text.js";
|
|
10
|
+
import { writer } from "../writer.js";
|
|
8
11
|
import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
|
|
12
|
+
import { looksLikeBranchId } from "../utils/formats.js";
|
|
13
|
+
import { listAllProjectBranches } from "../utils/enrichers.js";
|
|
9
14
|
import { autoPullEnvAfterPin } from "./env.js";
|
|
10
15
|
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
11
16
|
import { REGIONS } from "./projects.js";
|
|
@@ -25,6 +30,7 @@ const canPromptInteractively = () => !isCi() && Boolean(process.stdin.isTTY) &&
|
|
|
25
30
|
const nonInteractiveLinkCommands = () => {
|
|
26
31
|
const cli = getCliName();
|
|
27
32
|
return [
|
|
33
|
+
`${cli} link -y`,
|
|
28
34
|
`${cli} orgs list --output json`,
|
|
29
35
|
`${cli} projects list --org-id <org-id> --output json`,
|
|
30
36
|
`${cli} link --project-id <project-id> [--branch <name> | -y]`,
|
|
@@ -63,7 +69,7 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
63
69
|
},
|
|
64
70
|
yes: {
|
|
65
71
|
alias: "y",
|
|
66
|
-
describe: "Skip the
|
|
72
|
+
describe: "Skip prompts. Select the only organization and project, or print IDs and the flag to pass. Pin the default branch when several exist. Does not create a project unless --project-name and --region-id are set.",
|
|
67
73
|
type: "boolean",
|
|
68
74
|
default: false
|
|
69
75
|
},
|
|
@@ -73,7 +79,7 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
73
79
|
default: false
|
|
74
80
|
},
|
|
75
81
|
checks: {
|
|
76
|
-
describe: "Verify the org/project/branch exist (and resolve the org from the project) before writing. On by default; use --no-checks to write the context offline with no API calls — it then requires --org-id
|
|
82
|
+
describe: "Verify the org/project/branch exist (and resolve the org from the project) before writing. On by default; use --no-checks to write the context offline with no API calls — it then requires --org-id, --project-id, and --branch, and skips env pull.",
|
|
77
83
|
type: "boolean",
|
|
78
84
|
default: true
|
|
79
85
|
},
|
|
@@ -88,11 +94,12 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
88
94
|
default: true
|
|
89
95
|
}
|
|
90
96
|
}).example([
|
|
91
|
-
["$0 link
|
|
97
|
+
["$0 link -y", "Select the only organization and project, or print the --org-id / --project-id to pass"],
|
|
98
|
+
["$0 link --project-id polished-snowflake-12345678", "Link an existing project (org is inferred). Pins the only branch; several prompt in a TTY, or -y pins the default"],
|
|
92
99
|
["$0 link --project-id polished-snowflake-12345678 -y", "Same, pinning the project's default branch when several exist"],
|
|
93
100
|
["$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2", "Create a new project and link it"],
|
|
94
101
|
["$0 link --branch-id br-…", "Pin a branch in the already-linked project"],
|
|
95
|
-
["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678", "Write the context offline (no API calls, no verification)"],
|
|
102
|
+
["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678 --branch main", "Write the context offline (no API calls, no verification)"],
|
|
96
103
|
["$0 link --clear", "Forget the current org/project/branch context"]
|
|
97
104
|
]).epilogue(helpEpilogue("Non-interactive (CI, scripts, agents):", ...nonInteractiveLinkCommands().map((command) => ` ${command}`), orgScopedKeyHint)).strict();
|
|
98
105
|
const runLink = async (props) => {
|
|
@@ -107,17 +114,26 @@ const runLink = async (props) => {
|
|
|
107
114
|
const inputs = parseInputs(props);
|
|
108
115
|
validateInputs(inputs);
|
|
109
116
|
const existing = readContextFile(props.contextFile);
|
|
117
|
+
if (props.yes && hasIncompleteCreationInputs(inputs)) throw incompleteCreationError(props, inputs);
|
|
110
118
|
if (canResolveNonInteractively(inputs, existing)) {
|
|
111
119
|
await runNonInteractive(props, inputs, existing);
|
|
112
120
|
return;
|
|
113
121
|
}
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
if (props.yes) {
|
|
123
|
+
const resolved = await resolveYesInputs(props, inputs);
|
|
124
|
+
await runNonInteractive(props, resolved, existing);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!canPromptInteractively()) {
|
|
128
|
+
if (missingProjectForOrg(inputs)) throw orgNeedsProjectError(props, inputs);
|
|
129
|
+
throw new LinkInputError([
|
|
130
|
+
"Missing inputs and no interactive terminal for prompts.",
|
|
131
|
+
"",
|
|
132
|
+
"Use:",
|
|
133
|
+
nonInteractiveLinkHelp(),
|
|
134
|
+
orgScopedKeyHint
|
|
135
|
+
].join("\n"));
|
|
136
|
+
}
|
|
121
137
|
await runInteractive(props, inputs);
|
|
122
138
|
};
|
|
123
139
|
const handler = runLink;
|
|
@@ -162,6 +178,39 @@ const validateInputs = (inputs) => {
|
|
|
162
178
|
if (inputs.projectId && (inputs.projectName || inputs.regionId)) throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
|
|
163
179
|
if (inputs.projectName && inputs.branch) throw new Error(`Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then \`${getCliName()} checkout <branch>\`.`);
|
|
164
180
|
};
|
|
181
|
+
const hasIncompleteCreationInputs = (inputs) => Boolean(inputs.projectName) !== Boolean(inputs.regionId);
|
|
182
|
+
const missingProjectForOrg = (inputs) => Boolean(inputs.orgId) && !inputs.projectId && !inputs.projectName;
|
|
183
|
+
const orgNeedsProjectError = (props, inputs) => {
|
|
184
|
+
const orgId = inputs.orgId ?? "<org-id>";
|
|
185
|
+
const branchFlag = inputs.branch ? ` --branch ${quoteFlagValue(inputs.branch)}` : "";
|
|
186
|
+
const session = sessionRetryFlags(props);
|
|
187
|
+
return new LinkInputError([
|
|
188
|
+
"No project selected. Pass --project-id, or use -y to select the only project:",
|
|
189
|
+
` ${getCliName()} link -y --org-id ${quoteFlagValue(orgId)}${branchFlag}${session}`,
|
|
190
|
+
` ${getCliName()} link --org-id ${quoteFlagValue(orgId)} --project-id <project-id>${inputs.branch ? ` --branch ${quoteFlagValue(inputs.branch)}` : " --branch <name-or-id>"}${session}`
|
|
191
|
+
].join("\n"));
|
|
192
|
+
};
|
|
193
|
+
const quoteFlagValue = (value) => {
|
|
194
|
+
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) return value;
|
|
195
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
196
|
+
};
|
|
197
|
+
const sessionRetryFlags = (props) => {
|
|
198
|
+
const flags = [];
|
|
199
|
+
if (props.contextFile) flags.push(`--context-file ${quoteFlagValue(props.contextFile)}`);
|
|
200
|
+
if (props.configDir && props.configDir !== defaultDir) flags.push(`--config-dir ${quoteFlagValue(props.configDir)}`);
|
|
201
|
+
if (props.output !== "table") flags.push(`--output ${props.output}`);
|
|
202
|
+
if (!props.envPull) flags.push("--no-env-pull");
|
|
203
|
+
if (props.profile) flags.push(`--profile ${quoteFlagValue(props.profile)}`);
|
|
204
|
+
return flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
205
|
+
};
|
|
206
|
+
const incompleteCreationError = (props, inputs) => {
|
|
207
|
+
const orgFlag = inputs.orgId ? `--org-id ${quoteFlagValue(inputs.orgId)}` : "--org-id <org-id>";
|
|
208
|
+
const nameFlag = inputs.projectName ? `--project-name ${quoteFlagValue(inputs.projectName)}` : "--project-name <name>";
|
|
209
|
+
const regionFlag = inputs.regionId ? `--region-id ${quoteFlagValue(inputs.regionId)}` : "--region-id aws-us-east-2";
|
|
210
|
+
const example = `${getCliName()} link -y ${orgFlag} ${nameFlag} ${regionFlag}${sessionRetryFlags(props)}`;
|
|
211
|
+
if (inputs.projectName) return new LinkInputError(`--project-name requires --region-id. Example:\n ${example}`);
|
|
212
|
+
return new LinkInputError(`--region-id requires --project-name. Example:\n ${example}`);
|
|
213
|
+
};
|
|
165
214
|
/**
|
|
166
215
|
* Branch selection may still prompt after org and project resolve without the
|
|
167
216
|
* org/project wizard.
|
|
@@ -170,7 +219,6 @@ const canResolveNonInteractively = (inputs, existing) => {
|
|
|
170
219
|
if (inputs.projectId) return true;
|
|
171
220
|
if (inputs.orgId && inputs.projectName && inputs.regionId) return true;
|
|
172
221
|
if (inputs.branch && existing.projectId) return true;
|
|
173
|
-
if (inputs.orgId && !inputs.projectName && !inputs.branch) return true;
|
|
174
222
|
return false;
|
|
175
223
|
};
|
|
176
224
|
const clearContext = (contextFile) => {
|
|
@@ -180,16 +228,15 @@ const clearContext = (contextFile) => {
|
|
|
180
228
|
/**
|
|
181
229
|
* `--no-checks`: write the context offline. Makes no API calls — so no org
|
|
182
230
|
* inference, no existence/access verification, and no env pull — which means
|
|
183
|
-
* the caller must supply
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* re-creating a `.neon` from values you already trust.
|
|
231
|
+
* the caller must supply `--org-id`, `--project-id`, and `--branch`. This is
|
|
232
|
+
* the CLI surface over {@link setContext}, useful for scripted/offline setups
|
|
233
|
+
* and for re-creating a `.neon` from values you already trust.
|
|
187
234
|
*/
|
|
188
235
|
const runWithoutChecks = (props) => {
|
|
189
236
|
const inputs = parseInputs(props);
|
|
190
237
|
validateInputs(inputs);
|
|
191
238
|
if (inputs.projectName) throw new Error("--no-checks can't create a project (that needs API access). Pass --org-id and --project-id for an existing project, or drop --no-checks.");
|
|
192
|
-
if (!inputs.orgId || !inputs.projectId) throw new Error("--no-checks
|
|
239
|
+
if (!inputs.orgId || !inputs.projectId || !inputs.branch) throw new Error("--no-checks requires --org-id, --project-id, and --branch because identifiers cannot be resolved offline.");
|
|
193
240
|
setContext(props.contextFile, {
|
|
194
241
|
orgId: inputs.orgId,
|
|
195
242
|
projectId: inputs.projectId,
|
|
@@ -242,107 +289,105 @@ const verifyOrgAccess = async (props, orgId) => {
|
|
|
242
289
|
throw err;
|
|
243
290
|
}
|
|
244
291
|
};
|
|
292
|
+
const failWithBranchCandidates = (props, projectId, branches, reason) => {
|
|
293
|
+
writer(props).end(branches.map((b) => ({
|
|
294
|
+
id: b.id,
|
|
295
|
+
name: b.name ?? b.id
|
|
296
|
+
})), {
|
|
297
|
+
fields: ["id", "name"],
|
|
298
|
+
title: "Branches"
|
|
299
|
+
});
|
|
300
|
+
const orgFlag = props.orgId ? ` --org-id ${quoteFlagValue(props.orgId)}` : "";
|
|
301
|
+
throw new LinkInputError(`${reason}\n ${getCliName()} link -y${orgFlag} --project-id ${quoteFlagValue(projectId)} --branch <name-or-id>${sessionRetryFlags(props)}`);
|
|
302
|
+
};
|
|
303
|
+
const listAllBranches = async (props, projectId) => listAllProjectBranches(props.apiClient, projectId);
|
|
245
304
|
/**
|
|
246
305
|
* Resolve a branch reference (name *or* id) to the matching branch, while
|
|
247
306
|
* confirming it actually exists in the project. Unlike the shared
|
|
248
307
|
* `branchIdResolve`, this also verifies references that already look like ids
|
|
249
|
-
* (so a typo'd `br-…` doesn't silently get written)
|
|
250
|
-
* branches when nothing matches so the user can correct it (or run `checkout`).
|
|
308
|
+
* (so a typo'd `br-…` doesn't silently get written).
|
|
251
309
|
*/
|
|
252
310
|
const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
253
|
-
const
|
|
254
|
-
const match =
|
|
311
|
+
const branches = await listAllBranches(props, projectId);
|
|
312
|
+
const match = branches.find((b) => b.id === branchRef) ?? branches.find((b) => b.name === branchRef);
|
|
255
313
|
if (match) return match;
|
|
256
|
-
|
|
314
|
+
return failWithBranchCandidates(props, projectId, branches, `Branch '${branchRef}' not found in project '${projectId}'. Pass --branch with a name or ID from the list:`);
|
|
257
315
|
};
|
|
258
316
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
* the id
|
|
317
|
+
* Persist the name when later commands will look it up as a name. A name that
|
|
318
|
+
* already looks like a branch id is trusted as an id without listing, so keep
|
|
319
|
+
* the real id in that case.
|
|
262
320
|
*/
|
|
263
|
-
const branchPersistValue = (branch) =>
|
|
321
|
+
const branchPersistValue = (branch) => {
|
|
322
|
+
if (branch.name && !looksLikeBranchId(branch.name)) return branch.name;
|
|
323
|
+
return branch.id;
|
|
324
|
+
};
|
|
264
325
|
/**
|
|
265
326
|
* Verify the project (and the org, when supplied) and resolve the org id to
|
|
266
327
|
* persist.
|
|
267
328
|
*
|
|
268
329
|
* The project is always fetched, which both validates it and yields its
|
|
269
330
|
* `org_id`. When `--org-id` is passed too: if the project reports an org it must
|
|
270
|
-
* match (else a clear mismatch error); if it reports none,
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* field is simply omitted.
|
|
331
|
+
* match (else a clear mismatch error); if it reports none, fail rather than
|
|
332
|
+
* attaching an unrelated org. Without `--org-id` the project's own org is used.
|
|
333
|
+
* Projects on a personal account have no org, so `undefined` is a valid result —
|
|
334
|
+
* the field is simply omitted.
|
|
275
335
|
*/
|
|
276
|
-
const resolveOrgForProject = async (props, inputs,
|
|
336
|
+
const resolveOrgForProject = async (props, inputs, projectId) => {
|
|
277
337
|
const projectOrg = (await fetchProjectOrThrow(props, projectId)).org_id ?? void 0;
|
|
278
338
|
if (inputs.orgId) {
|
|
279
339
|
if (projectOrg && projectOrg !== inputs.orgId) throw new LinkInputError(`Project '${projectId}' belongs to organization '${projectOrg}', not '${inputs.orgId}'. Omit --org-id to use the project's own org, or pass the matching ID.`);
|
|
280
|
-
if (!projectOrg)
|
|
340
|
+
if (!projectOrg) throw new LinkInputError(`Project '${projectId}' does not report an organization matching --org-id ${inputs.orgId}. Omit --org-id to link using the project's own context.`);
|
|
281
341
|
return inputs.orgId;
|
|
282
342
|
}
|
|
283
343
|
if (projectOrg) return projectOrg;
|
|
284
|
-
if (projectId === existing.projectId && existing.orgId) return existing.orgId;
|
|
285
344
|
};
|
|
286
345
|
/**
|
|
287
|
-
* Keep
|
|
288
|
-
* a checkout.
|
|
346
|
+
* Keep a same-project pin only after it still resolves on the project.
|
|
289
347
|
*/
|
|
290
348
|
const resolvePinnedBranch = async (props, inputs, existing, projectId) => {
|
|
291
349
|
if (inputs.branch) {
|
|
292
350
|
const branch = await resolveBranchRef(props, projectId, inputs.branch);
|
|
293
|
-
return {
|
|
294
|
-
branch: branchPersistValue(branch),
|
|
295
|
-
empty: false
|
|
296
|
-
};
|
|
351
|
+
return { branch: branchPersistValue(branch) };
|
|
297
352
|
}
|
|
298
353
|
if (projectId === existing.projectId) {
|
|
299
354
|
const pinned = contextBranch(existing);
|
|
300
|
-
if (pinned)
|
|
301
|
-
branch
|
|
302
|
-
|
|
303
|
-
}
|
|
355
|
+
if (pinned) {
|
|
356
|
+
const branch = await resolveBranchRef(props, projectId, pinned);
|
|
357
|
+
return { branch: branchPersistValue(branch) };
|
|
358
|
+
}
|
|
304
359
|
}
|
|
305
360
|
return resolveBranchFromList(props, projectId);
|
|
306
361
|
};
|
|
307
362
|
const resolveBranchFromList = async (props, projectId) => {
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
if (branches.length === 0) return { empty: true };
|
|
363
|
+
const branches = await listAllBranches(props, projectId);
|
|
364
|
+
if (branches.length === 0) throw new LinkInputError(`Project '${projectId}' has no branches to link. Create or restore a branch before retrying, or select another project with --project-id.`);
|
|
311
365
|
if (branches.length === 1) {
|
|
312
366
|
const [only] = branches;
|
|
313
|
-
if (!only)
|
|
314
|
-
return {
|
|
315
|
-
branch: branchPersistValue(only),
|
|
316
|
-
empty: false
|
|
317
|
-
};
|
|
367
|
+
if (!only) throw new LinkInputError(`Project '${projectId}' has no branches to link. Create or restore a branch before retrying, or select another project with --project-id.`);
|
|
368
|
+
return { branch: branchPersistValue(only) };
|
|
318
369
|
}
|
|
319
370
|
if (props.yes) {
|
|
320
371
|
const def = branches.find((b) => b.default);
|
|
321
|
-
if (!def)
|
|
322
|
-
return {
|
|
323
|
-
branch: branchPersistValue(def),
|
|
324
|
-
empty: false
|
|
325
|
-
};
|
|
372
|
+
if (!def) return failWithBranchCandidates(props, projectId, branches, `Project '${projectId}' has no default branch. Pass --branch with a name or ID from the list:`);
|
|
373
|
+
return { branch: branchPersistValue(def) };
|
|
326
374
|
}
|
|
327
375
|
if (canPromptInteractively()) {
|
|
328
376
|
const picked = await pickBranchInteractively(branches, {
|
|
329
377
|
message: "Which branch would you like to link?",
|
|
330
|
-
nonInteractiveMessage:
|
|
378
|
+
nonInteractiveMessage: "No branch could be selected without an interactive terminal. Pass --branch <name-or-id>, or -y to pin the default branch."
|
|
331
379
|
});
|
|
332
380
|
if (picked.kind === "existing") {
|
|
333
381
|
const existing = branches.find((b) => b.id === picked.branchId);
|
|
334
|
-
return {
|
|
335
|
-
branch: existing ? branchPersistValue(existing) : picked.branchId,
|
|
336
|
-
empty: false
|
|
337
|
-
};
|
|
382
|
+
return { branch: existing ? branchPersistValue(existing) : picked.branchId };
|
|
338
383
|
}
|
|
339
|
-
await createBranch(props.apiClient, projectId, picked.name, branches);
|
|
340
|
-
return {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
};
|
|
384
|
+
const created = await createBranch(props.apiClient, projectId, picked.name, branches);
|
|
385
|
+
return { branch: branchPersistValue({
|
|
386
|
+
id: created,
|
|
387
|
+
name: picked.name
|
|
388
|
+
}) };
|
|
344
389
|
}
|
|
345
|
-
return {
|
|
390
|
+
return failWithBranchCandidates(props, projectId, branches, `Project '${projectId}' has multiple branches. Pass --branch <name-or-id>, or -y to pin its default branch.`);
|
|
346
391
|
};
|
|
347
392
|
const runNonInteractive = async (props, inputs, existing) => {
|
|
348
393
|
if (inputs.projectName) {
|
|
@@ -370,7 +415,7 @@ const runNonInteractive = async (props, inputs, existing) => {
|
|
|
370
415
|
return;
|
|
371
416
|
}
|
|
372
417
|
if (inputs.projectId) {
|
|
373
|
-
const orgId = await resolveOrgForProject(props, inputs,
|
|
418
|
+
const orgId = await resolveOrgForProject(props, inputs, inputs.projectId);
|
|
374
419
|
const resolved = await resolvePinnedBranch(props, inputs, existing, inputs.projectId);
|
|
375
420
|
applyContext(props.contextFile, {
|
|
376
421
|
orgId,
|
|
@@ -382,14 +427,13 @@ const runNonInteractive = async (props, inputs, existing) => {
|
|
|
382
427
|
orgId,
|
|
383
428
|
projectId: inputs.projectId,
|
|
384
429
|
branch: resolved.branch,
|
|
385
|
-
created: false
|
|
386
|
-
empty: resolved.empty
|
|
430
|
+
created: false
|
|
387
431
|
});
|
|
388
432
|
return;
|
|
389
433
|
}
|
|
390
434
|
if (inputs.branch && existing.projectId) {
|
|
391
435
|
const projectId = existing.projectId;
|
|
392
|
-
const orgId = await resolveOrgForProject(props, inputs,
|
|
436
|
+
const orgId = await resolveOrgForProject(props, inputs, projectId);
|
|
393
437
|
const resolved = await resolvePinnedBranch(props, inputs, existing, projectId);
|
|
394
438
|
applyContext(props.contextFile, {
|
|
395
439
|
orgId,
|
|
@@ -401,31 +445,11 @@ const runNonInteractive = async (props, inputs, existing) => {
|
|
|
401
445
|
orgId,
|
|
402
446
|
projectId,
|
|
403
447
|
branch: resolved.branch,
|
|
404
|
-
created: false
|
|
405
|
-
empty: resolved.empty
|
|
406
|
-
});
|
|
407
|
-
return;
|
|
408
|
-
}
|
|
409
|
-
if (inputs.orgId) {
|
|
410
|
-
const orgId = inputs.orgId;
|
|
411
|
-
await verifyOrgAccess(props, orgId);
|
|
412
|
-
const projectId = existing.projectId;
|
|
413
|
-
const branch = projectId ? contextBranch(existing) : void 0;
|
|
414
|
-
applyContext(props.contextFile, {
|
|
415
|
-
orgId,
|
|
416
|
-
projectId,
|
|
417
|
-
branch
|
|
418
|
-
});
|
|
419
|
-
printSummary(props, {
|
|
420
|
-
contextFile: props.contextFile,
|
|
421
|
-
orgId,
|
|
422
|
-
projectId,
|
|
423
|
-
branch,
|
|
424
|
-
created: false,
|
|
425
|
-
orgOnly: true
|
|
448
|
+
created: false
|
|
426
449
|
});
|
|
427
450
|
return;
|
|
428
451
|
}
|
|
452
|
+
throw orgNeedsProjectError(props, inputs);
|
|
429
453
|
};
|
|
430
454
|
const runInteractive = async (props, inputs) => {
|
|
431
455
|
if (!props.yes) {
|
|
@@ -439,6 +463,7 @@ const runInteractive = async (props, inputs) => {
|
|
|
439
463
|
} else if (orgResolution.orgKeyLimited) throw new Error("This API key is organization-scoped, so the CLI cannot list your organizations, and no existing project was found in this org to auto-detect the ID. Re-run with `--org-id <your_org_id>` (find it in the Neon Console under Settings).");
|
|
440
464
|
else orgId = await promptOrgFromList(orgResolution.orgs);
|
|
441
465
|
if (inputs.projectName && inputs.regionId) {
|
|
466
|
+
if (inputs.branch) throw new Error(`Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then \`${getCliName()} checkout <branch>\`.`);
|
|
442
467
|
const created = await createProject(props, {
|
|
443
468
|
orgId,
|
|
444
469
|
name: inputs.projectName,
|
|
@@ -461,9 +486,10 @@ const runInteractive = async (props, inputs) => {
|
|
|
461
486
|
return;
|
|
462
487
|
}
|
|
463
488
|
const projects = await listAllProjects(props, orgId);
|
|
464
|
-
const action = await promptProjectChoice(projects, inputs.projectName);
|
|
489
|
+
const action = await promptProjectChoice(projects, inputs.projectName, { allowCreate: !inputs.branch });
|
|
465
490
|
if (action.type === "existing") {
|
|
466
|
-
const
|
|
491
|
+
const existing = readContextFile(props.contextFile);
|
|
492
|
+
const resolved = await resolvePinnedBranch(props, inputs, existing, action.projectId);
|
|
467
493
|
applyContext(props.contextFile, {
|
|
468
494
|
orgId,
|
|
469
495
|
projectId: action.projectId,
|
|
@@ -476,11 +502,11 @@ const runInteractive = async (props, inputs) => {
|
|
|
476
502
|
branch: resolved.branch,
|
|
477
503
|
created: false,
|
|
478
504
|
projectName: action.name,
|
|
479
|
-
regionId: action.regionId
|
|
480
|
-
empty: resolved.empty
|
|
505
|
+
regionId: action.regionId
|
|
481
506
|
});
|
|
482
507
|
return;
|
|
483
508
|
}
|
|
509
|
+
if (inputs.branch) throw new Error(`Conflicting inputs: --branch pins a branch of an existing project, but creating a project starts from its default branch. Link an existing project, or omit --branch and create first.`);
|
|
484
510
|
const projectName = inputs.projectName ?? await promptProjectName(action.suggestedName);
|
|
485
511
|
const regionId = inputs.regionId ?? await promptRegion(props);
|
|
486
512
|
const created = await createProject(props, {
|
|
@@ -505,12 +531,12 @@ const runInteractive = async (props, inputs) => {
|
|
|
505
531
|
};
|
|
506
532
|
const confirmRelinkIfNeeded = async (props) => {
|
|
507
533
|
const existing = readContextFile(props.contextFile);
|
|
508
|
-
if (!existing.
|
|
534
|
+
if (!existing.projectId) return true;
|
|
509
535
|
const { proceed } = await prompts({
|
|
510
536
|
onState: onPromptState,
|
|
511
537
|
type: "confirm",
|
|
512
538
|
name: "proceed",
|
|
513
|
-
message: `${props.contextFile} is already linked to project ${existing.projectId} (org ${existing.orgId}). Re-link?`,
|
|
539
|
+
message: existing.orgId ? `${props.contextFile} is already linked to project ${existing.projectId} (org ${existing.orgId}). Re-link?` : `${props.contextFile} is already linked to project ${existing.projectId}. Re-link?`,
|
|
514
540
|
initial: true
|
|
515
541
|
});
|
|
516
542
|
if (!proceed) {
|
|
@@ -539,21 +565,23 @@ const promptOrgFromList = async (orgs) => {
|
|
|
539
565
|
});
|
|
540
566
|
return orgId;
|
|
541
567
|
};
|
|
542
|
-
const promptProjectChoice = async (projects, suggestedName) => {
|
|
543
|
-
const choices = [{
|
|
568
|
+
const promptProjectChoice = async (projects, suggestedName, opts) => {
|
|
569
|
+
const choices = [...opts.allowCreate ? [{
|
|
544
570
|
title: "+ Create new project…",
|
|
545
571
|
value: CREATE_NEW_SENTINEL
|
|
546
|
-
}, ...projects.map((project) => ({
|
|
572
|
+
}] : [], ...projects.map((project) => ({
|
|
547
573
|
title: `${project.name} (${project.id})`,
|
|
548
574
|
value: project.id
|
|
549
575
|
}))];
|
|
576
|
+
if (choices.length === 0) throw new LinkInputError("No projects are available to link. Omit --branch to create a project, or pass --project-id for an existing one.");
|
|
577
|
+
const createOffset = opts.allowCreate ? 1 : 0;
|
|
550
578
|
const { selection } = await prompts({
|
|
551
579
|
onState: onPromptState,
|
|
552
580
|
type: "select",
|
|
553
581
|
name: "selection",
|
|
554
582
|
message: "Which project would you like to link?",
|
|
555
583
|
choices,
|
|
556
|
-
initial: projects.length > 0 ?
|
|
584
|
+
initial: projects.length > 0 ? createOffset : 0
|
|
557
585
|
});
|
|
558
586
|
if (selection === CREATE_NEW_SENTINEL) return {
|
|
559
587
|
type: "create",
|
|
@@ -666,6 +694,71 @@ const listAllProjects = async (props, orgId) => {
|
|
|
666
694
|
}
|
|
667
695
|
return result;
|
|
668
696
|
};
|
|
697
|
+
const CANDIDATE_FIELDS = ["id", "name"];
|
|
698
|
+
const extraYesFlags = (props, inputs) => {
|
|
699
|
+
const flags = [];
|
|
700
|
+
if (inputs.projectName) flags.push(`--project-name ${quoteFlagValue(inputs.projectName)}`);
|
|
701
|
+
if (inputs.regionId) flags.push(`--region-id ${quoteFlagValue(inputs.regionId)}`);
|
|
702
|
+
if (inputs.branch) flags.push(`--branch ${quoteFlagValue(inputs.branch)}`);
|
|
703
|
+
return `${flags.length > 0 ? ` ${flags.join(" ")}` : ""}${sessionRetryFlags(props)}`;
|
|
704
|
+
};
|
|
705
|
+
const yesProjectIdCommand = (props, inputs, orgId = inputs.orgId) => {
|
|
706
|
+
const orgFlag = orgId ? ` --org-id ${quoteFlagValue(orgId)}` : "";
|
|
707
|
+
const branchFlag = inputs.branch ? ` --branch ${quoteFlagValue(inputs.branch)}` : "";
|
|
708
|
+
return `${getCliName()} link -y${orgFlag} --project-id <project-id>${branchFlag}${sessionRetryFlags(props)}`;
|
|
709
|
+
};
|
|
710
|
+
const printNamedCandidates = (props, title, items) => {
|
|
711
|
+
writer(props).end(items, {
|
|
712
|
+
fields: CANDIDATE_FIELDS,
|
|
713
|
+
title
|
|
714
|
+
});
|
|
715
|
+
};
|
|
716
|
+
const orgScopedKeyNeedsOrgId = () => new LinkInputError("This API key is organization-scoped, so the CLI cannot list your organizations, and no existing project was found in this org to auto-detect the ID. Re-run with `--org-id <your_org_id>` (find it in the Neon Console under Settings).");
|
|
717
|
+
const resolveYesOrgId = async (props, inputs) => {
|
|
718
|
+
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
719
|
+
if (orgResolution.kind === "resolved") {
|
|
720
|
+
if (orgResolution.autoDetected) log.info(`Detected organization ${orgResolution.orgId} from your existing projects (organization-scoped API key).`);
|
|
721
|
+
return orgResolution.orgId;
|
|
722
|
+
}
|
|
723
|
+
if (orgResolution.orgKeyLimited) throw orgScopedKeyNeedsOrgId();
|
|
724
|
+
const orgs = orgResolution.orgs;
|
|
725
|
+
if (orgs.length === 0) throw new LinkInputError(["No organizations were returned for this account. Pass --project-id for a project you can access:", ` ${yesProjectIdCommand(props, inputs)}`].join("\n"));
|
|
726
|
+
if (orgs.length === 1) {
|
|
727
|
+
const [only] = orgs;
|
|
728
|
+
return only.id;
|
|
729
|
+
}
|
|
730
|
+
printNamedCandidates(props, "Organizations", orgs.map((org) => ({
|
|
731
|
+
id: org.id,
|
|
732
|
+
name: org.name
|
|
733
|
+
})));
|
|
734
|
+
throw new LinkInputError(["Multiple organizations are available. Pass --org-id with an ID from the list:", ` ${getCliName()} link -y --org-id <org-id>${extraYesFlags(props, inputs)}`].join("\n"));
|
|
735
|
+
};
|
|
736
|
+
const resolveYesInputs = async (props, inputs) => {
|
|
737
|
+
const orgId = await resolveYesOrgId(props, inputs);
|
|
738
|
+
if (inputs.projectName && inputs.regionId) return {
|
|
739
|
+
...inputs,
|
|
740
|
+
orgId
|
|
741
|
+
};
|
|
742
|
+
const projects = await listAllProjects(props, orgId);
|
|
743
|
+
if (projects.length === 0) throw new LinkInputError([
|
|
744
|
+
`No projects are available in organization '${orgId}'.`,
|
|
745
|
+
"To create and link a project, pass --project-name and --region-id:",
|
|
746
|
+
` ${getCliName()} link -y --org-id ${quoteFlagValue(orgId)} --project-name <name> --region-id aws-us-east-2${sessionRetryFlags(props)}`
|
|
747
|
+
].join("\n"));
|
|
748
|
+
if (projects.length === 1) {
|
|
749
|
+
const [only] = projects;
|
|
750
|
+
return {
|
|
751
|
+
...inputs,
|
|
752
|
+
orgId,
|
|
753
|
+
projectId: only.id
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
printNamedCandidates(props, "Projects", projects.map((project) => ({
|
|
757
|
+
id: project.id,
|
|
758
|
+
name: project.name
|
|
759
|
+
})));
|
|
760
|
+
throw new LinkInputError([`Multiple projects are available in organization '${orgId}'. Pass --project-id with an ID from the list:`, ` ${yesProjectIdCommand(props, inputs, orgId)}`].join("\n"));
|
|
761
|
+
};
|
|
669
762
|
const fetchRegions = async (props) => {
|
|
670
763
|
try {
|
|
671
764
|
const { data } = await props.apiClient.getActiveRegions();
|
|
@@ -708,30 +801,26 @@ const createProject = async (props, args) => {
|
|
|
708
801
|
const printSummary = (_props, summary) => {
|
|
709
802
|
const lines = [];
|
|
710
803
|
if (summary.created) lines.push(`Created project ${summary.projectId}${summary.projectName ? ` ("${summary.projectName}")` : ""}${summary.regionId ? ` in ${summary.regionId}` : ""}.`);
|
|
711
|
-
lines.push(
|
|
804
|
+
lines.push(`Linked ${summary.contextFile}:`);
|
|
712
805
|
if (summary.orgId) lines.push(` orgId: ${summary.orgId}`);
|
|
713
|
-
|
|
714
|
-
|
|
806
|
+
lines.push(` projectId: ${summary.projectId}`);
|
|
807
|
+
lines.push(` branch: ${summary.branch}`);
|
|
715
808
|
if (summary.noChecks) {
|
|
716
809
|
lines.push("");
|
|
717
810
|
lines.push("Written offline (--no-checks): nothing was verified.");
|
|
718
|
-
} else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
719
|
-
lines.push("");
|
|
720
|
-
if (summary.empty) lines.push(`This project has no branches, so none was pinned. Create a branch, then run \`${getCliName()} checkout <branch>\` to pin it and pull its env vars.`);
|
|
721
|
-
else lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
722
811
|
}
|
|
723
812
|
lines.push("");
|
|
724
813
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
725
814
|
};
|
|
726
815
|
/**
|
|
727
|
-
* Print the link summary, then run the bundled `env pull` so a
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
816
|
+
* Print the link summary, then run the bundled `env pull` so a completed `link` ends with
|
|
817
|
+
* the pinned branch's connection string on disk. `--no-checks` skips the pull because
|
|
818
|
+
* nothing was verified. `--no-env-pull` opts out (env pull's own status / skip hint is
|
|
819
|
+
* logged to stderr).
|
|
731
820
|
*/
|
|
732
821
|
const finalizeLink = async (props, summary) => {
|
|
733
822
|
printSummary(props, summary);
|
|
734
|
-
if (!summary.branch || !summary.projectId) return;
|
|
823
|
+
if (!summary.branch || !summary.projectId || summary.noChecks) return;
|
|
735
824
|
const { config: _offerConfig, ...rest } = props;
|
|
736
825
|
await autoPullEnvAfterPin({
|
|
737
826
|
...rest,
|
package/dist/commands/logs.js
CHANGED
|
@@ -16,7 +16,7 @@ var logs_exports = /* @__PURE__ */ __exportAll({
|
|
|
16
16
|
handler: () => handler
|
|
17
17
|
});
|
|
18
18
|
const LOGS_EPILOG = `
|
|
19
|
-
Logs
|
|
19
|
+
Logs are currently available only for projects in aws-us-east-2, aws-us-east-1, aws-eu-central-1, and aws-ap-southeast-1.
|
|
20
20
|
|
|
21
21
|
For more information, visit https://neon.com/docs/reference/neon-cli`;
|
|
22
22
|
const LOG_SOURCES = [
|
|
@@ -2,7 +2,7 @@ import { log } from "../log.js";
|
|
|
2
2
|
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
3
3
|
import { sendError } from "../analytics.js";
|
|
4
4
|
import { writer } from "../writer.js";
|
|
5
|
-
import { branchIdFromProps } from "../utils/enrichers.js";
|
|
5
|
+
import { branchIdFromProps, listAllProjectBranches } from "../utils/enrichers.js";
|
|
6
6
|
import { parsePointInTime } from "../utils/point_in_time.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { createPatch } from "diff";
|
|
@@ -92,13 +92,11 @@ const parseSchemaDiffParams = async (props) => {
|
|
|
92
92
|
props.compareSource = props.baseBranch;
|
|
93
93
|
props.baseBranch = props.branch;
|
|
94
94
|
} else if (props.branch) {
|
|
95
|
-
|
|
96
|
-
if (data.branches.find((b) => b.id === props.branch || b.name === props.branch)?.parent_id == void 0) throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
|
|
95
|
+
if ((await listAllProjectBranches(props.apiClient, props.projectId)).find((b) => b.id === props.branch || b.name === props.branch)?.parent_id == void 0) throw new Error(`No branch specified. Your context branch (${props.branch}) has no parent, so no comparison is possible.`);
|
|
97
96
|
log.info(`No branches specified. Comparing your context branch '${props.branch}' with its parent`);
|
|
98
97
|
props.compareSource = "^parent";
|
|
99
98
|
} else {
|
|
100
|
-
|
|
101
|
-
if (data.branches.find((b) => b.default)?.parent_id == void 0) throw new Error("No branch specified. Include a base branch or add a set-context branch to continue. Your default branch has no parent, so no comparison is possible.");
|
|
99
|
+
if ((await listAllProjectBranches(props.apiClient, props.projectId)).find((b) => b.default)?.parent_id == void 0) throw new Error("No branch specified. Include a base branch or add a set-context branch to continue. Your default branch has no parent, so no comparison is possible.");
|
|
102
100
|
log.info(`No branches specified. Comparing default branch with its parent`);
|
|
103
101
|
props.compareSource = "^parent";
|
|
104
102
|
}
|
|
@@ -26,7 +26,7 @@ const builder = (argv) => argv.usage("$0 set-context [options]").options({
|
|
|
26
26
|
}
|
|
27
27
|
});
|
|
28
28
|
const handler = (props) => {
|
|
29
|
-
log.warning(`\`${getCliName()} set-context\` is deprecated and will be removed in a future release. Use \`${getCliName()} link\` instead — it verifies inputs and
|
|
29
|
+
log.warning(`\`${getCliName()} set-context\` is deprecated and will be removed in a future release. Use \`${getCliName()} link\` instead — it verifies inputs and writes a complete context (or \`${getCliName()} link --no-checks --org-id … --project-id … --branch …\` for an offline write).`);
|
|
30
30
|
const context = {
|
|
31
31
|
projectId: props.projectId,
|
|
32
32
|
orgId: props.orgId,
|
package/dist/plugins/run.js
CHANGED
package/dist/skills/catalog.js
CHANGED
package/dist/utils/enrichers.js
CHANGED
|
@@ -2,12 +2,32 @@ import { isNeonApiError, messageFromBody } from "../api.js";
|
|
|
2
2
|
import { isConfigInit, isCurrentBranchProbe } from "../context.js";
|
|
3
3
|
import { looksLikeBranchId } from "./formats.js";
|
|
4
4
|
//#region src/utils/enrichers.ts
|
|
5
|
+
const BRANCHES_LIST_LIMIT = 100;
|
|
6
|
+
/**
|
|
7
|
+
* Branch list pages cap at 100. A name stored in `.neon` can live on a later
|
|
8
|
+
* page, so resolution has to walk `pagination.next` before treating it as missing.
|
|
9
|
+
*/
|
|
10
|
+
const listAllProjectBranches = async (apiClient, projectId) => {
|
|
11
|
+
const result = [];
|
|
12
|
+
let cursor;
|
|
13
|
+
while (true) {
|
|
14
|
+
const { data } = await apiClient.listProjectBranches({
|
|
15
|
+
projectId,
|
|
16
|
+
limit: BRANCHES_LIST_LIMIT,
|
|
17
|
+
cursor
|
|
18
|
+
});
|
|
19
|
+
result.push(...data.branches);
|
|
20
|
+
cursor = data.pagination?.next;
|
|
21
|
+
if (!cursor || data.branches.length === 0) break;
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
5
25
|
const branchIdResolve = async ({ branch, apiClient, projectId }) => {
|
|
6
26
|
branch = branch.toString();
|
|
7
27
|
if (looksLikeBranchId(branch)) return branch;
|
|
8
|
-
const
|
|
9
|
-
const branchData =
|
|
10
|
-
if (!branchData) throw new Error(`Branch ${branch} not found.\nAvailable branches: ${
|
|
28
|
+
const branches = await listAllProjectBranches(apiClient, projectId);
|
|
29
|
+
const branchData = branches.find((b) => b.name === branch);
|
|
30
|
+
if (!branchData) throw new Error(`Branch ${branch} not found.\nAvailable branches: ${branches.map((b) => b.name).join(", ")}`);
|
|
11
31
|
return branchData.id;
|
|
12
32
|
};
|
|
13
33
|
const getBranchIdFromProps = async (props) => {
|
|
@@ -17,8 +37,7 @@ const getBranchIdFromProps = async (props) => {
|
|
|
17
37
|
apiClient: props.apiClient,
|
|
18
38
|
projectId: props.projectId
|
|
19
39
|
});
|
|
20
|
-
const
|
|
21
|
-
const defaultBranch = data.branches.find((b) => b.default);
|
|
40
|
+
const defaultBranch = (await listAllProjectBranches(props.apiClient, props.projectId)).find((b) => b.default);
|
|
22
41
|
if (defaultBranch) return defaultBranch.id;
|
|
23
42
|
throw new Error("No default branch found");
|
|
24
43
|
};
|
|
@@ -28,8 +47,7 @@ const branchIdFromProps = async (props) => {
|
|
|
28
47
|
};
|
|
29
48
|
const resolveBranchRef = async (props) => {
|
|
30
49
|
const branch = typeof props.branch === "string" ? props.branch : props.id;
|
|
31
|
-
const
|
|
32
|
-
const branches = data.branches;
|
|
50
|
+
const branches = await listAllProjectBranches(props.apiClient, props.projectId);
|
|
33
51
|
const listingFields = (listed) => ({
|
|
34
52
|
isDefault: listed.default === true,
|
|
35
53
|
isProtected: listed.protected === true,
|
|
@@ -111,4 +129,4 @@ const fillSingleOrg = async (props) => {
|
|
|
111
129
|
};
|
|
112
130
|
};
|
|
113
131
|
//#endregion
|
|
114
|
-
export { branchIdFromProps, branchIdResolve, fillSingleOrg, fillSingleProject, resolveBranchRef, resolveSingleDatabase };
|
|
132
|
+
export { branchIdFromProps, branchIdResolve, fillSingleOrg, fillSingleProject, listAllProjectBranches, resolveBranchRef, resolveSingleDatabase };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
"yaml": "^2.9.0",
|
|
64
64
|
"yargs": "17.7.2",
|
|
65
65
|
"yoctocolors": "^2.1.2",
|
|
66
|
-
"@neon/config": "1.7.
|
|
67
|
-
"@neon/
|
|
68
|
-
"@neon/
|
|
66
|
+
"@neon/config": "1.7.2",
|
|
67
|
+
"@neon/sdk": "6.0.0",
|
|
68
|
+
"@neon/config-runtime": "1.6.2"
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
71
|
"@napi-rs/keyring": "1.3.0",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"typescript": "^5.9.0",
|
|
99
99
|
"vitest": "^3.0.9",
|
|
100
100
|
"@neon-internals/cli-core": "0.0.0",
|
|
101
|
-
"@neon-internals/env-core": "0.0.
|
|
101
|
+
"@neon-internals/env-core": "0.0.21",
|
|
102
102
|
"@neon/e2e-harness": "0.0.0",
|
|
103
103
|
"@neon/functions": "0.11.0"
|
|
104
104
|
},
|