neon 2.44.0 → 2.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -1
- package/dist/_shared/paths.js +3 -4
- package/dist/commands/bootstrap.js +12 -11
- package/dist/commands/checkout.js +7 -6
- package/dist/commands/config.js +2 -1
- package/dist/commands/data_api.js +4 -3
- package/dist/commands/env.js +5 -4
- package/dist/commands/functions.js +3 -2
- package/dist/commands/init.js +82 -37
- package/dist/commands/ip_allow.js +3 -2
- package/dist/commands/link.js +17 -16
- package/dist/commands/projects.js +3 -2
- package/dist/commands/set_context.js +5 -4
- package/dist/current_branch_fast_path.js +2 -1
- package/dist/dev/env.js +8 -7
- package/dist/dev/runtime.js +45 -1
- package/dist/dev/websocket.js +1031 -0
- package/dist/index.js +2 -2
- package/dist/init/agents.js +127 -0
- package/dist/init/auth.js +77 -0
- package/dist/init/bootstrap.js +448 -0
- package/dist/init/build_config.js +2 -0
- package/dist/init/detect_agent.js +108 -0
- package/dist/init/editors.js +62 -0
- package/dist/init/enrich_output.js +71 -0
- package/dist/init/extension.js +191 -0
- package/dist/init/inspect.js +287 -0
- package/dist/init/interactive.js +651 -0
- package/dist/init/neonctl.js +184 -0
- package/dist/init/orchestrate.js +190 -0
- package/dist/init/phases/auth.js +209 -0
- package/dist/init/phases/cleanup.js +27 -0
- package/dist/init/phases/db.js +283 -0
- package/dist/init/phases/getting_started.js +228 -0
- package/dist/init/phases/mcp.js +227 -0
- package/dist/init/phases/migrations.js +251 -0
- package/dist/init/phases/neon_auth.js +135 -0
- package/dist/init/phases/setup.js +729 -0
- package/dist/init/phases/skills.js +89 -0
- package/dist/init/phases/status.js +70 -0
- package/dist/init/resolve_context.js +107 -0
- package/dist/init/route_command.js +100 -0
- package/dist/init/skills.js +248 -0
- package/dist/init/types.js +1 -0
- package/dist/init/vsix.js +111 -0
- package/dist/psql/command/cmd_meta.js +2 -2
- package/dist/psql/core/mainloop.js +1 -1
- package/dist/psql/core/startup.js +1 -1
- package/dist/psql/core/syncVars.js +3 -3
- package/dist/psql/index.js +1 -1
- package/dist/utils/cli_name.js +14 -0
- package/dist/utils/esbuild.js +1 -1
- package/dist/utils/write_sync.js +39 -0
- package/package.json +18 -12
package/dist/commands/link.js
CHANGED
|
@@ -4,6 +4,7 @@ import { applyContext, contextBranch, readContextFile, setContext, updateContext
|
|
|
4
4
|
import { isCi } from "../env.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
6
|
import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
|
|
7
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
7
8
|
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
8
9
|
import { autoPullEnvAfterPin, renderAgentPullNote } from "./env.js";
|
|
9
10
|
import { REGIONS } from "./projects.js";
|
|
@@ -34,7 +35,7 @@ export const builder = (argv) => argv
|
|
|
34
35
|
alias: "branch-id",
|
|
35
36
|
describe: "Branch name or ID to pin in the context (resolved to its ID before writing). " +
|
|
36
37
|
"Without it, link only resolves the org and project — pin a branch with " +
|
|
37
|
-
|
|
38
|
+
`\`${getCliName()} checkout <branch>\` (link never guesses a default).`,
|
|
38
39
|
type: "string",
|
|
39
40
|
},
|
|
40
41
|
params: {
|
|
@@ -76,7 +77,7 @@ export const builder = (argv) => argv
|
|
|
76
77
|
.example([
|
|
77
78
|
[
|
|
78
79
|
"$0 link --project-id polished-snowflake-12345678",
|
|
79
|
-
|
|
80
|
+
`Link an existing project (org is inferred); pin a branch later with '${getCliName()} checkout'`,
|
|
80
81
|
],
|
|
81
82
|
[
|
|
82
83
|
"$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2",
|
|
@@ -120,9 +121,9 @@ export const handler = async (props) => {
|
|
|
120
121
|
"Missing inputs and CI environment detected (no TTY for prompts).",
|
|
121
122
|
"",
|
|
122
123
|
"Use one of:",
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
` ${getCliName()} link --agent (JSON state machine for agents)`,
|
|
125
|
+
` ${getCliName()} link --project-id <project> (link to an existing project; org is inferred)`,
|
|
126
|
+
` ${getCliName()} link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)`,
|
|
126
127
|
].join("\n"));
|
|
127
128
|
process.exit(1);
|
|
128
129
|
return;
|
|
@@ -180,7 +181,7 @@ const validateInputs = (inputs) => {
|
|
|
180
181
|
throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
|
|
181
182
|
}
|
|
182
183
|
if (inputs.projectName && inputs.branch) {
|
|
183
|
-
throw new Error(
|
|
184
|
+
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>\`.`);
|
|
184
185
|
}
|
|
185
186
|
};
|
|
186
187
|
/**
|
|
@@ -338,7 +339,7 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
|
338
339
|
.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`)
|
|
339
340
|
.join(", ")
|
|
340
341
|
: "(none)";
|
|
341
|
-
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with
|
|
342
|
+
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`${getCliName()} checkout <branch>\`.`, "NOT_FOUND");
|
|
342
343
|
};
|
|
343
344
|
/**
|
|
344
345
|
* The value to persist for a branch: prefer its human-readable **name** (nicer
|
|
@@ -717,7 +718,7 @@ const runAgent = async (props, inputs) => {
|
|
|
717
718
|
context_file: props.contextFile,
|
|
718
719
|
context: { orgId, projectId },
|
|
719
720
|
project: { id: projectId },
|
|
720
|
-
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run
|
|
721
|
+
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.`,
|
|
721
722
|
});
|
|
722
723
|
return;
|
|
723
724
|
}
|
|
@@ -737,7 +738,7 @@ const runAgent = async (props, inputs) => {
|
|
|
737
738
|
name: region.name,
|
|
738
739
|
default: region.default,
|
|
739
740
|
})),
|
|
740
|
-
next_command_template:
|
|
741
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name ${shellArg(projectName)} --region-id <region_id>`,
|
|
741
742
|
});
|
|
742
743
|
return;
|
|
743
744
|
}
|
|
@@ -781,7 +782,7 @@ const runAgent = async (props, inputs) => {
|
|
|
781
782
|
// the instruction rather than silently dropped.
|
|
782
783
|
const projects = await listAllProjects(props, orgId);
|
|
783
784
|
const branchNote = branch
|
|
784
|
-
? ` 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
|
|
785
|
+
? ` 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}\`.`
|
|
785
786
|
: "";
|
|
786
787
|
emitAgent({
|
|
787
788
|
status: "needs_project",
|
|
@@ -796,9 +797,9 @@ const runAgent = async (props, inputs) => {
|
|
|
796
797
|
})),
|
|
797
798
|
create_option: {
|
|
798
799
|
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.",
|
|
799
|
-
next_command_template:
|
|
800
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`,
|
|
800
801
|
},
|
|
801
|
-
next_command_template:
|
|
802
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
|
|
802
803
|
});
|
|
803
804
|
};
|
|
804
805
|
const emitAgent = (response) => {
|
|
@@ -865,7 +866,7 @@ const buildNeedsOrgResponse = (resolution) => {
|
|
|
865
866
|
status: "needs_org",
|
|
866
867
|
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.",
|
|
867
868
|
options: [],
|
|
868
|
-
next_command_template:
|
|
869
|
+
next_command_template: `${getCliName()} link --agent --org-id <org_id>`,
|
|
869
870
|
};
|
|
870
871
|
}
|
|
871
872
|
const orgs = resolution.orgs;
|
|
@@ -875,7 +876,7 @@ const buildNeedsOrgResponse = (resolution) => {
|
|
|
875
876
|
? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking."
|
|
876
877
|
: `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.`,
|
|
877
878
|
options: orgs.map((org) => ({ id: org.id, name: org.name })),
|
|
878
|
-
next_command_template:
|
|
879
|
+
next_command_template: `${getCliName()} link --agent --org-id <org_id>`,
|
|
879
880
|
};
|
|
880
881
|
};
|
|
881
882
|
const toAgentError = (err) => {
|
|
@@ -954,7 +955,7 @@ const resolveInteractiveBranch = async (props, projectId) => {
|
|
|
954
955
|
const picked = await pickBranchInteractively(branches, {
|
|
955
956
|
message: "Which branch would you like to link?",
|
|
956
957
|
nonInteractiveMessage: "No branch could be selected without an interactive terminal. " +
|
|
957
|
-
|
|
958
|
+
`Re-run \`${getCliName()} link\` interactively, or \`${getCliName()} checkout <branch>\` to pin one.`,
|
|
958
959
|
});
|
|
959
960
|
if (picked.kind === "existing") {
|
|
960
961
|
const existing = branches.find((b) => b.id === picked.branchId);
|
|
@@ -1031,7 +1032,7 @@ const printSummary = (_props, summary) => {
|
|
|
1031
1032
|
}
|
|
1032
1033
|
else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
1033
1034
|
lines.push("");
|
|
1034
|
-
lines.push(
|
|
1035
|
+
lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
1035
1036
|
}
|
|
1036
1037
|
lines.push("");
|
|
1037
1038
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
@@ -4,6 +4,7 @@ import { updateContextFile } from "../context.js";
|
|
|
4
4
|
import { isCi } from "../env.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
6
|
import { projectCreateRequest, projectUpdateRequest, } from "../parameters.gen.js";
|
|
7
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
7
8
|
import { getComputeUnits } from "../utils/compute_units.js";
|
|
8
9
|
import { psql } from "../utils/psql.js";
|
|
9
10
|
import { writer } from "../writer.js";
|
|
@@ -392,11 +393,11 @@ The organization ID has been saved in ${props.contextFile}
|
|
|
392
393
|
|
|
393
394
|
If you'd like to change the default organization later, use
|
|
394
395
|
|
|
395
|
-
|
|
396
|
+
${getCliName()} link --org-id <org_id>
|
|
396
397
|
|
|
397
398
|
Or to clear the context file and forget the default organization
|
|
398
399
|
|
|
399
|
-
|
|
400
|
+
${getCliName()} link --clear
|
|
400
401
|
|
|
401
402
|
`);
|
|
402
403
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { applyContext } from "../context.js";
|
|
2
2
|
import { log } from "../log.js";
|
|
3
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
3
4
|
export const command = "set-context";
|
|
4
|
-
export const describe =
|
|
5
|
+
export const describe = `Deprecated: use \`${getCliName()} link\`. Set the .neon context (raw write).`;
|
|
5
6
|
export const builder = (argv) => argv.usage("$0 set-context [options]").options({
|
|
6
7
|
"project-id": {
|
|
7
8
|
describe: "Project ID",
|
|
@@ -17,9 +18,9 @@ export const builder = (argv) => argv.usage("$0 set-context [options]").options(
|
|
|
17
18
|
},
|
|
18
19
|
});
|
|
19
20
|
export const handler = (props) => {
|
|
20
|
-
log.warning(
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
log.warning(`\`${getCliName()} set-context\` is deprecated and will be removed in a future release. ` +
|
|
22
|
+
`Use \`${getCliName()} link\` instead — it verifies inputs and infers the org for you ` +
|
|
23
|
+
`(or \`${getCliName()} link --no-checks\` for the same write-without-checks behavior).`);
|
|
23
24
|
const context = {
|
|
24
25
|
projectId: props.projectId,
|
|
25
26
|
orgId: props.orgId,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { contextBranch, currentContextFile, readContextFile, } from "./context.js";
|
|
2
2
|
import { log } from "./log.js";
|
|
3
|
+
import { getCliName } from "./utils/cli_name.js";
|
|
3
4
|
/**
|
|
4
5
|
* Offline fast path for `(config) status --current-branch` (used by shell prompts).
|
|
5
6
|
*
|
|
@@ -35,7 +36,7 @@ cwd = process.cwd()) => {
|
|
|
35
36
|
process.stdout.write(`${branch}\n`);
|
|
36
37
|
}
|
|
37
38
|
else {
|
|
38
|
-
log.info(
|
|
39
|
+
log.info(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
39
40
|
process.exitCode = 1;
|
|
40
41
|
}
|
|
41
42
|
return true;
|
package/dist/dev/env.js
CHANGED
|
@@ -2,6 +2,7 @@ import { loadConfigFromFile } from "@neon/config";
|
|
|
2
2
|
import { plan, pullConfig } from "@neon/config-runtime";
|
|
3
3
|
import { fetchEnvReusingSecrets, } from "@neon/env/runtime";
|
|
4
4
|
import { log } from "../log.js";
|
|
5
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
5
6
|
/** The API-targeting options every runtime call forwards from the context. */
|
|
6
7
|
const apiOptions = (ctx) => ({
|
|
7
8
|
...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
|
|
@@ -58,7 +59,7 @@ export const resolveNeonEnvVars = async (ctx) => {
|
|
|
58
59
|
if (config) {
|
|
59
60
|
if (!ctx.projectId || !ctx.branchId) {
|
|
60
61
|
throw new MissingBranchContextError("Found a neon.ts but could not resolve the project/branch. " +
|
|
61
|
-
|
|
62
|
+
`Run \`${getCliName()} link\` and \`${getCliName()} checkout <branch>\`, or pass ` +
|
|
62
63
|
"--project-id / --branch.");
|
|
63
64
|
}
|
|
64
65
|
// Resolve env from the policy with its `preview.functions` removed. Functions carry no
|
|
@@ -86,8 +87,8 @@ export const resolveNeonEnvVars = async (ctx) => {
|
|
|
86
87
|
// the functions API here and only mints a storage credential when a bucket exists.
|
|
87
88
|
return await fetchAndProject(pulled.config, ctx);
|
|
88
89
|
}
|
|
89
|
-
throw new MissingBranchContextError(
|
|
90
|
-
|
|
90
|
+
throw new MissingBranchContextError(`No project/branch context found. Link a branch (\`${getCliName()} link\` / ` +
|
|
91
|
+
`\`${getCliName()} checkout\`) or pass --project-id and --branch.`);
|
|
91
92
|
};
|
|
92
93
|
/**
|
|
93
94
|
* `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
|
|
@@ -113,8 +114,8 @@ export const resolveDevEnv = async (ctx) => {
|
|
|
113
114
|
return {
|
|
114
115
|
vars: {},
|
|
115
116
|
skipped: {
|
|
116
|
-
reason:
|
|
117
|
-
|
|
117
|
+
reason: `no linked Neon branch — run \`${getCliName()} link\`, then ` +
|
|
118
|
+
`\`${getCliName()} checkout <branch>\`, to inject DATABASE_URL and friends`,
|
|
118
119
|
},
|
|
119
120
|
};
|
|
120
121
|
}
|
|
@@ -167,8 +168,8 @@ const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
|
167
168
|
const names = missing.map((change) => change.identifier).join(", ");
|
|
168
169
|
throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
|
|
169
170
|
"does not have it yet, so the matching env vars cannot be injected. " +
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
`Provision it first with \`${getCliName()} deploy\` (or \`${getCliName()} config apply\`), ` +
|
|
172
|
+
`then re-run \`${getCliName()} dev\`.`);
|
|
172
173
|
};
|
|
173
174
|
/**
|
|
174
175
|
* A planned change that provisions a branch-level resource the branch lacks: a
|
package/dist/dev/runtime.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { getRequestListener } from "@hono/node-server";
|
|
5
|
+
import { createUpgradeListener, installWebSocketBridge, } from "./websocket.js";
|
|
5
6
|
const isFunction = (value) => typeof value === "function";
|
|
6
7
|
const hasFetchMethod = (value) => typeof value === "object" &&
|
|
7
8
|
value !== null &&
|
|
@@ -27,6 +28,30 @@ export const resolveFetchHandler = (mod) => {
|
|
|
27
28
|
" export default { fetch(req) { /* ... */ } }\n" +
|
|
28
29
|
" export default function (req) { /* ... */ }");
|
|
29
30
|
};
|
|
31
|
+
const hasUpgradeMethod = (value) => typeof value === "object" &&
|
|
32
|
+
value !== null &&
|
|
33
|
+
"upgrade" in value &&
|
|
34
|
+
typeof value.upgrade === "function";
|
|
35
|
+
/**
|
|
36
|
+
* Resolve the user's optional WebSocket entrypoint: a named `export function upgrade`,
|
|
37
|
+
* or an `upgrade` method on the default export. `undefined` when the module has
|
|
38
|
+
* neither, which is the common case — a function without one either uses
|
|
39
|
+
* `upgradeWebSocket()` inside `fetch` or serves no WebSockets at all.
|
|
40
|
+
*
|
|
41
|
+
* Resolution order matches the deployed runtime exactly (named export first, then the
|
|
42
|
+
* default-export method), so a module that resolves one way locally cannot resolve the
|
|
43
|
+
* other way once deployed.
|
|
44
|
+
*/
|
|
45
|
+
export const resolveUpgradeHandler = (mod) => {
|
|
46
|
+
if (typeof mod.upgrade === "function")
|
|
47
|
+
return mod.upgrade;
|
|
48
|
+
const defaultExport = mod.default;
|
|
49
|
+
if (hasUpgradeMethod(defaultExport)) {
|
|
50
|
+
const target = defaultExport;
|
|
51
|
+
return (req, socket, head) => target.upgrade(req, socket, head);
|
|
52
|
+
}
|
|
53
|
+
return undefined;
|
|
54
|
+
};
|
|
30
55
|
/**
|
|
31
56
|
* Wrap a fetch handler so user errors become a 500 response (with the message
|
|
32
57
|
* in the body during dev) instead of crashing the child process.
|
|
@@ -87,11 +112,30 @@ const listen = (server, port, hostname) => new Promise((resolveListen, rejectLis
|
|
|
87
112
|
export const startRuntime = async ({ source, port, hostname, }) => {
|
|
88
113
|
const absoluteSource = resolve(process.cwd(), source);
|
|
89
114
|
const mod = (await import(pathToFileURL(absoluteSource).href));
|
|
90
|
-
const
|
|
115
|
+
const fetchHandler = resolveFetchHandler(mod);
|
|
116
|
+
const handler = withErrorBoundary(fetchHandler);
|
|
117
|
+
// Publish the bridge `upgradeWebSocket()` reads before the user module can serve a
|
|
118
|
+
// request, so the helper resolves locally exactly as it does when deployed.
|
|
119
|
+
installWebSocketBridge();
|
|
91
120
|
const listener = getRequestListener(handler, { hostname });
|
|
92
121
|
const server = createServer((incoming, outgoing) => {
|
|
93
122
|
void listener(incoming, outgoing);
|
|
94
123
|
});
|
|
124
|
+
// Node emits 'upgrade' rather than 'request' for a WebSocket handshake. Without
|
|
125
|
+
// this listener Node hands the handshake to the ordinary request handler, which
|
|
126
|
+
// answers 200 on a connection the client expects to be a 101 — so a function's
|
|
127
|
+
// WebSocket code silently never ran under `neon dev`.
|
|
128
|
+
//
|
|
129
|
+
// The upgrade path takes the RAW handler, not the error-boundary-wrapped one. The
|
|
130
|
+
// boundary turns a throw into a 500 Response, which on this path is
|
|
131
|
+
// indistinguishable from a handler that deliberately declined the upgrade — so a
|
|
132
|
+
// crashing handler would answer 501 ("no WebSocket support") instead of surfacing
|
|
133
|
+
// the error. The upgrade listener has its own equivalent boundary, and reports a
|
|
134
|
+
// throw as the 502 the deployed runtime returns.
|
|
135
|
+
server.on("upgrade", createUpgradeListener({
|
|
136
|
+
fetch: fetchHandler,
|
|
137
|
+
upgrade: resolveUpgradeHandler(mod),
|
|
138
|
+
}));
|
|
95
139
|
const boundPort = await bindPort(server, port, hostname);
|
|
96
140
|
process.stdout.write(`neon-dev:ready ${boundPort}\n`);
|
|
97
141
|
return boundPort;
|