neon 4.5.2 → 4.7.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 +18 -55
- package/dist/commands/bootstrap.js +25 -121
- package/dist/commands/env.js +3 -20
- package/dist/commands/link.js +33 -230
- package/dist/commands/mcp.js +1 -1
- package/dist/commands/plugins.js +1 -1
- package/dist/commands/skills.js +1 -1
- package/dist/init/bootstrap.js +1 -6
- package/dist/utils/cli_name.js +1 -12
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -256,14 +256,14 @@ The Neon CLI supports autocompletion, which you can configure in a few easy step
|
|
|
256
256
|
`link` resolves what it can and **verifies every identifier you pass** before writing, so a `.neon` is never left half-written or pointing at something that doesn't exist:
|
|
257
257
|
|
|
258
258
|
- **org** is inferred from the project (so `--project-id` alone is enough); it's omitted only when the project has no organization (personal account).
|
|
259
|
-
- **project** is taken from `--project-id` (or chosen interactively
|
|
259
|
+
- **project** is taken from `--project-id` (or chosen interactively).
|
|
260
260
|
- **branch** is left to an explicit [`neon checkout <branch>`](#checkout) — `link` never silently pins a project's default branch (that would make later commands quietly target, say, production). It only records a branch when you pass `--branch`, when one is already pinned for the same project (preserved), when you pick one in the interactive picker, or for a freshly **created** project (whose single branch is unambiguous).
|
|
261
261
|
|
|
262
262
|
When a branch ends up pinned, `link` also runs [`env pull`](#env-pull) so the branch's Neon env vars (`DATABASE_URL`, …) land in a local `.env`. With no branch pinned there is nothing to pull, so `link` instead nudges you to run `neon checkout`. Pass `--no-env-pull` to skip the pull (for example when injecting env at runtime with `neon-env run` or `neon dev`).
|
|
263
263
|
|
|
264
264
|
> **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.
|
|
265
265
|
|
|
266
|
-
There are
|
|
266
|
+
There are two modes:
|
|
267
267
|
|
|
268
268
|
**Interactive (default)** — guided prompts for humans:
|
|
269
269
|
|
|
@@ -322,64 +322,21 @@ neon link --no-checks --org-id org-abc123 --project-id polished-snowflake-123456
|
|
|
322
322
|
|
|
323
323
|
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: …`.
|
|
324
324
|
|
|
325
|
-
**
|
|
325
|
+
**Agents and scripts (no TTY):** List, then link. `neon link --help` prints the same recipe.
|
|
326
326
|
|
|
327
327
|
```bash
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"options": [
|
|
333
|
-
{ "id": "org-abc123", "name": "Personal Org" },
|
|
334
|
-
{ "id": "org-team", "name": "Team Org" }
|
|
335
|
-
],
|
|
336
|
-
"next_command_template": "neon link --agent --org-id <org_id>"
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
$ neon link --agent --org-id org-abc123
|
|
340
|
-
{
|
|
341
|
-
"status": "needs_project",
|
|
342
|
-
"instruction": "Ask the user whether to link to one of these 1 existing projects (use next_command_template with --project-id) or create a new project (use create_option.next_command_template).",
|
|
343
|
-
"options": [
|
|
344
|
-
{ "id": "polished-snowflake-12345678", "name": "my-app" }
|
|
345
|
-
],
|
|
346
|
-
"create_option": {
|
|
347
|
-
"instruction": "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
|
|
348
|
-
"next_command_template": "neon link --agent --org-id org-abc123 --project-name <name> --region-id <region_id>"
|
|
349
|
-
},
|
|
350
|
-
"next_command_template": "neon link --agent --org-id org-abc123 --project-id <project_id>"
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
$ neon link --agent --org-id org-abc123 --project-id polished-snowflake-12345678
|
|
354
|
-
{
|
|
355
|
-
"status": "linked",
|
|
356
|
-
"context_file": "/path/to/cwd/.neon",
|
|
357
|
-
"context": {
|
|
358
|
-
"orgId": "org-abc123",
|
|
359
|
-
"projectId": "polished-snowflake-12345678"
|
|
360
|
-
},
|
|
361
|
-
"project": { "id": "polished-snowflake-12345678" },
|
|
362
|
-
"message": "Linked /path/to/cwd/.neon to project polished-snowflake-12345678 (org org-abc123). No branch pinned — run `neon checkout <branch>` (omit the branch to list options) to pin one and pull its env vars."
|
|
363
|
-
}
|
|
328
|
+
neon orgs list --output json
|
|
329
|
+
neon projects list --org-id <org-id> --output json
|
|
330
|
+
neon link --project-id <project-id>
|
|
331
|
+
neon link --org-id <org-id> --project-name <name> --region-id aws-us-east-2
|
|
364
332
|
```
|
|
365
333
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
**Organization-scoped API keys** (those created at the organization level rather than the user level) cannot list user organizations or call the regions endpoint. `link` handles this transparently:
|
|
369
|
-
|
|
370
|
-
- If the API key is org-scoped and at least one project already exists in the org, the CLI auto-detects the `org_id` from the first project. In interactive mode it prints an informational message; in `--agent` mode it skips straight to `needs_project`.
|
|
371
|
-
- If the API key is org-scoped and no projects exist yet, `--agent` returns a `needs_org` response with `options: []` and an instruction telling the user to find their org ID in the Neon Console. Interactive mode prints an error pointing to `--org-id`.
|
|
372
|
-
- When the regions endpoint is not allowed, `link` falls back to a built-in static region list.
|
|
334
|
+
Organization-scoped API keys cannot list user organizations (`orgs list`) or call the regions endpoint:
|
|
373
335
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"status": "error",
|
|
379
|
-
"code": "CLIENT_ERROR",
|
|
380
|
-
"message": "user has no access to projects"
|
|
381
|
-
}
|
|
382
|
-
```
|
|
336
|
+
- Pass `--org-id` (Neon Console → Settings) or `--project-id` (org is inferred from the project).
|
|
337
|
+
- If the key is org-scoped and at least one project already exists, interactive `link` auto-detects the org from the first project and prints an informational message.
|
|
338
|
+
- If no projects exist yet, interactive `link` errors pointing at `--org-id`.
|
|
339
|
+
- 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`.
|
|
383
340
|
|
|
384
341
|
**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 both `--org-id` and `--project-id` (`--branch` optional, stored verbatim). Handy for scripted/CI setups or re-creating a `.neon` from values you already trust:
|
|
385
342
|
|
|
@@ -710,6 +667,12 @@ $ neon bootstrap my-app
|
|
|
710
667
|
|
|
711
668
|
# Scaffold a specific template into the current directory (no prompts)
|
|
712
669
|
$ neon bootstrap . --template hono
|
|
670
|
+
|
|
671
|
+
# List templates
|
|
672
|
+
$ neon bootstrap --list-templates
|
|
673
|
+
|
|
674
|
+
# Machine-readable catalog
|
|
675
|
+
$ neon bootstrap --list-templates --output json
|
|
713
676
|
```
|
|
714
677
|
|
|
715
678
|
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.
|
|
@@ -3,8 +3,9 @@ import { isCi } from "../env.js";
|
|
|
3
3
|
import { log } from "../log.js";
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
5
|
import { t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { writer } from "../writer.js";
|
|
7
|
+
import { FALLBACK_TEMPLATES, ensureTargetUsable, fetchTemplates, findTemplate, scaffoldTemplate, templateIds } from "../init/bootstrap.js";
|
|
8
|
+
import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
|
|
8
9
|
import { existsSync } from "node:fs";
|
|
9
10
|
import { join, relative, resolve } from "node:path";
|
|
10
11
|
import chalk from "chalk";
|
|
@@ -16,6 +17,7 @@ var bootstrap_exports = /* @__PURE__ */ __exportAll({
|
|
|
16
17
|
describe: () => describe,
|
|
17
18
|
handler: () => handler
|
|
18
19
|
});
|
|
20
|
+
const removedAgent = () => `\`${getCliName()} bootstrap --agent\` was removed. List templates with \`${getCliName()} bootstrap --list-templates --output json\`. Scaffold with \`${getCliName()} bootstrap <directory> --template <id>\` or \`${getCliName()} bootstrap <directory> --default\`.`;
|
|
19
21
|
const command = "bootstrap [directory]";
|
|
20
22
|
const describe = "Scaffold a new project from a Neon starter template";
|
|
21
23
|
const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").positional("directory", {
|
|
@@ -28,7 +30,7 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
|
|
|
28
30
|
},
|
|
29
31
|
"list-templates": {
|
|
30
32
|
alias: ["list", "ls"],
|
|
31
|
-
describe: "List available templates and exit.",
|
|
33
|
+
describe: "List available templates and exit. --output json and --output yaml print a machine-readable catalog.",
|
|
32
34
|
type: "boolean",
|
|
33
35
|
default: false
|
|
34
36
|
},
|
|
@@ -38,9 +40,8 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
|
|
|
38
40
|
default: false
|
|
39
41
|
},
|
|
40
42
|
agent: {
|
|
41
|
-
|
|
42
|
-
type: "boolean"
|
|
43
|
-
default: false
|
|
43
|
+
hidden: true,
|
|
44
|
+
type: "boolean"
|
|
44
45
|
},
|
|
45
46
|
default: {
|
|
46
47
|
alias: "y",
|
|
@@ -63,20 +64,33 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
|
|
|
63
64
|
type: "boolean",
|
|
64
65
|
default: true
|
|
65
66
|
}
|
|
66
|
-
}).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", "Quick start: scaffold the default template and run setup without prompting").example("$0 bootstrap
|
|
67
|
+
}).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", "Quick start: scaffold the default template and run setup without prompting").example("$0 bootstrap --list-templates --output json", "Print the template catalog as JSON").check((argv) => {
|
|
68
|
+
if (argv.agent === true) throw new Error(removedAgent());
|
|
69
|
+
return true;
|
|
70
|
+
}).strict();
|
|
67
71
|
const handler = async (props) => {
|
|
68
72
|
if (props.listTemplates) {
|
|
69
73
|
const templates = await fetchTemplates();
|
|
74
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
75
|
+
writer(props).end(templates.map((t) => ({
|
|
76
|
+
id: t.id,
|
|
77
|
+
title: t.title,
|
|
78
|
+
description: t.description,
|
|
79
|
+
services: t.services ?? []
|
|
80
|
+
})), { fields: [
|
|
81
|
+
"id",
|
|
82
|
+
"title",
|
|
83
|
+
"description",
|
|
84
|
+
"services"
|
|
85
|
+
] });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
70
88
|
for (const t of templates) {
|
|
71
89
|
const services = t.services && t.services.length > 0 ? ` [${t.services.join(" · ")}]` : "";
|
|
72
90
|
process.stdout.write(`${t.id} — ${t.description}${services}\n`);
|
|
73
91
|
}
|
|
74
92
|
return;
|
|
75
93
|
}
|
|
76
|
-
if (props.agent) {
|
|
77
|
-
await runAgentSafely(props);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
94
|
const templates = await resolveTemplateList(props);
|
|
81
95
|
const interactive = !props.default && Boolean(process.stdout.isTTY) && !isCi();
|
|
82
96
|
const template = await resolveSelectedTemplate(props, interactive, templates);
|
|
@@ -163,15 +177,6 @@ const scaffold = async (template, targetDir) => {
|
|
|
163
177
|
log.info("Scaffolded %d files into %s.", filesWritten, targetDir);
|
|
164
178
|
return filesWritten;
|
|
165
179
|
};
|
|
166
|
-
/**
|
|
167
|
-
* After a human scaffold, offer the things you almost always do next: install
|
|
168
|
-
* dependencies, initialize a git repo, and link the directory to a Neon
|
|
169
|
-
* project. In an interactive terminal each is a y/n prompt (skippable up front
|
|
170
|
-
* with --no-install / --no-git / --no-link); `--default` runs install + git
|
|
171
|
-
* without asking; otherwise we just print the manual steps so nothing runs
|
|
172
|
-
* behind the user's back. Agent mode never reaches here — it returns these as
|
|
173
|
-
* structured `next_steps` instead (see {@link runAgent}).
|
|
174
|
-
*/
|
|
175
180
|
const runPostScaffoldSteps = async (props, targetDir, interactive) => {
|
|
176
181
|
const inferred = inferPackageManager(targetDir);
|
|
177
182
|
const defaultPm = resolvePackageManager(targetDir);
|
|
@@ -307,103 +312,6 @@ const printNextSteps = (targetDir, pm, opts) => {
|
|
|
307
312
|
log.info(" See the README to run it.");
|
|
308
313
|
log.info("");
|
|
309
314
|
};
|
|
310
|
-
const runAgentSafely = async (props) => {
|
|
311
|
-
try {
|
|
312
|
-
await runAgent(props);
|
|
313
|
-
} catch (err) {
|
|
314
|
-
emitAgent(toAgentError(err));
|
|
315
|
-
process.exit(1);
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
/**
|
|
319
|
-
* The `--agent` flow: resolve what the flags determine and emit one JSON object
|
|
320
|
-
* describing either the next input needed (`needs_template` / `needs_directory`)
|
|
321
|
-
* or the terminal result (`scaffolded`). Unlike interactive mode it never
|
|
322
|
-
* prompts and never runs install/git/link itself — those come back as structured
|
|
323
|
-
* `next_steps` so the agent can confirm with the user and run them (the link
|
|
324
|
-
* step chains into `neon link --agent`).
|
|
325
|
-
*/
|
|
326
|
-
const runAgent = async (props) => {
|
|
327
|
-
if (!props.template) {
|
|
328
|
-
const templates = await fetchTemplates();
|
|
329
|
-
emitAgent({
|
|
330
|
-
status: "needs_template",
|
|
331
|
-
instruction: `Ask the user which template to scaffold, then re-run the next_command_template with the chosen --template value${props.directory ? "" : " and a target directory"}.`,
|
|
332
|
-
options: templates.map((template) => ({
|
|
333
|
-
id: template.id,
|
|
334
|
-
title: template.title,
|
|
335
|
-
description: template.description,
|
|
336
|
-
...template.services ? { services: template.services } : {}
|
|
337
|
-
})),
|
|
338
|
-
next_command_template: `${getCliName()} bootstrap --agent ${props.directory ? shellArg(props.directory) : "<directory>"} --template <template_id>`
|
|
339
|
-
});
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
const templates = await resolveTemplateList(props);
|
|
343
|
-
const template = findTemplate(templates, props.template);
|
|
344
|
-
if (!template) throw new BootstrapInputError(`Unknown template "${props.template}". Available templates: ${templateIds(templates)}.`, "UNKNOWN_TEMPLATE");
|
|
345
|
-
if (props.directory === void 0) {
|
|
346
|
-
emitAgent({
|
|
347
|
-
status: "needs_directory",
|
|
348
|
-
instruction: "Ask the user which directory to scaffold into (use \".\" for the current directory), then re-run the next_command_template with it.",
|
|
349
|
-
next_command_template: `${getCliName()} bootstrap --agent <directory> --template ${shellArg(template.id)}`
|
|
350
|
-
});
|
|
351
|
-
return;
|
|
352
|
-
}
|
|
353
|
-
const targetDir = resolve(process.cwd(), props.directory === "." ? "" : props.directory);
|
|
354
|
-
ensureTargetUsable(targetDir, props.force);
|
|
355
|
-
const filesWritten = await scaffold(template, targetDir);
|
|
356
|
-
const dir = displayDir(targetDir);
|
|
357
|
-
const runIn = isCurrentDir(targetDir) ? "" : `cd ${shellArg(dir)} && `;
|
|
358
|
-
const installPm = resolvePackageManager(targetDir);
|
|
359
|
-
emitAgent({
|
|
360
|
-
status: "scaffolded",
|
|
361
|
-
directory: targetDir,
|
|
362
|
-
template: {
|
|
363
|
-
id: template.id,
|
|
364
|
-
title: template.title
|
|
365
|
-
},
|
|
366
|
-
files_written: filesWritten,
|
|
367
|
-
next_steps: [
|
|
368
|
-
{
|
|
369
|
-
action: "install_dependencies",
|
|
370
|
-
instruction: `Ask the user whether to install dependencies, then run this in the project directory. ${DO_NOT_SUBSTITUTE_HINT}`,
|
|
371
|
-
command: `${runIn}${formatInstallCommand(installPm)}`
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
action: "initialize_git",
|
|
375
|
-
instruction: "Ask the user whether to initialize a git repository in the project directory.",
|
|
376
|
-
command: `${runIn}git init`
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
action: "link_neon_project",
|
|
380
|
-
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.",
|
|
381
|
-
command: `${runIn}${getCliName()} link --agent`
|
|
382
|
-
}
|
|
383
|
-
],
|
|
384
|
-
message: `Scaffolded "${template.title}" (${filesWritten} files) into ${dir}. Offer the next_steps to the user: install dependencies, initialize git, then link a Neon project.`
|
|
385
|
-
});
|
|
386
|
-
};
|
|
387
|
-
const emitAgent = (response) => {
|
|
388
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
389
|
-
};
|
|
390
|
-
const toAgentError = (err) => {
|
|
391
|
-
if (err instanceof BootstrapInputError) return {
|
|
392
|
-
status: "error",
|
|
393
|
-
code: err.agentCode,
|
|
394
|
-
message: err.message
|
|
395
|
-
};
|
|
396
|
-
if (err instanceof Error) return {
|
|
397
|
-
status: "error",
|
|
398
|
-
code: "INTERNAL_ERROR",
|
|
399
|
-
message: err.message
|
|
400
|
-
};
|
|
401
|
-
return {
|
|
402
|
-
status: "error",
|
|
403
|
-
code: "INTERNAL_ERROR",
|
|
404
|
-
message: String(err)
|
|
405
|
-
};
|
|
406
|
-
};
|
|
407
315
|
const isCurrentDir = (targetDir) => relative(process.cwd(), targetDir) === "";
|
|
408
316
|
/**
|
|
409
317
|
* The path to show the user: the bare relative path for the common
|
|
@@ -415,10 +323,6 @@ const displayDir = (targetDir) => {
|
|
|
415
323
|
if (rel === "") return ".";
|
|
416
324
|
return rel.startsWith("..") ? targetDir : rel;
|
|
417
325
|
};
|
|
418
|
-
const shellArg = (value) => {
|
|
419
|
-
if (/^[A-Za-z0-9._:/-]+$/.test(value)) return value;
|
|
420
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
421
|
-
};
|
|
422
326
|
const onPromptState = (state) => {
|
|
423
327
|
if (state.aborted) {
|
|
424
328
|
process.stdout.write("\x1B[?25h");
|
package/dist/commands/env.js
CHANGED
|
@@ -19,8 +19,7 @@ var env_exports = /* @__PURE__ */ __exportAll({
|
|
|
19
19
|
command: () => "env",
|
|
20
20
|
describe: () => describe,
|
|
21
21
|
handler: () => handler,
|
|
22
|
-
pull: () => pull
|
|
23
|
-
renderAgentPullNote: () => renderAgentPullNote
|
|
22
|
+
pull: () => pull
|
|
24
23
|
});
|
|
25
24
|
const command = "env";
|
|
26
25
|
const describe = "Manage a branch's Neon env variables locally";
|
|
@@ -229,8 +228,7 @@ const pickSelectedVars = (vars, selectedKeys) => {
|
|
|
229
228
|
* On by default; `--no-env-pull` opts out (e.g. when env is injected at runtime via
|
|
230
229
|
* `neon-env run` / `neon dev`, or to keep secrets out of the working tree). The pin is the
|
|
231
230
|
* command's primary effect and has already succeeded by the time this runs, so a pull failure
|
|
232
|
-
* degrades to a warning rather than failing the command.
|
|
233
|
-
* `link --agent` can fold an accurate note into its JSON message.
|
|
231
|
+
* degrades to a warning rather than failing the command.
|
|
234
232
|
*/
|
|
235
233
|
const autoPullEnvAfterPin = async (props) => {
|
|
236
234
|
if (!props.envPull) {
|
|
@@ -250,21 +248,6 @@ Run \`${getCliName()} env pull\` once resolved (e.g. \`${getCliName()} deploy\`
|
|
|
250
248
|
}
|
|
251
249
|
};
|
|
252
250
|
/**
|
|
253
|
-
* Render the one-line env-pull note appended to `link --agent`'s JSON `message`, so an agent
|
|
254
|
-
* reading the structured output knows whether its branch env is already on disk.
|
|
255
|
-
*/
|
|
256
|
-
const renderAgentPullNote = (result) => {
|
|
257
|
-
switch (result.status) {
|
|
258
|
-
case "written": {
|
|
259
|
-
const credential = result.credential?.issued ? ` Issued a new branch credential, so ${result.credential.keys.join(", ")} changed.` : "";
|
|
260
|
-
return ` Pulled ${result.written.length} Neon env var${result.written.length === 1 ? "" : "s"} into ${result.file}.${credential}`;
|
|
261
|
-
}
|
|
262
|
-
case "empty": return " No Neon env vars to pull for this branch yet.";
|
|
263
|
-
case "skipped": return ` Skipped env pull (--no-env-pull); run \`${getCliName()} env pull\` later, or inject env at runtime with \`neon-env run -- <your dev command>\`.`;
|
|
264
|
-
case "failed": return ` Could not pull env vars (${result.message}); run \`${getCliName()} env pull\` once resolved.`;
|
|
265
|
-
}
|
|
266
|
-
};
|
|
267
|
-
/**
|
|
268
251
|
* Keep only the recognized Neon variables from the resolved set, so a stray inherited
|
|
269
252
|
* value never lands in the user's `.env` file. (Today `resolveNeonEnvVars` only emits Neon
|
|
270
253
|
* vars, but filtering keeps the contract explicit and future-proof.)
|
|
@@ -278,4 +261,4 @@ const pickNeonVars = (vars) => {
|
|
|
278
261
|
return out;
|
|
279
262
|
};
|
|
280
263
|
//#endregion
|
|
281
|
-
export { ENV_PULL_SKIPPED_HINT, autoPullEnvAfterPin, builder, command, describe, handler, pull,
|
|
264
|
+
export { ENV_PULL_SKIPPED_HINT, autoPullEnvAfterPin, builder, command, describe, handler, pull, env_exports as t };
|
package/dist/commands/link.js
CHANGED
|
@@ -5,8 +5,9 @@ import { isNeonApiError, messageFromBody } from "../api.js";
|
|
|
5
5
|
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile } from "../context.js";
|
|
6
6
|
import { getCliName } from "../utils/cli_name.js";
|
|
7
7
|
import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
|
|
8
|
-
import { autoPullEnvAfterPin
|
|
8
|
+
import { autoPullEnvAfterPin } from "./env.js";
|
|
9
9
|
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
10
|
+
import { helpEpilogue } from "../utils/help_text.js";
|
|
10
11
|
import { REGIONS } from "./projects.js";
|
|
11
12
|
import prompts from "prompts";
|
|
12
13
|
//#region src/commands/link.ts
|
|
@@ -18,6 +19,19 @@ var link_exports = /* @__PURE__ */ __exportAll({
|
|
|
18
19
|
});
|
|
19
20
|
const PROJECTS_LIST_LIMIT = 100;
|
|
20
21
|
const CREATE_NEW_SENTINEL = "__create_new__";
|
|
22
|
+
const canPromptInteractively = () => !isCi() && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
23
|
+
const nonInteractiveLinkCommands = () => {
|
|
24
|
+
const cli = getCliName();
|
|
25
|
+
return [
|
|
26
|
+
`${cli} orgs list --output json`,
|
|
27
|
+
`${cli} projects list --org-id <org-id> --output json`,
|
|
28
|
+
`${cli} link --project-id <project-id>`,
|
|
29
|
+
`${cli} link --org-id <org-id> --project-name <name> --region-id aws-us-east-2`
|
|
30
|
+
];
|
|
31
|
+
};
|
|
32
|
+
const nonInteractiveLinkHelp = () => nonInteractiveLinkCommands().map((command) => ` ${command}`).join("\n");
|
|
33
|
+
const orgScopedKeyHint = "Organization-scoped API keys cannot list orgs; pass --org-id.";
|
|
34
|
+
const removedAgent = () => `\`${getCliName()} link --agent\` was removed.\nUse:\n${nonInteractiveLinkHelp()}\n${orgScopedKeyHint}`;
|
|
21
35
|
const command = "link";
|
|
22
36
|
const describe = "Link the current directory to a Neon project";
|
|
23
37
|
const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
@@ -47,9 +61,8 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
47
61
|
type: "string"
|
|
48
62
|
},
|
|
49
63
|
agent: {
|
|
50
|
-
|
|
51
|
-
type: "boolean"
|
|
52
|
-
default: false
|
|
64
|
+
hidden: true,
|
|
65
|
+
type: "boolean"
|
|
53
66
|
},
|
|
54
67
|
yes: {
|
|
55
68
|
alias: "y",
|
|
@@ -78,7 +91,10 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
78
91
|
["$0 link --branch-id br-…", "Pin a branch in the already-linked project"],
|
|
79
92
|
["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678", "Write the context offline (no API calls, no verification)"],
|
|
80
93
|
["$0 link --clear", "Forget the current org/project/branch context"]
|
|
81
|
-
])
|
|
94
|
+
]).epilogue(helpEpilogue("Non-interactive (CI, scripts, agents):", ...nonInteractiveLinkCommands().map((command) => ` ${command}`), orgScopedKeyHint)).check((argv) => {
|
|
95
|
+
if (argv.agent === true) throw new Error(removedAgent());
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
82
98
|
const handler = async (props) => {
|
|
83
99
|
if (props.clear) {
|
|
84
100
|
clearContext(props.contextFile);
|
|
@@ -88,10 +104,6 @@ const handler = async (props) => {
|
|
|
88
104
|
runWithoutChecks(props);
|
|
89
105
|
return;
|
|
90
106
|
}
|
|
91
|
-
if (props.agent) {
|
|
92
|
-
await runAgentSafely(props);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
107
|
const inputs = parseInputs(props);
|
|
96
108
|
validateInputs(inputs);
|
|
97
109
|
const existing = readContextFile(props.contextFile);
|
|
@@ -99,14 +111,13 @@ const handler = async (props) => {
|
|
|
99
111
|
await runNonInteractive(props, inputs, existing);
|
|
100
112
|
return;
|
|
101
113
|
}
|
|
102
|
-
if (
|
|
114
|
+
if (!canPromptInteractively()) {
|
|
103
115
|
log.error([
|
|
104
|
-
"Missing inputs and
|
|
116
|
+
"Missing inputs and no interactive terminal for prompts.",
|
|
105
117
|
"",
|
|
106
|
-
"Use
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
` ${getCliName()} link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)`
|
|
118
|
+
"Use:",
|
|
119
|
+
nonInteractiveLinkHelp(),
|
|
120
|
+
orgScopedKeyHint
|
|
110
121
|
].join("\n"));
|
|
111
122
|
process.exit(1);
|
|
112
123
|
return;
|
|
@@ -195,20 +206,6 @@ const runWithoutChecks = (props) => {
|
|
|
195
206
|
projectId: inputs.projectId,
|
|
196
207
|
branch: inputs.branch
|
|
197
208
|
});
|
|
198
|
-
if (props.agent) {
|
|
199
|
-
emitAgent({
|
|
200
|
-
status: "linked",
|
|
201
|
-
context_file: props.contextFile,
|
|
202
|
-
context: {
|
|
203
|
-
orgId: inputs.orgId,
|
|
204
|
-
projectId: inputs.projectId,
|
|
205
|
-
branch: inputs.branch
|
|
206
|
-
},
|
|
207
|
-
project: { id: inputs.projectId },
|
|
208
|
-
message: `Wrote ${props.contextFile} without checks (org ${inputs.orgId}, project ${inputs.projectId}${inputs.branch ? `, branch ${inputs.branch}` : ""}). No verification or env pull was performed.`
|
|
209
|
-
});
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
209
|
printSummary(props, {
|
|
213
210
|
contextFile: props.contextFile,
|
|
214
211
|
orgId: inputs.orgId,
|
|
@@ -218,25 +215,14 @@ const runWithoutChecks = (props) => {
|
|
|
218
215
|
noChecks: true
|
|
219
216
|
});
|
|
220
217
|
};
|
|
221
|
-
/**
|
|
222
|
-
* A bad user-supplied identifier (project/org/branch that doesn't exist or
|
|
223
|
-
* isn't accessible). Carries an `agentCode` so `--agent` mode can report a
|
|
224
|
-
* precise `status: error` code instead of a generic INTERNAL_ERROR, while the
|
|
225
|
-
* human path just prints the clear `message`.
|
|
226
|
-
*/
|
|
227
218
|
var LinkInputError = class extends Error {
|
|
228
|
-
constructor(message
|
|
219
|
+
constructor(message) {
|
|
229
220
|
super(message);
|
|
230
221
|
this.name = "LinkInputError";
|
|
231
|
-
this.agentCode = agentCode;
|
|
232
222
|
}
|
|
233
223
|
};
|
|
234
224
|
const httpStatus = (err) => isNeonApiError(err) ? err.status : void 0;
|
|
235
|
-
/**
|
|
236
|
-
* Fetch a project, turning the common failure modes into clear, actionable
|
|
237
|
-
* errors. 401 is rethrown so the global handler can refresh credentials;
|
|
238
|
-
* everything else surfaces as a `LinkInputError` the user (or agent) can act on.
|
|
239
|
-
*/
|
|
225
|
+
/** 401 must reach the global handler so it can refresh credentials. */
|
|
240
226
|
const fetchProjectOrThrow = async (props, projectId) => {
|
|
241
227
|
try {
|
|
242
228
|
const { data } = await props.apiClient.getProject(projectId);
|
|
@@ -244,8 +230,8 @@ const fetchProjectOrThrow = async (props, projectId) => {
|
|
|
244
230
|
} catch (err) {
|
|
245
231
|
const status = httpStatus(err);
|
|
246
232
|
if (status === 401) throw err;
|
|
247
|
-
if (status === 403) throw new LinkInputError(`You don't have access to project '${projectId}'. Check that your API key's account or organization can see it
|
|
248
|
-
if (status === 404) throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it
|
|
233
|
+
if (status === 403) throw new LinkInputError(`You don't have access to project '${projectId}'. Check that your API key's account or organization can see it.`);
|
|
234
|
+
if (status === 404) throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`);
|
|
249
235
|
throw err;
|
|
250
236
|
}
|
|
251
237
|
};
|
|
@@ -263,7 +249,7 @@ const verifyOrgAccess = async (props, orgId) => {
|
|
|
263
249
|
} catch (err) {
|
|
264
250
|
const status = httpStatus(err);
|
|
265
251
|
if (status === 401) throw err;
|
|
266
|
-
if (status === 403 || status === 404) throw new LinkInputError(`Organization '${orgId}' not found, or your API key doesn't have access to it. Find your org ID in the Neon Console under Settings
|
|
252
|
+
if (status === 403 || status === 404) throw new LinkInputError(`Organization '${orgId}' not found, or your API key doesn't have access to it. Find your org ID in the Neon Console under Settings.`);
|
|
267
253
|
throw err;
|
|
268
254
|
}
|
|
269
255
|
};
|
|
@@ -278,7 +264,7 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
|
278
264
|
const { data } = await props.apiClient.listProjectBranches({ projectId });
|
|
279
265
|
const match = data.branches.find((b) => b.id === branchRef) ?? data.branches.find((b) => b.name === branchRef);
|
|
280
266
|
if (match) return match;
|
|
281
|
-
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${data.branches.length > 0 ? data.branches.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`).join(", ") : "(none)"}. Pin one with \`${getCliName()} checkout <branch
|
|
267
|
+
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${data.branches.length > 0 ? data.branches.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`).join(", ") : "(none)"}. Pin one with \`${getCliName()} checkout <branch>\`.`);
|
|
282
268
|
};
|
|
283
269
|
/**
|
|
284
270
|
* The value to persist for a branch: prefer its human-readable **name** (nicer
|
|
@@ -301,7 +287,7 @@ const branchPersistValue = (branch) => branch.name ?? branch.id;
|
|
|
301
287
|
const resolveOrgForProject = async (props, inputs, existing, projectId) => {
|
|
302
288
|
const projectOrg = (await fetchProjectOrThrow(props, projectId)).org_id ?? void 0;
|
|
303
289
|
if (inputs.orgId) {
|
|
304
|
-
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
|
|
290
|
+
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.`);
|
|
305
291
|
if (!projectOrg) await verifyOrgAccess(props, inputs.orgId);
|
|
306
292
|
return inputs.orgId;
|
|
307
293
|
}
|
|
@@ -568,135 +554,6 @@ const promptRegion = async (props) => {
|
|
|
568
554
|
});
|
|
569
555
|
return regionId;
|
|
570
556
|
};
|
|
571
|
-
const runAgentSafely = async (props) => {
|
|
572
|
-
try {
|
|
573
|
-
const inputs = parseInputs(props);
|
|
574
|
-
validateInputs(inputs);
|
|
575
|
-
await runAgent(props, inputs);
|
|
576
|
-
} catch (err) {
|
|
577
|
-
emitAgent(toAgentError(err));
|
|
578
|
-
process.exit(1);
|
|
579
|
-
}
|
|
580
|
-
};
|
|
581
|
-
const runAgent = async (props, inputs) => {
|
|
582
|
-
const { projectId, projectName, regionId, branch } = inputs;
|
|
583
|
-
const existing = readContextFile(props.contextFile);
|
|
584
|
-
if (projectId) {
|
|
585
|
-
const orgId = await resolveOrgForProject(props, inputs, existing, projectId);
|
|
586
|
-
const pinnedBranch = await resolvePinnedBranch(props, inputs, existing, projectId);
|
|
587
|
-
applyContext(props.contextFile, {
|
|
588
|
-
orgId,
|
|
589
|
-
projectId,
|
|
590
|
-
branch: pinnedBranch
|
|
591
|
-
});
|
|
592
|
-
const orgSuffix = orgId ? ` (org ${orgId})` : "";
|
|
593
|
-
if (pinnedBranch) {
|
|
594
|
-
const pullNote = renderAgentPullNote(await autoPullEnvAfterPin({
|
|
595
|
-
...props,
|
|
596
|
-
projectId,
|
|
597
|
-
branch: pinnedBranch,
|
|
598
|
-
envPull: props.envPull
|
|
599
|
-
}));
|
|
600
|
-
emitAgent({
|
|
601
|
-
status: "linked",
|
|
602
|
-
context_file: props.contextFile,
|
|
603
|
-
context: {
|
|
604
|
-
orgId,
|
|
605
|
-
projectId,
|
|
606
|
-
branch: pinnedBranch
|
|
607
|
-
},
|
|
608
|
-
project: { id: projectId },
|
|
609
|
-
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix} on branch ${pinnedBranch}.${pullNote}`
|
|
610
|
-
});
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
emitAgent({
|
|
614
|
-
status: "linked",
|
|
615
|
-
context_file: props.contextFile,
|
|
616
|
-
context: {
|
|
617
|
-
orgId,
|
|
618
|
-
projectId
|
|
619
|
-
},
|
|
620
|
-
project: { id: projectId },
|
|
621
|
-
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run \`${getCliName()} checkout <branch>\` (omit the branch to list options) to pin one and pull its env vars.`
|
|
622
|
-
});
|
|
623
|
-
return;
|
|
624
|
-
}
|
|
625
|
-
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
626
|
-
if (orgResolution.kind === "needs_selection") {
|
|
627
|
-
emitAgent(buildNeedsOrgResponse(orgResolution));
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
const orgId = orgResolution.orgId;
|
|
631
|
-
if (projectName && !regionId) {
|
|
632
|
-
const regions = await fetchRegions(props);
|
|
633
|
-
emitAgent({
|
|
634
|
-
status: "needs_project_details",
|
|
635
|
-
instruction: `Ask the user which region to create project "${projectName}" in. After they pick one, re-run the next_command_template with the chosen --region-id value.`,
|
|
636
|
-
regions: regions.map((region) => ({
|
|
637
|
-
id: region.region_id,
|
|
638
|
-
name: region.name,
|
|
639
|
-
default: region.default
|
|
640
|
-
})),
|
|
641
|
-
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name ${shellArg(projectName)} --region-id <region_id>`
|
|
642
|
-
});
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
if (projectName && regionId) {
|
|
646
|
-
await verifyOrgAccess(props, orgId);
|
|
647
|
-
const created = await createProject(props, {
|
|
648
|
-
orgId,
|
|
649
|
-
name: projectName,
|
|
650
|
-
regionId
|
|
651
|
-
});
|
|
652
|
-
applyContext(props.contextFile, {
|
|
653
|
-
orgId,
|
|
654
|
-
projectId: created.project.id,
|
|
655
|
-
branch: created.branchName
|
|
656
|
-
});
|
|
657
|
-
const pullNote = renderAgentPullNote(await autoPullEnvAfterPin({
|
|
658
|
-
...props,
|
|
659
|
-
projectId: created.project.id,
|
|
660
|
-
branch: created.branchName,
|
|
661
|
-
envPull: props.envPull
|
|
662
|
-
}));
|
|
663
|
-
emitAgent({
|
|
664
|
-
status: "linked",
|
|
665
|
-
context_file: props.contextFile,
|
|
666
|
-
context: {
|
|
667
|
-
orgId,
|
|
668
|
-
projectId: created.project.id,
|
|
669
|
-
branch: created.branchName
|
|
670
|
-
},
|
|
671
|
-
project: {
|
|
672
|
-
id: created.project.id,
|
|
673
|
-
name: created.project.name,
|
|
674
|
-
region_id: created.project.region_id
|
|
675
|
-
},
|
|
676
|
-
message: `Created project ${created.project.id} ("${created.project.name ?? projectName}") in ${created.project.region_id ?? regionId} and linked ${props.contextFile} on branch ${created.branchName}.${pullNote}`
|
|
677
|
-
});
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
const projects = await listAllProjects(props, orgId);
|
|
681
|
-
const branchNote = branch ? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then \`${getCliName()} checkout ${branch}\`.` : "";
|
|
682
|
-
emitAgent({
|
|
683
|
-
status: "needs_project",
|
|
684
|
-
instruction: (projects.length === 0 ? `Organization ${orgId} has no projects yet. Ask the user for a name for the new project, then re-run the create_option.next_command_template.` : `Ask the user whether to link to one of these ${projects.length} existing projects (use next_command_template with --project-id) or create a new project (use create_option.next_command_template).`) + branchNote,
|
|
685
|
-
options: projects.map((project) => ({
|
|
686
|
-
id: project.id,
|
|
687
|
-
name: project.name,
|
|
688
|
-
region_id: project.region_id
|
|
689
|
-
})),
|
|
690
|
-
create_option: {
|
|
691
|
-
instruction: "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
|
|
692
|
-
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`
|
|
693
|
-
},
|
|
694
|
-
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`
|
|
695
|
-
});
|
|
696
|
-
};
|
|
697
|
-
const emitAgent = (response) => {
|
|
698
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
699
|
-
};
|
|
700
557
|
const ORG_KEY_LIMITED_FRAGMENT = "not allowed for organization API keys";
|
|
701
558
|
const isOrgKeyLimitedError = (err) => {
|
|
702
559
|
if (!isNeonApiError(err)) return false;
|
|
@@ -754,56 +611,6 @@ const detectOrgIdFromProjects = async (props) => {
|
|
|
754
611
|
return;
|
|
755
612
|
}
|
|
756
613
|
};
|
|
757
|
-
const buildNeedsOrgResponse = (resolution) => {
|
|
758
|
-
if (resolution.orgKeyLimited) return {
|
|
759
|
-
status: "needs_org",
|
|
760
|
-
instruction: "This Neon API key is organization-scoped, so the CLI cannot list the user's organizations and no existing project was found to auto-detect the org ID. Ask the user for their Neon organization ID (visible in the Neon Console under the org's Settings page, formatted like `org-bitter-breeze-12345678`) and re-run the next_command_template with that --org-id.",
|
|
761
|
-
options: [],
|
|
762
|
-
next_command_template: `${getCliName()} link --agent --org-id <org_id>`
|
|
763
|
-
};
|
|
764
|
-
const orgs = resolution.orgs;
|
|
765
|
-
return {
|
|
766
|
-
status: "needs_org",
|
|
767
|
-
instruction: orgs.length === 0 ? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking." : `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ? "" : "s"} they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.`,
|
|
768
|
-
options: orgs.map((org) => ({
|
|
769
|
-
id: org.id,
|
|
770
|
-
name: org.name
|
|
771
|
-
})),
|
|
772
|
-
next_command_template: `${getCliName()} link --agent --org-id <org_id>`
|
|
773
|
-
};
|
|
774
|
-
};
|
|
775
|
-
const toAgentError = (err) => {
|
|
776
|
-
if (err instanceof LinkInputError) return {
|
|
777
|
-
status: "error",
|
|
778
|
-
code: err.agentCode,
|
|
779
|
-
message: err.message
|
|
780
|
-
};
|
|
781
|
-
if (isNeonApiError(err)) {
|
|
782
|
-
const status = err.status;
|
|
783
|
-
const apiMessage = messageFromBody(err.data);
|
|
784
|
-
const message = apiMessage !== void 0 && apiMessage.length > 0 ? apiMessage : err.message;
|
|
785
|
-
let code = "API_ERROR";
|
|
786
|
-
if (status === 401 || status === 403) code = "AUTH_ERROR";
|
|
787
|
-
else if (status !== void 0 && status >= 400 && status < 500) code = "CLIENT_ERROR";
|
|
788
|
-
else if (status !== void 0 && status >= 500) code = "SERVER_ERROR";
|
|
789
|
-
else if (err.code === "ECONNABORTED") code = "TIMEOUT";
|
|
790
|
-
return {
|
|
791
|
-
status: "error",
|
|
792
|
-
code,
|
|
793
|
-
message
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
if (err instanceof Error) return {
|
|
797
|
-
status: "error",
|
|
798
|
-
code: "INTERNAL_ERROR",
|
|
799
|
-
message: err.message
|
|
800
|
-
};
|
|
801
|
-
return {
|
|
802
|
-
status: "error",
|
|
803
|
-
code: "INTERNAL_ERROR",
|
|
804
|
-
message: String(err)
|
|
805
|
-
};
|
|
806
|
-
};
|
|
807
614
|
const listAllProjects = async (props, orgId) => {
|
|
808
615
|
const result = [];
|
|
809
616
|
let cursor;
|
|
@@ -965,10 +772,6 @@ const onPromptState = (state) => {
|
|
|
965
772
|
process.exit(1);
|
|
966
773
|
}
|
|
967
774
|
};
|
|
968
|
-
const shellArg = (value) => {
|
|
969
|
-
if (/^[A-Za-z0-9._:/-]+$/.test(value)) return value;
|
|
970
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
971
|
-
};
|
|
972
775
|
const mustString = (value, name) => {
|
|
973
776
|
if (value === void 0) throw new Error(`Internal error: expected ${name} to be set.`);
|
|
974
777
|
return value;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -4,13 +4,13 @@ import { readContextFile } from "../context.js";
|
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
5
|
import { writer } from "../writer.js";
|
|
6
6
|
import { noPassthrough, single } from "../utils/flags.js";
|
|
7
|
+
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
7
8
|
import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
|
|
8
9
|
import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
|
|
9
10
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
10
11
|
import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
|
|
11
12
|
import { confirmMcpInstall } from "../mcp/wizard.js";
|
|
12
13
|
import { resolveMcpPlan } from "../mcp/plan.js";
|
|
13
|
-
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
14
14
|
//#region src/commands/mcp.ts
|
|
15
15
|
var mcp_exports = /* @__PURE__ */ __exportAll({
|
|
16
16
|
builder: () => builder,
|
package/dist/commands/plugins.js
CHANGED
|
@@ -2,11 +2,11 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { writer } from "../writer.js";
|
|
4
4
|
import { noPassthrough } from "../utils/flags.js";
|
|
5
|
+
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
5
6
|
import { NEON_MCP_URL } from "../mcp/install.js";
|
|
6
7
|
import { getAgentDisplayName } from "../mcp/agents.js";
|
|
7
8
|
import "../init/agents.js";
|
|
8
9
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
9
|
-
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
10
10
|
import { pluginsInstallableAgents } from "../plugins/targets.js";
|
|
11
11
|
import { resolvePluginsPlan } from "../plugins/plan.js";
|
|
12
12
|
import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
|
package/dist/commands/skills.js
CHANGED
|
@@ -2,10 +2,10 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { writer } from "../writer.js";
|
|
4
4
|
import { noPassthrough } from "../utils/flags.js";
|
|
5
|
+
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
5
6
|
import { getAgentDisplayName } from "../mcp/agents.js";
|
|
6
7
|
import "../init/agents.js";
|
|
7
8
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
8
|
-
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
9
9
|
import { skillsHelpValues, skillsYesHelp } from "../skills/catalog.js";
|
|
10
10
|
import { mappedSkillsAgentNames, skillsInstallableAgents } from "../skills/targets.js";
|
|
11
11
|
import { confirmSkillsInstall, confirmSkillsUpdate } from "../skills/wizard.js";
|
package/dist/init/bootstrap.js
CHANGED
|
@@ -305,12 +305,7 @@ const downloadTemplate = async (template) => {
|
|
|
305
305
|
if (files.length === 0) throw new Error(`Template subdirectory "${subdir}" was not found in ${owner}/${repo}@${ref}.`);
|
|
306
306
|
return files;
|
|
307
307
|
};
|
|
308
|
-
/**
|
|
309
|
-
* A bad caller-supplied input that an agent (or human) can correct: an unknown
|
|
310
|
-
* template id or a non-empty target directory. Carries an `agentCode` so an
|
|
311
|
-
* agent surface can report a precise error code instead of a generic
|
|
312
|
-
* INTERNAL_ERROR, while a human path just surfaces the clear `message`.
|
|
313
|
-
*/
|
|
308
|
+
/** `agentCode` lets programmatic callers branch without parsing human-readable messages. */
|
|
314
309
|
var BootstrapInputError = class extends Error {
|
|
315
310
|
constructor(message, agentCode) {
|
|
316
311
|
super(message);
|
package/dist/utils/cli_name.js
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
2
|
//#region src/utils/cli_name.ts
|
|
3
|
-
/**
|
|
4
|
-
* The name this CLI was invoked as: `neon` (current) or `neonctl` (legacy alias).
|
|
5
|
-
*
|
|
6
|
-
* Use this for any user-facing string that suggests a command to run — help text,
|
|
7
|
-
* error hints, and especially `--agent` `next_command_template` values, which an agent
|
|
8
|
-
* executes verbatim. Hardcoding `neonctl` breaks those on installs of the `neon` package,
|
|
9
|
-
* which no longer ships a `neonctl` binary (removed in `neon@2.38.0`).
|
|
10
|
-
*
|
|
11
|
-
* Derived from `process.argv[1]`, which is fixed for the process lifetime, so the result
|
|
12
|
-
* is stable whether this is called at module load or per invocation. Mirrors the name used
|
|
13
|
-
* for yargs `.scriptName()` in `index.ts`.
|
|
14
|
-
*/
|
|
3
|
+
/** Command hints must use the invoked binary so users can run them verbatim. */
|
|
15
4
|
const getCliName = () => basename(process.argv[1] ?? "") === "neonctl" ? "neonctl" : "neon";
|
|
16
5
|
//#endregion
|
|
17
6
|
export { getCliName };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.7.0",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"yargs": "17.7.2",
|
|
65
65
|
"yoctocolors": "^2.1.2",
|
|
66
66
|
"@neon/config": "1.0.4",
|
|
67
|
-
"@neon/
|
|
68
|
-
"@neon/
|
|
67
|
+
"@neon/sdk": "2.3.0",
|
|
68
|
+
"@neon/config-runtime": "1.0.4"
|
|
69
69
|
},
|
|
70
70
|
"optionalDependencies": {
|
|
71
71
|
"@napi-rs/keyring": "1.3.0",
|