neon 2.1.2 → 2.29.1
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/LICENSE.md +178 -0
- package/README.md +601 -0
- package/dist/analytics.js +156 -0
- package/dist/api.js +665 -0
- package/dist/auth.js +127 -0
- package/dist/callback.html +51 -0
- package/dist/cli.js +9 -0
- package/dist/commands/auth.js +214 -0
- package/dist/commands/bootstrap.js +481 -0
- package/dist/commands/branches.js +481 -0
- package/dist/commands/bucket.js +543 -0
- package/dist/commands/checkout.js +289 -0
- package/dist/commands/config.js +544 -0
- package/dist/commands/connection_string.js +172 -0
- package/dist/commands/data_api.js +285 -0
- package/dist/commands/databases.js +82 -0
- package/dist/commands/deploy.js +26 -0
- package/dist/commands/dev.js +698 -0
- package/dist/commands/env.js +166 -0
- package/dist/commands/functions.js +373 -0
- package/dist/commands/index.js +54 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/ip_allow.js +137 -0
- package/dist/commands/link.js +1121 -0
- package/dist/commands/neon_auth.js +1028 -0
- package/dist/commands/operations.js +28 -0
- package/dist/commands/orgs.js +24 -0
- package/dist/commands/projects.js +372 -0
- package/dist/commands/psql.js +62 -0
- package/dist/commands/roles.js +65 -0
- package/dist/commands/schema_diff.js +151 -0
- package/dist/commands/set_context.js +29 -0
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +15 -0
- package/dist/commands/vpc_endpoints.js +134 -0
- package/dist/config.js +11 -0
- package/dist/config_format.js +72 -0
- package/dist/context.js +177 -0
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +240 -0
- package/dist/dev/functions.js +70 -0
- package/dist/dev/inputs.js +63 -0
- package/dist/dev/runtime.js +146 -0
- package/dist/env.js +36 -0
- package/dist/env_file.js +159 -0
- package/dist/errors.js +80 -0
- package/dist/functions_api.js +44 -0
- package/dist/help.js +146 -0
- package/dist/index.js +234 -0
- package/dist/log.js +18 -0
- package/dist/parameters.gen.js +480 -0
- package/dist/pkg.js +25 -0
- package/dist/psql/cli.js +53 -0
- package/dist/psql/command/cmd_cond.js +437 -0
- package/dist/psql/command/cmd_connect.js +820 -0
- package/dist/psql/command/cmd_copy.js +1035 -0
- package/dist/psql/command/cmd_describe.js +1815 -0
- package/dist/psql/command/cmd_format.js +922 -0
- package/dist/psql/command/cmd_io.js +2193 -0
- package/dist/psql/command/cmd_lo.js +393 -0
- package/dist/psql/command/cmd_meta.js +970 -0
- package/dist/psql/command/cmd_misc.js +187 -0
- package/dist/psql/command/cmd_pipeline.js +1148 -0
- package/dist/psql/command/cmd_restrict.js +171 -0
- package/dist/psql/command/cmd_show.js +766 -0
- package/dist/psql/command/dispatch.js +343 -0
- package/dist/psql/command/inputQueue.js +42 -0
- package/dist/psql/command/shared.js +71 -0
- package/dist/psql/complete/filenames.js +139 -0
- package/dist/psql/complete/index.js +104 -0
- package/dist/psql/complete/matcher.js +315 -0
- package/dist/psql/complete/psqlVars.js +247 -0
- package/dist/psql/complete/queries.js +493 -0
- package/dist/psql/complete/rules.js +2424 -0
- package/dist/psql/core/common.js +1253 -0
- package/dist/psql/core/help.js +576 -0
- package/dist/psql/core/mainloop.js +1360 -0
- package/dist/psql/core/prompt.js +440 -0
- package/dist/psql/core/settings.js +684 -0
- package/dist/psql/core/sqlHelp.js +1066 -0
- package/dist/psql/core/startup.js +846 -0
- package/dist/psql/core/syncVars.js +116 -0
- package/dist/psql/core/variables.js +287 -0
- package/dist/psql/describe/formatters.js +1290 -0
- package/dist/psql/describe/processNamePattern.js +270 -0
- package/dist/psql/describe/queries.js +2378 -0
- package/dist/psql/describe/versionGate.js +43 -0
- package/dist/psql/index.js +2030 -0
- package/dist/psql/io/history.js +299 -0
- package/dist/psql/io/input.js +120 -0
- package/dist/psql/io/lineEditor/buffer.js +325 -0
- package/dist/psql/io/lineEditor/complete.js +227 -0
- package/dist/psql/io/lineEditor/filename.js +159 -0
- package/dist/psql/io/lineEditor/index.js +894 -0
- package/dist/psql/io/lineEditor/keymap.js +745 -0
- package/dist/psql/io/lineEditor/vt100.js +363 -0
- package/dist/psql/io/pgpass.js +202 -0
- package/dist/psql/io/pgservice.js +194 -0
- package/dist/psql/io/psqlrc.js +422 -0
- package/dist/psql/print/aligned.js +1765 -0
- package/dist/psql/print/asciidoc.js +248 -0
- package/dist/psql/print/crosstab.js +463 -0
- package/dist/psql/print/csv.js +95 -0
- package/dist/psql/print/html.js +258 -0
- package/dist/psql/print/json.js +96 -0
- package/dist/psql/print/latex.js +396 -0
- package/dist/psql/print/pager.js +267 -0
- package/dist/psql/print/troff.js +258 -0
- package/dist/psql/print/unaligned.js +119 -0
- package/dist/psql/print/units.js +135 -0
- package/dist/psql/scanner/slash.js +515 -0
- package/dist/psql/scanner/sql.js +914 -0
- package/dist/psql/scanner/stringutils.js +394 -0
- package/dist/psql/types/backslash.js +1 -0
- package/dist/psql/types/connection.js +1 -0
- package/dist/psql/types/index.js +7 -0
- package/dist/psql/types/printer.js +1 -0
- package/dist/psql/types/repl.js +1 -0
- package/dist/psql/types/scanner.js +24 -0
- package/dist/psql/types/settings.js +1 -0
- package/dist/psql/types/variables.js +1 -0
- package/dist/psql/wire/connection.js +2858 -0
- package/dist/psql/wire/copy.js +108 -0
- package/dist/psql/wire/notify.js +59 -0
- package/dist/psql/wire/pipeline.js +521 -0
- package/dist/psql/wire/protocol.js +466 -0
- package/dist/psql/wire/sasl.js +296 -0
- package/dist/psql/wire/tls.js +602 -0
- package/dist/storage_api.js +147 -0
- package/dist/test_utils/fixtures.js +122 -0
- package/dist/test_utils/oauth_server.js +9 -0
- package/dist/types.js +1 -0
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/auth.js +5 -0
- package/dist/utils/branch_notice.js +22 -0
- package/dist/utils/branch_picker.js +103 -0
- package/dist/utils/compute_units.js +28 -0
- package/dist/utils/enrichers.js +161 -0
- package/dist/utils/esbuild.js +158 -0
- package/dist/utils/formats.js +18 -0
- package/dist/utils/middlewares.js +20 -0
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +56 -0
- package/dist/utils/psql.js +120 -0
- package/dist/utils/string.js +5 -0
- package/dist/utils/ui.js +59 -0
- package/dist/utils/zip.js +4 -0
- package/dist/writer.js +97 -0
- package/package.json +117 -14
- package/.jshintrc +0 -3
- package/CHANGELOG.markdown +0 -22
- package/LICENSE +0 -21
- package/README.markdown +0 -76
- package/bower.json +0 -23
- package/license.txt +0 -9
- package/neon.js +0 -234
- package/stdlib/bubbling_support.js +0 -32
- package/stdlib/custom_event.js +0 -54
- package/stdlib/custom_event_support.js +0 -195
- package/stdlib/index.js +0 -7
- package/stdlib/node_support.js +0 -123
- package/stdlib/widget.js +0 -340
- package/test/neon_browser.html +0 -11
- package/test/neon_stdlib_browser.html +0 -15
- package/test/neon_stdlib_test.js +0 -84
- package/test/neon_test.js +0 -64
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { NEON_ENV_VAR_KEYS } from "@neon/env";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { resolveNeonEnvVars } from "../dev/env.js";
|
|
5
|
+
import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
6
|
+
import { log } from "../log.js";
|
|
7
|
+
import { announceTargetBranch } from "../utils/branch_notice.js";
|
|
8
|
+
import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
|
|
9
|
+
export const command = "env";
|
|
10
|
+
export const describe = "Manage a branch's Neon env variables locally";
|
|
11
|
+
/**
|
|
12
|
+
* Shown (to stderr) when `link` / `checkout` skip the bundled env pull because the user passed
|
|
13
|
+
* `--no-env-pull`. Names the two ways to get the branch's vars without an on-disk file written
|
|
14
|
+
* eagerly: an explicit `neonctl env pull`, or runtime injection via `neon-env run`.
|
|
15
|
+
*/
|
|
16
|
+
export const ENV_PULL_SKIPPED_HINT = "Skipped env pull (--no-env-pull). Run `neonctl env pull` to write this branch’s env vars " +
|
|
17
|
+
"(DATABASE_URL, …) into a local .env, or inject them at runtime with `neon-env run -- <your dev command>`.";
|
|
18
|
+
export const builder = (argv) => argv
|
|
19
|
+
.usage("$0 env <sub-command> [options]")
|
|
20
|
+
.options({
|
|
21
|
+
"project-id": { describe: "Project ID", type: "string" },
|
|
22
|
+
branch: { describe: "Branch ID or name", type: "string" },
|
|
23
|
+
})
|
|
24
|
+
.middleware(fillSingleProject)
|
|
25
|
+
.command("pull", "Write the branch's Neon env variables to a local .env file", (yargs) => yargs
|
|
26
|
+
.usage("$0 env pull [options]")
|
|
27
|
+
.options({
|
|
28
|
+
file: {
|
|
29
|
+
describe: "Target .env file to write. Defaults to an existing .env, " +
|
|
30
|
+
"otherwise .env.local. Only Neon variables are updated; other " +
|
|
31
|
+
"lines are preserved.",
|
|
32
|
+
type: "string",
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
.example("$0 env pull", "Write the linked branch's Neon vars into .env.local (or .env if present)")
|
|
36
|
+
.example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file"), async (args) => {
|
|
37
|
+
// Explicit `env pull` announces the branch it's reading from up front so the user
|
|
38
|
+
// can catch "pulled env from the wrong branch" before it overwrites their .env. The
|
|
39
|
+
// bundled auto-pull (link / checkout / apply) stays quiet — those already report the
|
|
40
|
+
// branch they pinned/applied to.
|
|
41
|
+
await pull(args, { announce: true });
|
|
42
|
+
})
|
|
43
|
+
.demandCommand(1);
|
|
44
|
+
export const handler = (args) => args;
|
|
45
|
+
/** Every OS-level env var name `@neon/env` can emit, used only for reporting. */
|
|
46
|
+
const NEON_VAR_NAMES = Object.values(NEON_ENV_VAR_KEYS).flatMap((group) => Object.values(group));
|
|
47
|
+
/**
|
|
48
|
+
* The Neon env vars `env pull` *owns*, so it removes any that the branch no longer has when
|
|
49
|
+
* it reconciles the local `.env` (see {@link pull}). Scoped to the unambiguously Neon-named
|
|
50
|
+
* vars — the `NEON_*` aliases plus `DATABASE_URL[_UNPOOLED]` — so switching a working
|
|
51
|
+
* directory to a project/branch without Auth / the Data API drops the now-stale
|
|
52
|
+
* `NEON_AUTH_*` / `NEON_DATA_API_*` lines instead of leaving credentials for features that
|
|
53
|
+
* aren't enabled.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately **excludes** the storage / AI Gateway vars Neon projects onto third-party SDK
|
|
56
|
+
* names (`AWS_*`, `OPENAI_*`): those collide with credentials a user may set by hand, so
|
|
57
|
+
* `env pull` only ever writes them, never prunes them. (Their Neon-branded siblings —
|
|
58
|
+
* `NEON_STORAGE_*` / `NEON_AI_GATEWAY_*` — are owned and pruned.)
|
|
59
|
+
*/
|
|
60
|
+
const NEON_OWNED_ENV_KEYS = [
|
|
61
|
+
...Object.values(NEON_ENV_VAR_KEYS.postgres),
|
|
62
|
+
...Object.values(NEON_ENV_VAR_KEYS.auth),
|
|
63
|
+
...Object.values(NEON_ENV_VAR_KEYS.dataApi),
|
|
64
|
+
NEON_ENV_VAR_KEYS.aiGateway.neonToken,
|
|
65
|
+
NEON_ENV_VAR_KEYS.aiGateway.neonBaseUrl,
|
|
66
|
+
];
|
|
67
|
+
export const pull = async (props, opts = {}) => {
|
|
68
|
+
const cwd = props.cwd ?? process.cwd();
|
|
69
|
+
const branch = await resolveBranchRef(props);
|
|
70
|
+
if (opts.announce) {
|
|
71
|
+
announceTargetBranch(props, branch, "Pulling env from branch");
|
|
72
|
+
}
|
|
73
|
+
const branchId = branch.branchId;
|
|
74
|
+
// Resolve the target file first and layer its current contents under the resolver's env
|
|
75
|
+
// source. This lets `fetchEnv` reuse one-time secrets that are already on disk — Neon Auth
|
|
76
|
+
// keys and the unified branch credential's `api_token` / `s3_secret_access_key`, which the
|
|
77
|
+
// API returns exactly once — instead of minting a fresh credential on every pull.
|
|
78
|
+
const targetPath = resolveEnvFilePath(cwd, props.file);
|
|
79
|
+
const existingEnv = existsSync(targetPath) ? readEnvFile(targetPath) : {};
|
|
80
|
+
// Reuse `neon dev`'s tiered resolver (neon.ts policy -> plan gate -> fetchEnv, else
|
|
81
|
+
// pullConfig -> fetchEnv). Unlike dev, an unresolved context or failure is surfaced —
|
|
82
|
+
// `env pull` is an explicit action, so it should error rather than write nothing.
|
|
83
|
+
const vars = await resolveNeonEnvVars({
|
|
84
|
+
cwd,
|
|
85
|
+
projectId: props.projectId,
|
|
86
|
+
branchId,
|
|
87
|
+
env: { ...process.env, ...existingEnv },
|
|
88
|
+
...(props.apiKey ? { apiKey: props.apiKey } : {}),
|
|
89
|
+
...(props.apiHost ? { apiHost: props.apiHost } : {}),
|
|
90
|
+
...(props.runtimeApi ? { api: props.runtimeApi } : {}),
|
|
91
|
+
});
|
|
92
|
+
const neonVars = pickNeonVars(vars);
|
|
93
|
+
if (Object.keys(neonVars).length === 0) {
|
|
94
|
+
log.info("No Neon env variables to pull for this branch (no DATABASE_URL or " +
|
|
95
|
+
"enabled Auth / Data API).");
|
|
96
|
+
return { status: "empty" };
|
|
97
|
+
}
|
|
98
|
+
// Reconcile rather than blindly merge: write the branch's current Neon vars and prune any
|
|
99
|
+
// Neon-owned vars the branch no longer has (e.g. NEON_AUTH_* / NEON_DATA_API_* carried over
|
|
100
|
+
// from a previous project/branch). Non-Neon lines are always preserved.
|
|
101
|
+
const { written, removed } = mergeEnvFile(targetPath, neonVars, {
|
|
102
|
+
managedKeys: NEON_OWNED_ENV_KEYS,
|
|
103
|
+
});
|
|
104
|
+
log.info("Pulled %d Neon variable%s into %s: %s", written.length, written.length === 1 ? "" : "s", targetPath, written.join(", "));
|
|
105
|
+
if (removed.length > 0) {
|
|
106
|
+
log.info("Removed %d stale Neon variable%s not enabled on this branch: %s", removed.length, removed.length === 1 ? "" : "s", removed.join(", "));
|
|
107
|
+
}
|
|
108
|
+
return { status: "written", written, file: targetPath };
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Pull a freshly-pinned branch's Neon env vars into a local `.env`, bundled into `link` and
|
|
112
|
+
* `checkout` so the branch-first loop is just *link + checkout* — `env pull` runs for you.
|
|
113
|
+
*
|
|
114
|
+
* On by default; `--no-env-pull` opts out (e.g. when env is injected at runtime via
|
|
115
|
+
* `neon-env run` / `neon dev`, or to keep secrets out of the working tree). The pin is the
|
|
116
|
+
* command's primary effect and has already succeeded by the time this runs, so a pull failure
|
|
117
|
+
* degrades to a warning rather than failing the command. Returns what happened so
|
|
118
|
+
* `link --agent` can fold an accurate note into its JSON message.
|
|
119
|
+
*/
|
|
120
|
+
export const autoPullEnvAfterPin = async (props) => {
|
|
121
|
+
if (!props.envPull) {
|
|
122
|
+
log.info(chalk.dim(ENV_PULL_SKIPPED_HINT));
|
|
123
|
+
return { status: "skipped" };
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
return await pull(props);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
130
|
+
log.warning("Branch pinned, but pulling its Neon env vars failed: %s\n" +
|
|
131
|
+
"Run `neonctl env pull` once resolved (e.g. `neonctl deploy` if a declared service " +
|
|
132
|
+
"is missing), or inject them at runtime with `neon-env run -- <your dev command>`.", message);
|
|
133
|
+
return { status: "failed", message };
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Render the one-line env-pull note appended to `link --agent`'s JSON `message`, so an agent
|
|
138
|
+
* reading the structured output knows whether its branch env is already on disk.
|
|
139
|
+
*/
|
|
140
|
+
export const renderAgentPullNote = (result) => {
|
|
141
|
+
switch (result.status) {
|
|
142
|
+
case "written":
|
|
143
|
+
return ` Pulled ${result.written.length} Neon env var${result.written.length === 1 ? "" : "s"} into ${result.file}.`;
|
|
144
|
+
case "empty":
|
|
145
|
+
return " No Neon env vars to pull for this branch yet.";
|
|
146
|
+
case "skipped":
|
|
147
|
+
return (" Skipped env pull (--no-env-pull); run `neonctl env pull` later, " +
|
|
148
|
+
"or inject env at runtime with `neon-env run -- <your dev command>`.");
|
|
149
|
+
case "failed":
|
|
150
|
+
return ` Could not pull env vars (${result.message}); run \`neonctl env pull\` once resolved.`;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* Keep only the recognized Neon variables from the resolved set, so a stray inherited
|
|
155
|
+
* value never lands in the user's `.env` file. (Today `resolveNeonEnvVars` only emits Neon
|
|
156
|
+
* vars, but filtering keeps the contract explicit and future-proof.)
|
|
157
|
+
*/
|
|
158
|
+
const pickNeonVars = (vars) => {
|
|
159
|
+
const out = {};
|
|
160
|
+
for (const name of NEON_VAR_NAMES) {
|
|
161
|
+
const value = vars[name];
|
|
162
|
+
if (value !== undefined)
|
|
163
|
+
out[name] = value;
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
};
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { isNeonApiError, retryOnLock } from "../api.js";
|
|
4
|
+
import { createDeployment, deleteFunction, getFunction, listFunctions, } from "../functions_api.js";
|
|
5
|
+
import { log } from "../log.js";
|
|
6
|
+
import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
|
|
7
|
+
import { bundleEntry } from "../utils/esbuild.js";
|
|
8
|
+
import { zipBundle } from "../utils/zip.js";
|
|
9
|
+
import { writer } from "../writer.js";
|
|
10
|
+
const FUNCTION_FIELDS = [
|
|
11
|
+
"slug",
|
|
12
|
+
"name",
|
|
13
|
+
"invocation_url",
|
|
14
|
+
"created_at",
|
|
15
|
+
];
|
|
16
|
+
const FUNCTIONS_LIST_LIMIT = 100;
|
|
17
|
+
// Table columns for `functions list`. `status` is a derived field (the
|
|
18
|
+
// table writer reads flat fields only): the current deployment's status.
|
|
19
|
+
const LIST_TABLE_FIELDS = [
|
|
20
|
+
"slug",
|
|
21
|
+
"name",
|
|
22
|
+
"status",
|
|
23
|
+
"invocation_url",
|
|
24
|
+
"created_at",
|
|
25
|
+
];
|
|
26
|
+
const DEPLOYMENT_FIELDS = [
|
|
27
|
+
"id",
|
|
28
|
+
"status",
|
|
29
|
+
"runtime",
|
|
30
|
+
"memory_mib",
|
|
31
|
+
"created_at",
|
|
32
|
+
];
|
|
33
|
+
// Deploy emits the resolved deployment plus the function's invocation_url, so a
|
|
34
|
+
// successful `functions deploy` tells the user exactly where to call the function.
|
|
35
|
+
const DEPLOY_RESULT_FIELDS = [
|
|
36
|
+
"id",
|
|
37
|
+
"status",
|
|
38
|
+
"invocation_url",
|
|
39
|
+
"runtime",
|
|
40
|
+
"memory_mib",
|
|
41
|
+
"created_at",
|
|
42
|
+
];
|
|
43
|
+
// In table mode a failed build's reason gets its own "deployment error"
|
|
44
|
+
// section after the deployment table; json/yaml carry the raw `error` field.
|
|
45
|
+
const writeDeploymentErrorSection = (out, dep) => {
|
|
46
|
+
if (dep.status === "failed" && dep.error) {
|
|
47
|
+
out.write({ reason: dep.error }, { fields: ["reason"], title: "deployment error" });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const SLUG_PATTERN = /^[a-z0-9]{1,20}$/;
|
|
51
|
+
const SLUG_HELP = "Use 1-20 lowercase letters and digits (no hyphens or other characters).";
|
|
52
|
+
// Entry-point discovery order inside --src.
|
|
53
|
+
const ENTRY_CANDIDATES = ["index.ts", "index.mjs", "index.js"];
|
|
54
|
+
// Overridable so tests can poll fast; defaults to 2s in real use.
|
|
55
|
+
const POLL_INTERVAL_MS = Number(process.env.NEON_FUNCTIONS_POLL_INTERVAL_MS) || 2000;
|
|
56
|
+
// Upper bound on --wait polling so the CLI never hangs (e.g. if our deployment
|
|
57
|
+
// never shows up as current_deployment). Overridable so tests can time out fast;
|
|
58
|
+
// defaults to 10 minutes in real use.
|
|
59
|
+
const POLL_TIMEOUT_MS = Number(process.env.NEON_FUNCTIONS_POLL_TIMEOUT_MS) || 600000;
|
|
60
|
+
export const command = "functions";
|
|
61
|
+
export const describe = "Manage Neon Functions";
|
|
62
|
+
export const aliases = ["function"];
|
|
63
|
+
export const builder = (argv) => argv
|
|
64
|
+
.usage("$0 function <sub-command> [options]")
|
|
65
|
+
.options({
|
|
66
|
+
"project-id": {
|
|
67
|
+
describe: "Project ID",
|
|
68
|
+
type: "string",
|
|
69
|
+
},
|
|
70
|
+
branch: {
|
|
71
|
+
describe: "Branch ID or name",
|
|
72
|
+
type: "string",
|
|
73
|
+
},
|
|
74
|
+
})
|
|
75
|
+
.middleware(fillSingleProject)
|
|
76
|
+
.command("deploy <slug>", "Deploy a function from a local directory", (yargs) => yargs
|
|
77
|
+
.positional("slug", {
|
|
78
|
+
describe: "Function slug (1-20 lowercase letters and digits)",
|
|
79
|
+
type: "string",
|
|
80
|
+
demandOption: true,
|
|
81
|
+
})
|
|
82
|
+
.options({
|
|
83
|
+
src: {
|
|
84
|
+
describe: "Function source: a directory containing index.ts, index.mjs, or index.js, or a path to the entry file",
|
|
85
|
+
type: "string",
|
|
86
|
+
},
|
|
87
|
+
// Removed flags, kept hidden so old invocations fail loudly instead
|
|
88
|
+
// of being silently ignored (the CLI has no .strictOptions()).
|
|
89
|
+
path: {
|
|
90
|
+
type: "string",
|
|
91
|
+
hidden: true,
|
|
92
|
+
},
|
|
93
|
+
entry: {
|
|
94
|
+
type: "string",
|
|
95
|
+
hidden: true,
|
|
96
|
+
},
|
|
97
|
+
runtime: {
|
|
98
|
+
describe: "Function runtime",
|
|
99
|
+
type: "string",
|
|
100
|
+
choices: ["nodejs24"],
|
|
101
|
+
},
|
|
102
|
+
env: {
|
|
103
|
+
describe: "Environment variable as KEY=VALUE (repeatable)",
|
|
104
|
+
type: "string",
|
|
105
|
+
array: true,
|
|
106
|
+
},
|
|
107
|
+
wait: {
|
|
108
|
+
describe: "Wait for the deployment to finish building",
|
|
109
|
+
type: "boolean",
|
|
110
|
+
default: true,
|
|
111
|
+
},
|
|
112
|
+
}), (args) => deploy(args))
|
|
113
|
+
.command("list", "List functions on the branch", (yargs) => yargs, (args) => list(args))
|
|
114
|
+
.command("get <slug>", "Show a function's details", (yargs) => yargs
|
|
115
|
+
.positional("slug", {
|
|
116
|
+
describe: "Function slug",
|
|
117
|
+
type: "string",
|
|
118
|
+
demandOption: true,
|
|
119
|
+
})
|
|
120
|
+
.options({
|
|
121
|
+
"list-env-variables": {
|
|
122
|
+
describe: "List the environment variable names of the active deployment",
|
|
123
|
+
type: "boolean",
|
|
124
|
+
alias: "E",
|
|
125
|
+
default: false,
|
|
126
|
+
},
|
|
127
|
+
}), (args) => get(args))
|
|
128
|
+
.command("delete <slug>", "Delete a function on the branch", (yargs) => yargs.positional("slug", {
|
|
129
|
+
describe: "Function slug",
|
|
130
|
+
type: "string",
|
|
131
|
+
demandOption: true,
|
|
132
|
+
}), (args) => deleteFn(args));
|
|
133
|
+
export const handler = (args) => {
|
|
134
|
+
return args;
|
|
135
|
+
};
|
|
136
|
+
const parseEnv = (entries) => {
|
|
137
|
+
if (!entries || entries.length === 0)
|
|
138
|
+
return undefined;
|
|
139
|
+
const map = {};
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const eq = entry.indexOf("=");
|
|
142
|
+
if (eq <= 0) {
|
|
143
|
+
throw new Error(`Invalid --env value "${entry}". Expected KEY=VALUE.`);
|
|
144
|
+
}
|
|
145
|
+
map[entry.slice(0, eq)] = entry.slice(eq + 1);
|
|
146
|
+
}
|
|
147
|
+
return JSON.stringify(map);
|
|
148
|
+
};
|
|
149
|
+
const statusHint = (slug, projectId, branchId) => `Check status with: neonctl function get ${slug} --project-id ${projectId} --branch ${branchId}`;
|
|
150
|
+
// Emit the resolved deployment together with the function's invocation_url, so the
|
|
151
|
+
// deploy output shows where the function is reachable (not just the deployment id).
|
|
152
|
+
const emitDeployResult = (props, deployment, fn) => {
|
|
153
|
+
const out = writer(props).write({ ...deployment, invocation_url: fn?.invocation_url }, { fields: DEPLOY_RESULT_FIELDS });
|
|
154
|
+
if (props.output !== "json" && props.output !== "yaml") {
|
|
155
|
+
writeDeploymentErrorSection(out, deployment);
|
|
156
|
+
}
|
|
157
|
+
out.end();
|
|
158
|
+
};
|
|
159
|
+
// A poll error worth retrying: a network error (no HTTP response), a 5xx, or a
|
|
160
|
+
// 404 from eventual consistency. Anything else (e.g. 401/403) is surfaced.
|
|
161
|
+
const isTransient = (err) => isNeonApiError(err) &&
|
|
162
|
+
(err.status === undefined || err.status === 404 || err.status >= 500);
|
|
163
|
+
const deploy = async (props) => {
|
|
164
|
+
if (props.path !== undefined || props.entry !== undefined) {
|
|
165
|
+
throw new Error("--path and --entry were removed. Use --src <dir>; the entry point " +
|
|
166
|
+
"is discovered as index.ts, index.mjs, or index.js in that directory.");
|
|
167
|
+
}
|
|
168
|
+
// At least one deploy option must be passed (--wait is excluded: it controls
|
|
169
|
+
// output, not what gets deployed).
|
|
170
|
+
const hasOption = props.src !== undefined ||
|
|
171
|
+
props.env !== undefined ||
|
|
172
|
+
props.runtime !== undefined;
|
|
173
|
+
if (!hasOption) {
|
|
174
|
+
throw new Error("Provide at least one option to deploy, e.g. --src or --env. " +
|
|
175
|
+
"See: neonctl function deploy --help.");
|
|
176
|
+
}
|
|
177
|
+
// Cheap, offline validation first - fail before any network round-trip.
|
|
178
|
+
if (!SLUG_PATTERN.test(props.slug)) {
|
|
179
|
+
throw new Error(`Invalid function slug "${props.slug}". ${SLUG_HELP}`);
|
|
180
|
+
}
|
|
181
|
+
const src = props.src ?? ".";
|
|
182
|
+
const runtime = props.runtime ?? "nodejs24";
|
|
183
|
+
const environment = parseEnv(props.env);
|
|
184
|
+
const srcStat = statSync(src, { throwIfNoEntry: false });
|
|
185
|
+
if (srcStat === undefined) {
|
|
186
|
+
throw new Error(`--src path not found: ${src}.`);
|
|
187
|
+
}
|
|
188
|
+
// A file is used as the entry point directly; a directory triggers discovery.
|
|
189
|
+
const source = srcStat.isFile()
|
|
190
|
+
? src
|
|
191
|
+
: ENTRY_CANDIDATES.map((name) => join(src, name)).find((p) => existsSync(p));
|
|
192
|
+
if (source === undefined) {
|
|
193
|
+
throw new Error(`No entry file found in ${src}. Expected one of: ${ENTRY_CANDIDATES.join(", ")}.`);
|
|
194
|
+
}
|
|
195
|
+
// Bundle before any network round-trip so a bundling failure fails fast.
|
|
196
|
+
const zip = zipBundle(await bundleEntry(source));
|
|
197
|
+
const branchId = await branchIdFromProps(props);
|
|
198
|
+
// Snapshot the current version before deploy so we can detect the new one
|
|
199
|
+
// afterward. A missing function (404) or no deployment yet → undefined.
|
|
200
|
+
let before;
|
|
201
|
+
try {
|
|
202
|
+
const fn = await getFunction(props.apiClient, props.projectId, branchId, props.slug);
|
|
203
|
+
before = fn.current_deployment?.id;
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
if (!(isNeonApiError(err) && err.status === 404))
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
await retryOnLock(() => createDeployment(props.apiClient, props.projectId, branchId, props.slug, {
|
|
210
|
+
zip,
|
|
211
|
+
runtime,
|
|
212
|
+
environment,
|
|
213
|
+
}));
|
|
214
|
+
log.info(`Function deployment triggered for function ${props.slug}.`);
|
|
215
|
+
// Best-effort interrupt: a Ctrl-C lands at the next poll boundary. (No
|
|
216
|
+
// automated test; mirrors the resolution branches below, verified manually.)
|
|
217
|
+
let interrupted = false;
|
|
218
|
+
const onSignal = () => {
|
|
219
|
+
interrupted = true;
|
|
220
|
+
};
|
|
221
|
+
process.once("SIGINT", onSignal);
|
|
222
|
+
process.once("SIGTERM", onSignal);
|
|
223
|
+
// Poll until a NEW version appears (id greater than the snapshot, or
|
|
224
|
+
// any version if there was none). --no-wait stops there; --wait stops at a
|
|
225
|
+
// terminal status. Bounded by POLL_TIMEOUT_MS so it never hangs.
|
|
226
|
+
let resolved;
|
|
227
|
+
// The function carries the invocation_url; keep the whole record (not just its
|
|
228
|
+
// current_deployment) so we can surface that URL on success.
|
|
229
|
+
let resolvedFn;
|
|
230
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
231
|
+
try {
|
|
232
|
+
while (!interrupted && Date.now() < deadline) {
|
|
233
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
234
|
+
if (interrupted)
|
|
235
|
+
break;
|
|
236
|
+
// The deploy already succeeded server-side; tolerate transient poll
|
|
237
|
+
// failures and retry on the next interval. Surface anything else.
|
|
238
|
+
let fn;
|
|
239
|
+
try {
|
|
240
|
+
fn = await getFunction(props.apiClient, props.projectId, branchId, props.slug);
|
|
241
|
+
}
|
|
242
|
+
catch (err) {
|
|
243
|
+
if (isTransient(err))
|
|
244
|
+
continue;
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
const dep = fn.current_deployment;
|
|
248
|
+
const isNew = dep !== undefined && (before === undefined || dep.id > before);
|
|
249
|
+
if (isNew && dep) {
|
|
250
|
+
resolved = dep;
|
|
251
|
+
resolvedFn = fn;
|
|
252
|
+
if (!props.wait)
|
|
253
|
+
break;
|
|
254
|
+
if (dep.status === "completed" || dep.status === "failed")
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
process.removeListener("SIGINT", onSignal);
|
|
261
|
+
process.removeListener("SIGTERM", onSignal);
|
|
262
|
+
}
|
|
263
|
+
if (interrupted) {
|
|
264
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
265
|
+
if (resolved)
|
|
266
|
+
emitDeployResult(props, resolved, resolvedFn);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (resolved === undefined) {
|
|
270
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
271
|
+
throw new Error(`Timed out waiting for the deployment of ${props.slug} to start. It may still be in progress.`);
|
|
272
|
+
}
|
|
273
|
+
emitDeployResult(props, resolved, resolvedFn);
|
|
274
|
+
if (!props.wait) {
|
|
275
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (resolved.status === "completed") {
|
|
279
|
+
log.info(`Function deployment ${props.slug}/${resolved.id} completed.`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (resolved.status === "failed") {
|
|
283
|
+
throw new Error(`Function deployment ${props.slug}/${resolved.id} failed.`);
|
|
284
|
+
}
|
|
285
|
+
// --wait, new version appeared but the deadline hit before it finished.
|
|
286
|
+
log.info(statusHint(props.slug, props.projectId, branchId));
|
|
287
|
+
throw new Error(`Timed out waiting for function deployment ${props.slug}/${resolved.id} to finish. It may still be building.`);
|
|
288
|
+
};
|
|
289
|
+
const get = async (props) => {
|
|
290
|
+
const branchId = await branchIdFromProps(props);
|
|
291
|
+
const fn = await getFunction(props.apiClient, props.projectId, branchId, props.slug);
|
|
292
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
293
|
+
writer(props).end(fn, { fields: FUNCTION_FIELDS });
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const out = writer(props).write(fn, {
|
|
297
|
+
fields: FUNCTION_FIELDS,
|
|
298
|
+
title: "function",
|
|
299
|
+
});
|
|
300
|
+
const current = fn.current_deployment;
|
|
301
|
+
const active = fn.active_deployment;
|
|
302
|
+
if (current && active && current.id === active.id) {
|
|
303
|
+
out.write(current, {
|
|
304
|
+
fields: DEPLOYMENT_FIELDS,
|
|
305
|
+
title: "deployment (current, active)",
|
|
306
|
+
});
|
|
307
|
+
writeDeploymentErrorSection(out, current);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
if (current) {
|
|
311
|
+
out.write(current, {
|
|
312
|
+
fields: DEPLOYMENT_FIELDS,
|
|
313
|
+
title: "current deployment",
|
|
314
|
+
});
|
|
315
|
+
// The failure reason is shown only for the current deployment;
|
|
316
|
+
// the active one completed successfully by definition.
|
|
317
|
+
writeDeploymentErrorSection(out, current);
|
|
318
|
+
}
|
|
319
|
+
if (active) {
|
|
320
|
+
out.write(active, {
|
|
321
|
+
fields: DEPLOYMENT_FIELDS,
|
|
322
|
+
title: "active deployment",
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (props.listEnvVariables) {
|
|
327
|
+
out.write((fn.active_deployment?.environment ?? []).map((name) => ({ name })), {
|
|
328
|
+
fields: ["name"],
|
|
329
|
+
title: "environment",
|
|
330
|
+
emptyMessage: "No environment variables on the active deployment.",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
out.end();
|
|
334
|
+
};
|
|
335
|
+
const deleteFn = async (props) => {
|
|
336
|
+
const branchId = await branchIdFromProps(props);
|
|
337
|
+
try {
|
|
338
|
+
await retryOnLock(() => deleteFunction(props.apiClient, props.projectId, branchId, props.slug));
|
|
339
|
+
}
|
|
340
|
+
catch (err) {
|
|
341
|
+
if (isNeonApiError(err) && err.status === 404) {
|
|
342
|
+
throw new Error(`Function "${props.slug}" not found on branch ${branchId}.`);
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
log.info(`Function ${props.slug} deleted from branch ${branchId}`);
|
|
347
|
+
};
|
|
348
|
+
const list = async (props) => {
|
|
349
|
+
const branchId = await branchIdFromProps(props);
|
|
350
|
+
const functions = [];
|
|
351
|
+
let cursor;
|
|
352
|
+
for (;;) {
|
|
353
|
+
const page = await listFunctions(props.apiClient, props.projectId, branchId, { cursor, limit: FUNCTIONS_LIST_LIMIT });
|
|
354
|
+
functions.push(...page.functions);
|
|
355
|
+
log.debug("Got %d functions, next cursor: %s", page.functions.length, page.next);
|
|
356
|
+
// A server echoing the same cursor would loop forever; treat it as
|
|
357
|
+
// the end of the list.
|
|
358
|
+
if (!page.next || page.next === cursor)
|
|
359
|
+
break;
|
|
360
|
+
cursor = page.next;
|
|
361
|
+
}
|
|
362
|
+
if (props.output === "json" || props.output === "yaml") {
|
|
363
|
+
writer(props).end(functions, { fields: FUNCTION_FIELDS });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
writer(props).end(functions.map((fn) => ({
|
|
367
|
+
...fn,
|
|
368
|
+
status: fn.current_deployment?.status ?? "",
|
|
369
|
+
})), {
|
|
370
|
+
fields: LIST_TABLE_FIELDS,
|
|
371
|
+
emptyMessage: "No functions found on this branch.",
|
|
372
|
+
});
|
|
373
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as auth from "./auth.js";
|
|
2
|
+
import * as bootstrap from "./bootstrap.js";
|
|
3
|
+
import * as branches from "./branches.js";
|
|
4
|
+
import * as bucket from "./bucket.js";
|
|
5
|
+
import * as checkout from "./checkout.js";
|
|
6
|
+
import * as config from "./config.js";
|
|
7
|
+
import * as cs from "./connection_string.js";
|
|
8
|
+
import * as dataApi from "./data_api.js";
|
|
9
|
+
import * as databases from "./databases.js";
|
|
10
|
+
import * as deploy from "./deploy.js";
|
|
11
|
+
import * as dev from "./dev.js";
|
|
12
|
+
import * as env from "./env.js";
|
|
13
|
+
import * as functions from "./functions.js";
|
|
14
|
+
import * as init from "./init.js";
|
|
15
|
+
import * as ipAllow from "./ip_allow.js";
|
|
16
|
+
import * as link from "./link.js";
|
|
17
|
+
import * as neonAuth from "./neon_auth.js";
|
|
18
|
+
import * as operations from "./operations.js";
|
|
19
|
+
import * as orgs from "./orgs.js";
|
|
20
|
+
import * as projects from "./projects.js";
|
|
21
|
+
import * as psql from "./psql.js";
|
|
22
|
+
import * as roles from "./roles.js";
|
|
23
|
+
import * as setContext from "./set_context.js";
|
|
24
|
+
import * as status from "./status.js";
|
|
25
|
+
import * as users from "./user.js";
|
|
26
|
+
import * as vpcEndpoints from "./vpc_endpoints.js";
|
|
27
|
+
export default [
|
|
28
|
+
auth,
|
|
29
|
+
users,
|
|
30
|
+
orgs,
|
|
31
|
+
projects,
|
|
32
|
+
ipAllow,
|
|
33
|
+
vpcEndpoints,
|
|
34
|
+
neonAuth,
|
|
35
|
+
branches,
|
|
36
|
+
databases,
|
|
37
|
+
roles,
|
|
38
|
+
operations,
|
|
39
|
+
cs,
|
|
40
|
+
psql,
|
|
41
|
+
setContext,
|
|
42
|
+
checkout,
|
|
43
|
+
link,
|
|
44
|
+
init,
|
|
45
|
+
dataApi,
|
|
46
|
+
functions,
|
|
47
|
+
dev,
|
|
48
|
+
config,
|
|
49
|
+
status,
|
|
50
|
+
deploy,
|
|
51
|
+
env,
|
|
52
|
+
bucket,
|
|
53
|
+
bootstrap,
|
|
54
|
+
];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { detectAgent, enrichResponse, interactiveInit, orchestrate, routeDataStep, } from "neon-init";
|
|
2
|
+
import { sendError } from "../analytics.js";
|
|
3
|
+
import { log } from "../log.js";
|
|
4
|
+
export const command = "init";
|
|
5
|
+
export const describe = "Initialize a project with Neon using your AI coding assistant";
|
|
6
|
+
export const builder = (yargs) => yargs
|
|
7
|
+
.option("context-file", {
|
|
8
|
+
hidden: true,
|
|
9
|
+
})
|
|
10
|
+
.option("agent", {
|
|
11
|
+
alias: "a",
|
|
12
|
+
type: "boolean",
|
|
13
|
+
default: false,
|
|
14
|
+
describe: "Enable agent/JSON mode (agent type is auto-detected).",
|
|
15
|
+
})
|
|
16
|
+
.option("data", {
|
|
17
|
+
type: "string",
|
|
18
|
+
describe: 'JSON object with a "step" field to route to a specific phase and phase-specific options.',
|
|
19
|
+
})
|
|
20
|
+
.option("skip-migrations", {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
default: false,
|
|
23
|
+
describe: "Skip the migrations phase.",
|
|
24
|
+
})
|
|
25
|
+
.option("preview", {
|
|
26
|
+
type: "boolean",
|
|
27
|
+
default: false,
|
|
28
|
+
describe: "Enable preview features (e.g. project bootstrapping from templates).",
|
|
29
|
+
})
|
|
30
|
+
.strict(false);
|
|
31
|
+
export const handler = async (argv) => {
|
|
32
|
+
try {
|
|
33
|
+
// Auto-detect agent from environment. When --agent is explicitly passed,
|
|
34
|
+
// always detect (the user asked for agent mode). Otherwise, require
|
|
35
|
+
// non-TTY stdin to distinguish agent from human in terminal.
|
|
36
|
+
const agent = (argv.agent || !process.stdin.isTTY ? detectAgent() : null) ||
|
|
37
|
+
undefined;
|
|
38
|
+
const isAgentMode = argv.agent || agent !== undefined;
|
|
39
|
+
// --data with a "step" field routes to the appropriate phase
|
|
40
|
+
if (argv.data && isAgentMode) {
|
|
41
|
+
let data;
|
|
42
|
+
try {
|
|
43
|
+
data = JSON.parse(argv.data);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
log.error("Invalid JSON in --data flag. Expected a JSON object.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (typeof data.step === "string") {
|
|
51
|
+
const result = await routeDataStep(data, agent);
|
|
52
|
+
log.info(JSON.stringify(enrichResponse(result), null, 2));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (isAgentMode) {
|
|
57
|
+
const result = await orchestrate({
|
|
58
|
+
agent,
|
|
59
|
+
skipMigrations: argv.skipMigrations,
|
|
60
|
+
preview: argv.preview,
|
|
61
|
+
});
|
|
62
|
+
log.info(JSON.stringify(enrichResponse(result), null, 2));
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await interactiveInit({ preview: argv.preview });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
const exitError = new Error(`failed to run neon-init`);
|
|
70
|
+
sendError(exitError, "NEON_INIT_FAILED");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
};
|