neon 2.45.0 → 2.47.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 +111 -4
- package/dist/_shared/paths.js +3 -4
- package/dist/analytics.js +82 -25
- package/dist/commands/bootstrap.js +12 -11
- package/dist/commands/checkout.js +7 -6
- package/dist/commands/config.js +26 -11
- package/dist/commands/data_api.js +4 -3
- package/dist/commands/dev.js +67 -14
- package/dist/commands/env.js +131 -9
- 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/config_template.js +20 -42
- package/dist/current_branch_fast_path.js +2 -1
- package/dist/dev/env.js +214 -10
- package/dist/env_services.js +51 -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/neon_services.js +143 -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/package_manager.js +51 -4
- package/dist/utils/service_picker.js +6 -6
- package/dist/utils/write_sync.js +39 -0
- package/package.json +18 -12
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared VSIX download utilities for extension installation.
|
|
3
|
+
* Supports corporate proxy via NEON_VSX_GALLERY_URL env var.
|
|
4
|
+
*/
|
|
5
|
+
import { createWriteStream } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { pipeline } from "node:stream/promises";
|
|
9
|
+
import { INTERNAL_VSX_GALLERY } from "./build_config.js";
|
|
10
|
+
export const NEON_EXTENSION_ID = "databricks.neon-local-connect";
|
|
11
|
+
const OPEN_VSX_API = "https://open-vsx.org/api";
|
|
12
|
+
/**
|
|
13
|
+
* Downloads a .vsix file for the Neon extension.
|
|
14
|
+
*
|
|
15
|
+
* Strategy:
|
|
16
|
+
* 1. If NEON_VSX_GALLERY_URL is set, download from the corporate proxy gallery
|
|
17
|
+
* 2. Otherwise, download from the public Open VSX API
|
|
18
|
+
*
|
|
19
|
+
* Returns the path to the temp .vsix file, or null on failure.
|
|
20
|
+
*/
|
|
21
|
+
export async function downloadVsix() {
|
|
22
|
+
// Runtime env var takes priority, then build-time baked value
|
|
23
|
+
const proxyGallery = process.env.NEON_VSX_GALLERY_URL || INTERNAL_VSX_GALLERY || "";
|
|
24
|
+
if (proxyGallery) {
|
|
25
|
+
return downloadFromGallery(proxyGallery);
|
|
26
|
+
}
|
|
27
|
+
return downloadFromOpenVsx();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Downloads from a VS Code marketplace-compatible gallery API (corporate proxy).
|
|
31
|
+
* Uses the VS Code extensionquery POST API to find the VSIX download URL.
|
|
32
|
+
*/
|
|
33
|
+
async function downloadFromGallery(galleryUrl) {
|
|
34
|
+
const [publisher, name] = NEON_EXTENSION_ID.split(".");
|
|
35
|
+
const baseUrl = galleryUrl.replace(/\/+$/, "");
|
|
36
|
+
const queryUrl = `${baseUrl}/extensionquery`;
|
|
37
|
+
try {
|
|
38
|
+
// Query the marketplace API for the extension
|
|
39
|
+
const queryRes = await fetch(queryUrl, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Accept: "application/json;api-version=6.1-preview.1",
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
filters: [
|
|
47
|
+
{
|
|
48
|
+
criteria: [
|
|
49
|
+
{ filterType: 7, value: `${publisher}.${name}` },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
flags: 914,
|
|
54
|
+
}),
|
|
55
|
+
signal: AbortSignal.timeout(15000),
|
|
56
|
+
});
|
|
57
|
+
if (!queryRes.ok)
|
|
58
|
+
return null;
|
|
59
|
+
const data = (await queryRes.json());
|
|
60
|
+
// Find the VSIX download URL from the response
|
|
61
|
+
const extension = data.results?.[0]?.extensions?.[0];
|
|
62
|
+
const latestVersion = extension?.versions?.[0];
|
|
63
|
+
const vsixFile = latestVersion?.files?.find((f) => f.assetType === "Microsoft.VisualStudio.Services.VSIXPackage");
|
|
64
|
+
if (!vsixFile?.source)
|
|
65
|
+
return null;
|
|
66
|
+
// Download the VSIX
|
|
67
|
+
const vsixRes = await fetch(vsixFile.source, {
|
|
68
|
+
signal: AbortSignal.timeout(30000),
|
|
69
|
+
redirect: "follow",
|
|
70
|
+
});
|
|
71
|
+
if (!vsixRes.ok || !vsixRes.body)
|
|
72
|
+
return null;
|
|
73
|
+
const tmpPath = join(tmpdir(), `${NEON_EXTENSION_ID}-proxy.vsix`);
|
|
74
|
+
const fileStream = createWriteStream(tmpPath);
|
|
75
|
+
await pipeline(vsixRes.body, fileStream);
|
|
76
|
+
return tmpPath;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Downloads from the public Open VSX API.
|
|
84
|
+
*/
|
|
85
|
+
async function downloadFromOpenVsx() {
|
|
86
|
+
const [publisher, name] = NEON_EXTENSION_ID.split(".");
|
|
87
|
+
const metaUrl = `${OPEN_VSX_API}/${publisher}/${name}/latest`;
|
|
88
|
+
try {
|
|
89
|
+
const metaRes = await fetch(metaUrl, {
|
|
90
|
+
signal: AbortSignal.timeout(10000),
|
|
91
|
+
});
|
|
92
|
+
if (!metaRes.ok)
|
|
93
|
+
return null;
|
|
94
|
+
const meta = (await metaRes.json());
|
|
95
|
+
const downloadUrl = meta.files?.download;
|
|
96
|
+
if (!downloadUrl)
|
|
97
|
+
return null;
|
|
98
|
+
const vsixRes = await fetch(downloadUrl, {
|
|
99
|
+
signal: AbortSignal.timeout(30000),
|
|
100
|
+
});
|
|
101
|
+
if (!vsixRes.ok || !vsixRes.body)
|
|
102
|
+
return null;
|
|
103
|
+
const tmpPath = join(tmpdir(), `${NEON_EXTENSION_ID}-${meta.version ?? "latest"}.vsix`);
|
|
104
|
+
const fileStream = createWriteStream(tmpPath);
|
|
105
|
+
await pipeline(vsixRes.body, fileStream);
|
|
106
|
+
return tmpPath;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every Neon service a `--service` flag can name, spelled the way a user types it — one
|
|
3
|
+
* vocabulary for the whole CLI.
|
|
4
|
+
*
|
|
5
|
+
* Kebab-case rather than the `neon.ts` field names (`aiGateway`, `buckets`) so a flag reads
|
|
6
|
+
* like a flag, and the full product name rather than a shortening (`object-storage`, not
|
|
7
|
+
* `storage`) so nothing is ambiguous when read on its own.
|
|
8
|
+
*
|
|
9
|
+
* Commands take a **subset** of this via {@link ParseServicesOptions.allowed} — `config init`
|
|
10
|
+
* can only declare what a `neon.ts` has a field for, `env pull` can only pull what produces
|
|
11
|
+
* env vars — but the spelling of a service never varies between them. The order here is the
|
|
12
|
+
* canonical one: parsing sorts into it, so a command's output never depends on the order the
|
|
13
|
+
* flags were typed in.
|
|
14
|
+
*
|
|
15
|
+
* Not to be confused with `NeonFeature` in `init/bootstrap.ts`, which is what a *template*
|
|
16
|
+
* requires. That list comes from remote manifests (`neondatabase/examples/bootstrap.yaml`),
|
|
17
|
+
* spells Postgres `database`, and is not ours to rename.
|
|
18
|
+
*/
|
|
19
|
+
export const NEON_SERVICES = [
|
|
20
|
+
"postgres",
|
|
21
|
+
"auth",
|
|
22
|
+
"data-api",
|
|
23
|
+
"functions",
|
|
24
|
+
"object-storage",
|
|
25
|
+
"ai-gateway",
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Spellings that used to be canonical, and the service they now mean. Accepted so a scripted
|
|
29
|
+
* `--services storage` keeps working, warned about so it does not quietly become a second
|
|
30
|
+
* vocabulary, and absent from help text, errors, and docs so nobody learns it fresh.
|
|
31
|
+
*/
|
|
32
|
+
const DEPRECATED_SERVICE_ALIASES = {
|
|
33
|
+
// `config init --services storage` shipped before the vocabulary was unified.
|
|
34
|
+
storage: "object-storage",
|
|
35
|
+
};
|
|
36
|
+
/** An explicit empty selection, for commands where "declare nothing" is a real answer. */
|
|
37
|
+
export const NO_SERVICES = "none";
|
|
38
|
+
/**
|
|
39
|
+
* What to tell someone still using a retired spelling. A message rather than a log call, so
|
|
40
|
+
* the parser stays free of the CLI's writer and each command can surface it in its own voice.
|
|
41
|
+
*/
|
|
42
|
+
export const deprecatedServiceMessage = (used, canonical) => `"${used}" is the old name for "${canonical}" and still works, but it will be removed. ` +
|
|
43
|
+
`Use "${canonical}".`;
|
|
44
|
+
/**
|
|
45
|
+
* Parse the raw values of a services flag into a canonical selection.
|
|
46
|
+
*
|
|
47
|
+
* Accepts the flag repeated (`-s auth -s postgres`) and comma-separated
|
|
48
|
+
* (`-s auth,postgres`), since both read naturally and users will try either. The result is
|
|
49
|
+
* deduplicated and sorted into {@link NEON_SERVICES} order, so what a command does never
|
|
50
|
+
* depends on typing order.
|
|
51
|
+
*
|
|
52
|
+
* An unrecognized name is rejected rather than dropped: a typo would otherwise act on
|
|
53
|
+
* everything *except* the service that was asked for, and report success. A name that is a
|
|
54
|
+
* real service but not one this command supports says so specifically — "functions has no env
|
|
55
|
+
* variables" is a different problem from a typo, and has a different fix.
|
|
56
|
+
*/
|
|
57
|
+
export const parseServices = (raw, options) => {
|
|
58
|
+
const { allowed, flag, noneMeans, whyUnavailable = {}, onDeprecated, } = options;
|
|
59
|
+
const supported = `Supported values: ${allowed.join(", ")}${noneMeans !== undefined ? `, ${NO_SERVICES}` : ""}.`;
|
|
60
|
+
const names = raw
|
|
61
|
+
.flatMap((value) => value.split(","))
|
|
62
|
+
.map((name) => name.trim())
|
|
63
|
+
.filter((name) => name !== "");
|
|
64
|
+
if (names.length === 0) {
|
|
65
|
+
throw new Error(`${flag} needs at least one service. ${supported}`);
|
|
66
|
+
}
|
|
67
|
+
if (noneMeans !== undefined && names.includes(NO_SERVICES)) {
|
|
68
|
+
// Deduplicate before deciding it was combined with something: a repeated value is
|
|
69
|
+
// a no-op everywhere else in this parser, so `-s none -s none` must be too.
|
|
70
|
+
if (new Set(names).size > 1) {
|
|
71
|
+
throw new Error(`${flag} ${NO_SERVICES} cannot be combined with other services.`);
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
// Canonicalize first and unconditionally, so a retired spelling is reported against the
|
|
76
|
+
// service it means rather than as a word nobody recognizes.
|
|
77
|
+
const deprecated = new Map();
|
|
78
|
+
const resolved = names.map((name) => {
|
|
79
|
+
const canonical = DEPRECATED_SERVICE_ALIASES[name];
|
|
80
|
+
if (canonical === undefined)
|
|
81
|
+
return name;
|
|
82
|
+
deprecated.set(name, canonical);
|
|
83
|
+
return canonical;
|
|
84
|
+
});
|
|
85
|
+
const unsupported = resolved.filter((name) => !allowed.some((service) => service === name));
|
|
86
|
+
if (unsupported.length > 0) {
|
|
87
|
+
throw new Error(`${unsupportedMessage(unsupported, flag, whyUnavailable)} ${supported}`);
|
|
88
|
+
}
|
|
89
|
+
// Warned only once the selection is valid: a run that fails validation should not also
|
|
90
|
+
// carry a "still works" claim about a value that never took effect.
|
|
91
|
+
for (const [used, canonical] of deprecated)
|
|
92
|
+
onDeprecated?.(used, canonical);
|
|
93
|
+
return NEON_SERVICES.filter((service) => allowed.includes(service) && resolved.includes(service));
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* The sentences explaining why a selection was refused. A real Neon service this command
|
|
97
|
+
* cannot act on is a different mistake from a typo — different cause, different fix — so the
|
|
98
|
+
* two are never answered with the same word, and each service carries its reason where the
|
|
99
|
+
* command supplied one.
|
|
100
|
+
*/
|
|
101
|
+
const unsupportedMessage = (unsupported, flag, whyUnavailable) => {
|
|
102
|
+
const known = unsupported.filter((name) => NEON_SERVICES.some((service) => service === name));
|
|
103
|
+
const unknown = unsupported.filter((name) => !known.some((service) => service === name));
|
|
104
|
+
return [
|
|
105
|
+
unknown.length > 0
|
|
106
|
+
? `Unknown service${unknown.length === 1 ? "" : "s"} ${unknown.join(", ")}.`
|
|
107
|
+
: undefined,
|
|
108
|
+
...known.map((service) => {
|
|
109
|
+
const why = whyUnavailable[service];
|
|
110
|
+
return `${service} is not something ${flag} can select${why ? `: ${why}` : ""}.`;
|
|
111
|
+
}),
|
|
112
|
+
]
|
|
113
|
+
.filter((part) => part !== undefined)
|
|
114
|
+
.join(" ");
|
|
115
|
+
};
|
|
116
|
+
/** Every spelling of the services flag, so a habit picked up on one command works on another. */
|
|
117
|
+
const SERVICE_FLAG_NAMES = ["s", "service", "services"];
|
|
118
|
+
/**
|
|
119
|
+
* The yargs option for a services flag, so every command that has one accepts the same
|
|
120
|
+
* spellings (`-s`, `--service`, `--services`) and the same value syntax. `key` is the name the
|
|
121
|
+
* command reads off `argv`; the rest become aliases.
|
|
122
|
+
*/
|
|
123
|
+
export const servicesOption = (params) => ({
|
|
124
|
+
alias: SERVICE_FLAG_NAMES.filter((name) => name !== params.key),
|
|
125
|
+
describe: [
|
|
126
|
+
`${params.describe}: ${params.allowed.join(", ")}.`,
|
|
127
|
+
params.noneMeans !== undefined
|
|
128
|
+
? `Pass "${NO_SERVICES}" for ${params.noneMeans}.`
|
|
129
|
+
: undefined,
|
|
130
|
+
"Repeat the flag or comma-separate.",
|
|
131
|
+
params.also,
|
|
132
|
+
]
|
|
133
|
+
.filter((part) => part !== undefined)
|
|
134
|
+
.join(" "),
|
|
135
|
+
type: "array",
|
|
136
|
+
string: true,
|
|
137
|
+
});
|
|
138
|
+
/**
|
|
139
|
+
* Narrow a yargs value for a services flag to the raw strings, or `undefined` when the flag
|
|
140
|
+
* was not given. `argv` is untyped at the handler, and `string: true` only guarantees the
|
|
141
|
+
* element type when the flag was actually parsed as an array.
|
|
142
|
+
*/
|
|
143
|
+
export const servicesFlagValue = (value) => Array.isArray(value) ? value.map(String) : undefined;
|
|
@@ -754,12 +754,12 @@ PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
|
|
|
754
754
|
* here on purpose (see the project's LICENSE file for terms).
|
|
755
755
|
*/
|
|
756
756
|
const NEON_NOTICE = `
|
|
757
|
-
This is an embedded psql reimplementation that ships with
|
|
757
|
+
This is an embedded psql reimplementation that ships with neon, the
|
|
758
758
|
command-line interface for Neon (https://neon.tech). Neon is part of
|
|
759
759
|
Databricks (https://www.databricks.com).
|
|
760
760
|
|
|
761
761
|
It is an independent reimplementation of psql and is not affiliated with
|
|
762
|
-
or endorsed by the PostgreSQL Global Development Group. See the
|
|
762
|
+
or endorsed by the PostgreSQL Global Development Group. See the neon
|
|
763
763
|
LICENSE file for distribution terms.
|
|
764
764
|
`;
|
|
765
765
|
/**
|
|
@@ -246,7 +246,7 @@ const isHelpKeyword = (line) => {
|
|
|
246
246
|
const stripped = trimmed.replace(/;+\s*$/u, "").trimEnd();
|
|
247
247
|
return stripped === "help";
|
|
248
248
|
};
|
|
249
|
-
const HELP_TEXT = "You are using psql-ts, the embedded TypeScript psql in
|
|
249
|
+
const HELP_TEXT = "You are using psql-ts, the embedded TypeScript psql in neon.\n" +
|
|
250
250
|
"Type: \\copyright for distribution terms\n" +
|
|
251
251
|
" \\h for help with SQL commands\n" +
|
|
252
252
|
" \\? for help with psql commands\n" +
|
|
@@ -223,7 +223,7 @@ export const CLIENT_VERSION = "1.0.0";
|
|
|
223
223
|
// `embedded-ts` label in place of a real PG build number since this is a
|
|
224
224
|
// reimplementation, not a compiled psql. Distinct from the `:VERSION` psql
|
|
225
225
|
// variable (the client-identity string seeded from CLIENT_VERSION).
|
|
226
|
-
const VERSION_STRING = `psql (PostgreSQL) embedded-ts (
|
|
226
|
+
const VERSION_STRING = `psql (PostgreSQL) embedded-ts (neon ${CLIENT_VERSION})`;
|
|
227
227
|
const pushAction = (acts, a) => {
|
|
228
228
|
acts.push(a);
|
|
229
229
|
};
|
|
@@ -85,16 +85,16 @@ export const syncConnectionVars = (vars, conn) => {
|
|
|
85
85
|
* identifier is real and traceable to the shipped binary, while keeping
|
|
86
86
|
* upstream's variable *shapes*:
|
|
87
87
|
*
|
|
88
|
-
* - `VERSION` → `psql-ts (
|
|
88
|
+
* - `VERSION` → `psql-ts (neon) <clientVersion>` — a banner that
|
|
89
89
|
* names the implementation so users can tell they are on the embedded
|
|
90
|
-
* TS port, mirroring the startup banner's `psql-ts (
|
|
90
|
+
* TS port, mirroring the startup banner's `psql-ts (neon, …)` shape.
|
|
91
91
|
* - `VERSION_NAME` → `<clientVersion>` (e.g. `2.22.0`).
|
|
92
92
|
* - `VERSION_NUM` → the same version mapped into PG's NNMMPP integer form
|
|
93
93
|
* (`2.22.0` → `22200`) via {@link clientVersionNum}, so a script doing a
|
|
94
94
|
* numeric `:VERSION_NUM` comparison gets a monotonic integer.
|
|
95
95
|
*/
|
|
96
96
|
export const setStartupVars = (vars, clientVersion) => {
|
|
97
|
-
vars.set("VERSION", `psql-ts (
|
|
97
|
+
vars.set("VERSION", `psql-ts (neon) ${clientVersion}`);
|
|
98
98
|
vars.set("VERSION_NAME", clientVersion);
|
|
99
99
|
vars.set("VERSION_NUM", String(clientVersionNum(clientVersion)));
|
|
100
100
|
};
|
package/dist/psql/index.js
CHANGED
|
@@ -337,7 +337,7 @@ const writeStartupBanner = (connection, out) => {
|
|
|
337
337
|
// Client identifier. Matches upstream's `psql (18.4, server X.Y)` shape
|
|
338
338
|
// but signals that this is the embedded TS implementation so users can tell
|
|
339
339
|
// when they're on the fallback path.
|
|
340
|
-
out.write(`psql-ts (
|
|
340
|
+
out.write(`psql-ts (neon, server ${serverVersion})\n`);
|
|
341
341
|
const tls = connection.getTlsInfo();
|
|
342
342
|
if (tls) {
|
|
343
343
|
const parts = [
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* The name this CLI was invoked as: `neon` (current) or `neonctl` (legacy alias).
|
|
4
|
+
*
|
|
5
|
+
* Use this for any user-facing string that suggests a command to run — help text,
|
|
6
|
+
* error hints, and especially `--agent` `next_command_template` values, which an agent
|
|
7
|
+
* executes verbatim. Hardcoding `neonctl` breaks those on installs of the `neon` package,
|
|
8
|
+
* which no longer ships a `neonctl` binary (removed in `neon@2.38.0`).
|
|
9
|
+
*
|
|
10
|
+
* Derived from `process.argv[1]`, which is fixed for the process lifetime, so the result
|
|
11
|
+
* is stable whether this is called at module load or per invocation. Mirrors the name used
|
|
12
|
+
* for yargs `.scriptName()` in `index.ts`.
|
|
13
|
+
*/
|
|
14
|
+
export const getCliName = () => basename(process.argv[1] ?? "") === "neonctl" ? "neonctl" : "neon";
|
package/dist/utils/esbuild.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { basename, join } from "node:path";
|
|
5
5
|
import which from "which";
|
|
6
|
-
const NOT_FOUND = "esbuild not found.
|
|
6
|
+
const NOT_FOUND = "esbuild not found. neon ships esbuild for most platforms; if you see " +
|
|
7
7
|
"this, install esbuild and ensure it is on your PATH (e.g. `npm i -g " +
|
|
8
8
|
"esbuild`), or set NEON_ESBUILD_PATH to an esbuild binary.";
|
|
9
9
|
// Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
2
4
|
import which from "which";
|
|
3
5
|
import { log } from "../log.js";
|
|
4
6
|
// npm first so it's the default/preselected choice; the rest follow in rough
|
|
@@ -9,6 +11,42 @@ export const PACKAGE_MANAGERS = [
|
|
|
9
11
|
"yarn",
|
|
10
12
|
"bun",
|
|
11
13
|
];
|
|
14
|
+
/**
|
|
15
|
+
* Lockfiles, and the package manager each one belongs to. npm is last on
|
|
16
|
+
* purpose: a repo with both a pnpm lockfile and a leftover `package-lock.json`
|
|
17
|
+
* (which a failed run like the one this fixes can leave behind) is a pnpm repo.
|
|
18
|
+
*/
|
|
19
|
+
const LOCKFILES = [
|
|
20
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
21
|
+
["yarn.lock", "yarn"],
|
|
22
|
+
// bun 1.2+ writes the text `bun.lock`; older versions the binary `bun.lockb`.
|
|
23
|
+
["bun.lock", "bun"],
|
|
24
|
+
["bun.lockb", "bun"],
|
|
25
|
+
["package-lock.json", "npm"],
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* The package manager the project at `cwd` uses, from its lockfile. Searches
|
|
29
|
+
* `cwd` and then each parent up to the repo root: in a monorepo the lockfile
|
|
30
|
+
* sits at the root while we scaffold into a package. Stopping at the root keeps
|
|
31
|
+
* a stray lockfile above the repository from deciding how we install into it.
|
|
32
|
+
*/
|
|
33
|
+
export const detectProjectPackageManager = (cwd) => {
|
|
34
|
+
let dir = cwd;
|
|
35
|
+
for (;;) {
|
|
36
|
+
for (const [file, pm] of LOCKFILES) {
|
|
37
|
+
if (existsSync(join(dir, file)))
|
|
38
|
+
return pm;
|
|
39
|
+
}
|
|
40
|
+
// After the lockfiles, not before: the repo root's own lockfile counts.
|
|
41
|
+
// `.git` is a file rather than a directory in a worktree or submodule.
|
|
42
|
+
if (existsSync(join(dir, ".git")))
|
|
43
|
+
return undefined;
|
|
44
|
+
const parent = dirname(dir);
|
|
45
|
+
if (parent === dir)
|
|
46
|
+
return undefined;
|
|
47
|
+
dir = parent;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
12
50
|
/**
|
|
13
51
|
* The package manager the CLI was invoked through, read from the
|
|
14
52
|
* `npm_config_user_agent` npm sets for `npm exec`/`npx`, `pnpm dlx`, `yarn
|
|
@@ -32,11 +70,20 @@ export const detectPackageManager = () => {
|
|
|
32
70
|
/** The package managers actually on PATH, in {@link PACKAGE_MANAGERS} order. */
|
|
33
71
|
export const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.sync(pm, { nothrow: true }) !== null);
|
|
34
72
|
/**
|
|
35
|
-
* Pick a package manager without prompting: the one the
|
|
36
|
-
* else the
|
|
37
|
-
* `config init`) where there's no
|
|
73
|
+
* Pick a package manager without prompting: the one the project at `cwd` uses,
|
|
74
|
+
* else the one the CLI was invoked through, else the first one installed, else
|
|
75
|
+
* npm. Used by non-interactive flows (e.g. `config init`) where there's no
|
|
76
|
+
* scaffold prompt to hang a picker off.
|
|
77
|
+
*
|
|
78
|
+
* The project wins over the invocation on purpose. `npx neon …` inside a pnpm
|
|
79
|
+
* repo should still install with pnpm — which tool launched us says nothing
|
|
80
|
+
* about which one owns that project's `node_modules`, and running npm against
|
|
81
|
+
* pnpm's symlinked tree is what this ordering exists to prevent.
|
|
38
82
|
*/
|
|
39
|
-
export const resolvePackageManager = () =>
|
|
83
|
+
export const resolvePackageManager = (cwd) => detectProjectPackageManager(cwd) ??
|
|
84
|
+
detectPackageManager() ??
|
|
85
|
+
installedPackageManagers()[0] ??
|
|
86
|
+
"npm";
|
|
40
87
|
/**
|
|
41
88
|
* The argv that adds `packages` as runtime dependencies with `pm`. npm spells it
|
|
42
89
|
* `install`; pnpm/yarn/bun use `add`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import prompts from "prompts";
|
|
2
|
-
import {
|
|
2
|
+
import { CONFIG_INIT_SERVICES } from "../config_template.js";
|
|
3
3
|
/**
|
|
4
|
-
* The picker's rows, in {@link
|
|
5
|
-
* CLI's README ("Managed Better Auth", "Object Storage") rather than the `neon.ts` field
|
|
4
|
+
* The picker's rows, in {@link CONFIG_INIT_SERVICES} order. Titles use the product names from
|
|
5
|
+
* the CLI's README ("Managed Better Auth", "Object Storage") rather than the `neon.ts` field
|
|
6
6
|
* names, since this is the list a user reads before they've seen a policy.
|
|
7
7
|
*/
|
|
8
8
|
const CHOICES = [
|
|
@@ -17,7 +17,7 @@ const CHOICES = [
|
|
|
17
17
|
description: "Long-running, without timeouts, and closer to your database.",
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
|
-
value: "storage",
|
|
20
|
+
value: "object-storage",
|
|
21
21
|
title: "Object Storage",
|
|
22
22
|
description: "S3-compatible blob storage that branches with your projects.",
|
|
23
23
|
},
|
|
@@ -58,7 +58,7 @@ export const pickServicesInteractively = async () => {
|
|
|
58
58
|
if (!Array.isArray(services)) {
|
|
59
59
|
throw new Error("Aborted: no services selected.");
|
|
60
60
|
}
|
|
61
|
-
// Order
|
|
61
|
+
// Order canonically rather than by selection order so the rendered neon.ts is
|
|
62
62
|
// independent of the order the rows were toggled in.
|
|
63
|
-
return
|
|
63
|
+
return CONFIG_INIT_SERVICES.filter((service) => services.includes(service));
|
|
64
64
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { writeSync } from "node:fs";
|
|
2
|
+
/**
|
|
3
|
+
* Write `text` to a file descriptor and do not return until all of it is out.
|
|
4
|
+
*
|
|
5
|
+
* `process.stdout.write` is asynchronous when stdout is a pipe — which it is whenever the
|
|
6
|
+
* caller is an agent or a script capturing output — so a `process.exit()` on the next line
|
|
7
|
+
* discards whatever has not been flushed. The caller sees an exit code and empty stdout.
|
|
8
|
+
*
|
|
9
|
+
* Use this for any final message that is followed by `process.exit`.
|
|
10
|
+
*/
|
|
11
|
+
export function writeAllSync(fd, text) {
|
|
12
|
+
let buffer = Buffer.from(text, "utf8");
|
|
13
|
+
while (buffer.length > 0) {
|
|
14
|
+
try {
|
|
15
|
+
buffer = buffer.subarray(writeSync(fd, buffer));
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
const code = err.code;
|
|
19
|
+
if (code === "EAGAIN") {
|
|
20
|
+
// Wait for the reader rather than spinning on it. A pipe nobody is draining
|
|
21
|
+
// would otherwise burn a core until it is; failure output is small enough
|
|
22
|
+
// that this should never be reached, which is the reason to bound it rather
|
|
23
|
+
// than assume so.
|
|
24
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
// The reader closed the pipe (`neon … | head`). There is nobody left to tell,
|
|
28
|
+
// and this is the last thing the process does, so stop rather than throw.
|
|
29
|
+
if (code === "EPIPE")
|
|
30
|
+
return;
|
|
31
|
+
// Anything else means the write did not happen and the caller is about to exit
|
|
32
|
+
// as though it had. Swallowing it turns a lost payload into an apparent success.
|
|
33
|
+
throw err;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** File descriptors, named so call sites read as the stream they mean. */
|
|
38
|
+
export const STDOUT_FD = 1;
|
|
39
|
+
export const STDERR_FD = 2;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neon",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.47.0",
|
|
4
4
|
"description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"neon",
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"node": ">=20.19.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@clack/core": "0.4.2",
|
|
43
|
+
"@clack/prompts": "0.10.1",
|
|
42
44
|
"@hono/node-server": "2.0.4",
|
|
43
45
|
"@segment/analytics-node": "1.3.0",
|
|
44
46
|
"chalk": "5.3.0",
|
|
@@ -46,20 +48,22 @@
|
|
|
46
48
|
"cli-table": "0.3.11",
|
|
47
49
|
"cliui": "8.0.1",
|
|
48
50
|
"diff": "5.2.0",
|
|
51
|
+
"execa": "^9.5.2",
|
|
49
52
|
"fflate": "^0.8.3",
|
|
50
53
|
"open": "^10.2.0",
|
|
51
54
|
"openid-client": "6.8.1",
|
|
52
55
|
"pg-protocol": "^1.14.0",
|
|
56
|
+
"picocolors": "^1.1.1",
|
|
53
57
|
"prompts": "2.4.2",
|
|
54
58
|
"undici": "^7.28.0",
|
|
55
59
|
"which": "3.0.1",
|
|
56
60
|
"yaml": "^2.9.0",
|
|
57
61
|
"yargs": "17.7.2",
|
|
62
|
+
"yoctocolors": "^2.1.2",
|
|
58
63
|
"@neon/sdk": "1.5.0",
|
|
59
|
-
"@neon/config": "0.14.
|
|
60
|
-
"@neon/
|
|
61
|
-
"neon-
|
|
62
|
-
"@neon/env": "0.14.0"
|
|
64
|
+
"@neon/config": "0.14.1",
|
|
65
|
+
"@neon/env": "0.15.0",
|
|
66
|
+
"@neon/config-runtime": "0.12.5"
|
|
63
67
|
},
|
|
64
68
|
"optionalDependencies": {
|
|
65
69
|
"esbuild": "0.28.1"
|
|
@@ -111,15 +115,17 @@
|
|
|
111
115
|
},
|
|
112
116
|
"scripts": {
|
|
113
117
|
"generateParams": "tsx generateOptionsFromSpec.ts",
|
|
118
|
+
"codegen": "node ../../scripts/sync-shared.mjs . && node scripts/set-vsx-gallery.mjs",
|
|
114
119
|
"clean": "rm -rf dist",
|
|
115
|
-
"build": "
|
|
120
|
+
"build": "pnpm codegen && pnpm generateParams && pnpm clean && tsc -p tsconfig.build.json && cp src/*.html ./dist",
|
|
121
|
+
"build:internal": "node ../../scripts/sync-shared.mjs . && node scripts/set-vsx-gallery.mjs https://cursor-vsx-proxy.cloud.databricks.com/gallery && pnpm generateParams && pnpm clean && tsc -p tsconfig.build.json && cp src/*.html ./dist",
|
|
116
122
|
"bundle": "node pkg.js",
|
|
117
|
-
"typecheck": "
|
|
118
|
-
"lint": "
|
|
123
|
+
"typecheck": "pnpm codegen && tsc --noEmit",
|
|
124
|
+
"lint": "pnpm codegen && pnpm typecheck && biome check src",
|
|
119
125
|
"lint:fix": "pnpm typecheck && biome check src --write",
|
|
120
|
-
"test": "
|
|
121
|
-
"test:ci": "
|
|
122
|
-
"test:e2e": "
|
|
123
|
-
"test:conformance": "
|
|
126
|
+
"test": "pnpm codegen && pnpm --filter neonctl... build && vitest run",
|
|
127
|
+
"test:ci": "pnpm codegen && pnpm build && vitest run",
|
|
128
|
+
"test:e2e": "pnpm codegen && pnpm build && vitest run --config vitest.e2e.config.ts",
|
|
129
|
+
"test:conformance": "pnpm codegen && vitest run --config tests/psql-conformance/vitest.config.ts"
|
|
124
130
|
}
|
|
125
131
|
}
|