neon 2.47.0 → 3.1.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 +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
package/dist/utils/openapi.js
CHANGED
|
@@ -1,114 +1,102 @@
|
|
|
1
|
-
|
|
2
|
-
// enumerate the available routes. The `neon api <path>` request path does NOT
|
|
3
|
-
// depend on this module — it is a pure passthrough — so a stale or unreachable
|
|
4
|
-
// spec never blocks a real API call. Listing degrades gracefully: fresh cache →
|
|
5
|
-
// live fetch → stale cache → clear error.
|
|
1
|
+
import { log } from "../log.js";
|
|
6
2
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
3
|
import { dirname, join } from "node:path";
|
|
8
|
-
|
|
4
|
+
//#region src/utils/openapi.ts
|
|
9
5
|
/** Public URL of the released Neon OpenAPI v2 spec. */
|
|
10
|
-
|
|
6
|
+
const DEFAULT_SPEC_URL = "https://neon.com/api_spec/release/v2.json";
|
|
11
7
|
const CACHE_FILE = "openapi-spec.json";
|
|
12
|
-
const CACHE_TTL_MS =
|
|
13
|
-
const FETCH_TIMEOUT_MS =
|
|
14
|
-
const HTTP_METHODS = new Set([
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
8
|
+
const CACHE_TTL_MS = 864e5;
|
|
9
|
+
const FETCH_TIMEOUT_MS = 1e4;
|
|
10
|
+
const HTTP_METHODS = /* @__PURE__ */ new Set([
|
|
11
|
+
"get",
|
|
12
|
+
"post",
|
|
13
|
+
"put",
|
|
14
|
+
"patch",
|
|
15
|
+
"delete",
|
|
16
|
+
"head",
|
|
17
|
+
"options"
|
|
22
18
|
]);
|
|
23
19
|
async function fetchSpec(url) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
finally {
|
|
39
|
-
clearTimeout(timer);
|
|
40
|
-
}
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
controller.abort();
|
|
23
|
+
}, FETCH_TIMEOUT_MS);
|
|
24
|
+
try {
|
|
25
|
+
const res = await fetch(url, {
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
headers: { Accept: "application/json" }
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) throw new Error(`Failed to fetch OpenAPI spec (${res.status} ${res.statusText})`);
|
|
30
|
+
return await res.json();
|
|
31
|
+
} finally {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
}
|
|
41
34
|
}
|
|
42
35
|
function readCache(cachePath) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(cachePath, "utf8"));
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
49
41
|
}
|
|
50
42
|
function writeCache(cachePath, data) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
43
|
+
try {
|
|
44
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
45
|
+
writeFileSync(cachePath, JSON.stringify(data));
|
|
46
|
+
} catch (err) {
|
|
47
|
+
log.debug("Failed to cache OpenAPI spec: %s", err);
|
|
48
|
+
}
|
|
58
49
|
}
|
|
59
50
|
/**
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
51
|
+
* Load the Neon OpenAPI spec, preferring a fresh on-disk cache, then a live
|
|
52
|
+
* fetch (which refreshes the cache), then a stale cache as a last resort.
|
|
53
|
+
* Returns `null` when no spec can be obtained.
|
|
54
|
+
*/
|
|
55
|
+
async function loadSpec(opts) {
|
|
56
|
+
const { configDir, specUrl, refresh } = opts;
|
|
57
|
+
const cachePath = join(configDir, CACHE_FILE);
|
|
58
|
+
if (!refresh) {
|
|
59
|
+
const cached = readCache(cachePath);
|
|
60
|
+
if (cached && cached.specUrl === specUrl && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
61
|
+
log.debug("Using cached OpenAPI spec from %s", cachePath);
|
|
62
|
+
return cached.spec;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
log.debug("Fetching OpenAPI spec from %s", specUrl);
|
|
67
|
+
const spec = await fetchSpec(specUrl);
|
|
68
|
+
writeCache(cachePath, {
|
|
69
|
+
fetchedAt: Date.now(),
|
|
70
|
+
specUrl,
|
|
71
|
+
spec
|
|
72
|
+
});
|
|
73
|
+
return spec;
|
|
74
|
+
} catch (err) {
|
|
75
|
+
log.debug("Failed to fetch OpenAPI spec: %s", err);
|
|
76
|
+
const stale = readCache(cachePath);
|
|
77
|
+
if (stale && stale.specUrl === specUrl) {
|
|
78
|
+
log.debug("Falling back to stale cached OpenAPI spec");
|
|
79
|
+
return stale.spec;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
91
83
|
}
|
|
92
84
|
/** Flatten a spec into a sorted list of routes (by path, then method). */
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
endpoints.sort((a, b) => a.path === b.path
|
|
111
|
-
? a.method.localeCompare(b.method)
|
|
112
|
-
: a.path.localeCompare(b.path));
|
|
113
|
-
return endpoints;
|
|
85
|
+
function getEndpoints(spec) {
|
|
86
|
+
const endpoints = [];
|
|
87
|
+
for (const [path, item] of Object.entries(spec.paths ?? {})) for (const [method, op] of Object.entries(item)) {
|
|
88
|
+
if (!HTTP_METHODS.has(method.toLowerCase())) continue;
|
|
89
|
+
const operation = op ?? {};
|
|
90
|
+
endpoints.push({
|
|
91
|
+
method: method.toUpperCase(),
|
|
92
|
+
path,
|
|
93
|
+
summary: operation.summary,
|
|
94
|
+
operationId: operation.operationId,
|
|
95
|
+
tags: operation.tags ?? []
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
endpoints.sort((a, b) => a.path === b.path ? a.method.localeCompare(b.method) : a.path.localeCompare(b.path));
|
|
99
|
+
return endpoints;
|
|
114
100
|
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { DEFAULT_SPEC_URL, getEndpoints, loadSpec };
|
|
@@ -1,122 +1,324 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { log } from "../log.js";
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
5
|
import which from "which";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
//#region src/utils/package_manager.ts
|
|
7
|
+
/**
|
|
8
|
+
* Package manager detection and install helpers for the Neon CLI.
|
|
9
|
+
*
|
|
10
|
+
* Every command that shells out to install dependencies should go through here
|
|
11
|
+
* rather than reading `npm_config_user_agent` or lockfiles on its own.
|
|
12
|
+
*
|
|
13
|
+
* **`detect*` may return undefined, `resolve*` never does**, and `inferPackageManager` is the
|
|
14
|
+
* composed detector for callers that can prompt rather than guess.
|
|
15
|
+
*
|
|
16
|
+
* - {@link detectProjectPackageManager} — lockfile walk from `cwd` up to the repo root
|
|
17
|
+
* - {@link detectInvokingPackageManager} — the tool that launched us (`npx`, `pnpm dlx`, …)
|
|
18
|
+
* - {@link inferPackageManager} — project, then invocation, or nothing (when there is
|
|
19
|
+
* something to ask the user, an unanswerable guess is worse than a prompt)
|
|
20
|
+
* - {@link resolvePackageManager} — project lockfile, then invocation, then PATH, then npm
|
|
21
|
+
* (installing into an existing project directory)
|
|
22
|
+
* - {@link resolveInvokingPackageManager} — invocation, then PATH, then npm (global installs,
|
|
23
|
+
* or when the target directory has no lockfile yet, e.g. a fresh scaffold)
|
|
24
|
+
* - {@link installArgs} — argv to install into a project; {@link globalInstallCommand} —
|
|
25
|
+
* command and argv to install a CLI globally, or undefined when nothing here can
|
|
26
|
+
* - {@link formatInstallCommand} — shell-ready `pnpm install` / `npm add …` for agents and hints
|
|
27
|
+
* - {@link formatExecCommand} — shell-ready `pnpm exec drizzle-kit …` for a binary the project
|
|
28
|
+
* already depends on, plus {@link MISSING_BINARY_HINT} for the step that carries it
|
|
29
|
+
*
|
|
30
|
+
* Never re-derive any of this at a call site: a second detector is a second answer, and the
|
|
31
|
+
* two disagree exactly where it hurts — an agent told to run `npm install` in a pnpm project.
|
|
32
|
+
* That includes spelling an install command by hand. Every install string the CLI prints,
|
|
33
|
+
* emits as agent JSON, or hands to {@link runCommand} comes from the formatters here.
|
|
34
|
+
*/
|
|
35
|
+
const PACKAGE_MANAGERS = [
|
|
36
|
+
"npm",
|
|
37
|
+
"pnpm",
|
|
38
|
+
"yarn",
|
|
39
|
+
"bun"
|
|
13
40
|
];
|
|
14
41
|
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
* Lockfiles, and the package manager each one belongs to. npm is last on
|
|
43
|
+
* purpose: a repo with both a pnpm lockfile and a leftover `package-lock.json`
|
|
44
|
+
* (which a failed run like the one this fixes can leave behind) is a pnpm repo.
|
|
45
|
+
*/
|
|
19
46
|
const LOCKFILES = [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
47
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
48
|
+
["yarn.lock", "yarn"],
|
|
49
|
+
["bun.lock", "bun"],
|
|
50
|
+
["bun.lockb", "bun"],
|
|
51
|
+
["package-lock.json", "npm"],
|
|
52
|
+
["npm-shrinkwrap.json", "npm"]
|
|
26
53
|
];
|
|
54
|
+
const lockfileIn = (dir) => {
|
|
55
|
+
for (const [file, pm] of LOCKFILES) if (existsSync(join(dir, file))) return pm;
|
|
56
|
+
};
|
|
57
|
+
/** `.git` is a file rather than a directory in a worktree or a submodule. */
|
|
58
|
+
const isRepoRoot = (dir) => existsSync(join(dir, ".git"));
|
|
59
|
+
/**
|
|
60
|
+
* The repository `dir` belongs to, or undefined when it belongs to none.
|
|
61
|
+
*/
|
|
62
|
+
const repoRoot = (dir) => {
|
|
63
|
+
let current = dir;
|
|
64
|
+
for (;;) {
|
|
65
|
+
if (isRepoRoot(current)) return current;
|
|
66
|
+
const parent = dirname(current);
|
|
67
|
+
if (parent === current) return void 0;
|
|
68
|
+
current = parent;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
27
71
|
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
dir = parent;
|
|
48
|
-
}
|
|
72
|
+
* The physical path, so the walk climbs real parents.
|
|
73
|
+
*
|
|
74
|
+
* `dirname` is lexical: given a symlink to `repo/packages/app`, its parent is
|
|
75
|
+
* the symlink's directory, not `repo/packages` — so the walk would leave the
|
|
76
|
+
* repository immediately and never see the root lockfile.
|
|
77
|
+
*
|
|
78
|
+
* A path that doesn't exist yet is fine and keeps its lexical form: `bootstrap`
|
|
79
|
+
* resolves a target directory before creating it. Anything else — a permission
|
|
80
|
+
* error, a symlink loop, a path component that is not a directory — is a broken
|
|
81
|
+
* input rather than a missing one, and silently falling back to the lexical path
|
|
82
|
+
* would pick a package manager off the wrong tree.
|
|
83
|
+
*/
|
|
84
|
+
const physicalPath = (dir) => {
|
|
85
|
+
try {
|
|
86
|
+
return realpathSync(dir);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err.code === "ENOENT") return dir;
|
|
89
|
+
throw new Error(`Could not resolve ${dir} while detecting the package manager: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
}
|
|
49
91
|
};
|
|
50
92
|
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
* The package manager the project at `cwd` uses, from its lockfile.
|
|
94
|
+
*
|
|
95
|
+
* The repository is the search boundary, and it is located *first*. Inside one,
|
|
96
|
+
* the walk climbs from `cwd` to the repo root, because in a monorepo the
|
|
97
|
+
* lockfile sits at the root while we install into a package. Outside any
|
|
98
|
+
* repository — a directory `bootstrap` is about to scaffold into, which has no
|
|
99
|
+
* `.git` until a later step — only `cwd` itself is read.
|
|
100
|
+
*
|
|
101
|
+
* Locating the boundary first is what makes that second case safe. Climbing and
|
|
102
|
+
* checking as it went, a fresh scaffold under `~` would inherit a stray
|
|
103
|
+
* `~/package-lock.json` and install with npm on the strength of a lockfile
|
|
104
|
+
* belonging to nothing.
|
|
105
|
+
*/
|
|
106
|
+
const detectProjectPackageManager = (cwd) => {
|
|
107
|
+
const start = physicalPath(cwd);
|
|
108
|
+
const root = repoRoot(start);
|
|
109
|
+
if (root === void 0) return lockfileIn(start);
|
|
110
|
+
let dir = start;
|
|
111
|
+
for (;;) {
|
|
112
|
+
const found = lockfileIn(dir);
|
|
113
|
+
if (found) return found;
|
|
114
|
+
if (dir === root) return void 0;
|
|
115
|
+
dir = dirname(dir);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* The package manager the CLI was invoked through, read from the
|
|
120
|
+
* `npm_config_user_agent` npm sets for `npm exec`/`npx`, `pnpm dlx`, `yarn
|
|
121
|
+
* dlx`, and `bunx` (so `pnpm dlx neonctl …` installs with pnpm). Returns
|
|
122
|
+
* undefined when there's nothing to infer from — e.g. a globally-installed
|
|
123
|
+
* `neon`/`neonctl` — so the caller can ask (or fall back) instead of silently
|
|
124
|
+
* assuming npm.
|
|
125
|
+
*/
|
|
126
|
+
const detectInvokingPackageManager = () => {
|
|
127
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
128
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
129
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
130
|
+
if (ua.startsWith("bun")) return "bun";
|
|
131
|
+
if (ua.startsWith("npm")) return "npm";
|
|
69
132
|
};
|
|
70
133
|
/** The package managers actually on PATH, in {@link PACKAGE_MANAGERS} order. */
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
134
|
+
const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.sync(pm, { nothrow: true }) !== null);
|
|
135
|
+
/**
|
|
136
|
+
* What can actually be inferred about the project at `cwd`: its lockfile, else
|
|
137
|
+
* the tool the CLI was invoked through, else nothing.
|
|
138
|
+
*
|
|
139
|
+
* Returns undefined rather than guessing, so an interactive caller can prompt.
|
|
140
|
+
* {@link resolvePackageManager} is the same chain with the unaskable fallbacks
|
|
141
|
+
* appended — use this one only where there is a user to ask.
|
|
142
|
+
*/
|
|
143
|
+
const inferPackageManager = (cwd) => detectProjectPackageManager(cwd) ?? detectInvokingPackageManager();
|
|
144
|
+
/**
|
|
145
|
+
* Pick a package manager without prompting: the one the project at `cwd` uses,
|
|
146
|
+
* else the one the CLI was invoked through, else the first one installed, else
|
|
147
|
+
* npm. Used by non-interactive flows (e.g. `config init`) where there's no
|
|
148
|
+
* scaffold prompt to hang a picker off.
|
|
149
|
+
*
|
|
150
|
+
* The project wins over the invocation on purpose. `npx neon …` inside a pnpm
|
|
151
|
+
* repo should still install with pnpm — which tool launched us says nothing
|
|
152
|
+
* about which one owns that project's `node_modules`, and running npm against
|
|
153
|
+
* pnpm's symlinked tree is what this ordering exists to prevent.
|
|
154
|
+
*/
|
|
155
|
+
const resolvePackageManager = (cwd) => inferPackageManager(cwd) ?? installedPackageManagers()[0] ?? "npm";
|
|
156
|
+
/**
|
|
157
|
+
* Pick a package manager when there is no project lockfile to read — global
|
|
158
|
+
* installs, or scaffolding into a directory that does not exist yet. Same chain
|
|
159
|
+
* as {@link resolvePackageManager} minus the lockfile walk.
|
|
160
|
+
*/
|
|
161
|
+
const resolveInvokingPackageManager = () => detectInvokingPackageManager() ?? installedPackageManagers()[0] ?? "npm";
|
|
162
|
+
/**
|
|
163
|
+
* The argv for an install with `pm`: every dependency in the manifest when
|
|
164
|
+
* `packages` is empty, otherwise those packages added to it. npm spells the add
|
|
165
|
+
* verb `install`; pnpm/yarn/bun use `add`. All four spell the whole-manifest
|
|
166
|
+
* install `install`.
|
|
167
|
+
*
|
|
168
|
+
* The one argv builder — pass its result to {@link runCommand}, or use
|
|
169
|
+
* {@link formatInstallCommand} for a string. Nothing else assembles these words.
|
|
170
|
+
*/
|
|
171
|
+
const installArgs = (pm, packages, options) => packages?.length ? [
|
|
172
|
+
pm === "npm" ? "install" : "add",
|
|
173
|
+
...options?.dev ? ["-D"] : [],
|
|
174
|
+
...packages
|
|
175
|
+
] : ["install"];
|
|
176
|
+
/**
|
|
177
|
+
* yarn's major version, or undefined when it can't be read. Only consulted on
|
|
178
|
+
* the global-install path, which already spawns.
|
|
179
|
+
*/
|
|
180
|
+
const yarnMajor = () => {
|
|
181
|
+
const major = spawnSync("yarn", ["--version"], {
|
|
182
|
+
encoding: "utf8",
|
|
183
|
+
timeout: 5e3,
|
|
184
|
+
shell: process.platform === "win32"
|
|
185
|
+
}).stdout?.trim().match(/^(\d+)\./);
|
|
186
|
+
return major ? Number(major[1]) : void 0;
|
|
187
|
+
};
|
|
188
|
+
/** The managers that can install a global CLI at all — yarn Berry cannot. */
|
|
189
|
+
const GLOBAL_CAPABLE = [
|
|
190
|
+
"npm",
|
|
191
|
+
"pnpm",
|
|
192
|
+
"bun"
|
|
193
|
+
];
|
|
194
|
+
/**
|
|
195
|
+
* The argv that installs `pkg` globally — a standalone CLI, not a project
|
|
196
|
+
* dependency — or undefined when this machine has no way to do it.
|
|
197
|
+
*
|
|
198
|
+
* Yarn Berry removed global installs with no replacement, so `yarn global add`
|
|
199
|
+
* works on Classic only and Berry answers "Unknown command". Berry falls back to
|
|
200
|
+
* whichever global-capable manager is actually on PATH, which is normally npm.
|
|
201
|
+
*
|
|
202
|
+
* Substituting a manager is safe *here* and nowhere else in this module: a
|
|
203
|
+
* global install has no project dependency tree to be wrong about, which is the
|
|
204
|
+
* whole reason the project's manager must win for {@link installArgs}. What is
|
|
205
|
+
* not safe is naming a command that cannot run — npm ships with Node but is
|
|
206
|
+
* packaged separately on some Linux distributions, so returning `npm install -g`
|
|
207
|
+
* unchecked would hand a Berry user a command their machine does not have. The
|
|
208
|
+
* caller decides what to say when there is nothing to return.
|
|
209
|
+
*/
|
|
210
|
+
const globalInstallCommand = (pm, pkg) => {
|
|
211
|
+
if (pm === "yarn" && yarnMajor() === 1) return {
|
|
212
|
+
command: "yarn",
|
|
213
|
+
args: [
|
|
214
|
+
"global",
|
|
215
|
+
"add",
|
|
216
|
+
pkg
|
|
217
|
+
]
|
|
218
|
+
};
|
|
219
|
+
const installed = installedPackageManagers();
|
|
220
|
+
const usable = pm !== "yarn" && installed.includes(pm) ? pm : installed.find((candidate) => GLOBAL_CAPABLE.includes(candidate));
|
|
221
|
+
if (!usable) return void 0;
|
|
222
|
+
return usable === "npm" ? {
|
|
223
|
+
command: "npm",
|
|
224
|
+
args: [
|
|
225
|
+
"install",
|
|
226
|
+
"-g",
|
|
227
|
+
pkg
|
|
228
|
+
]
|
|
229
|
+
} : {
|
|
230
|
+
command: usable,
|
|
231
|
+
args: [
|
|
232
|
+
"add",
|
|
233
|
+
"-g",
|
|
234
|
+
pkg
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* A shell-ready line that runs `binary` out of the project's own
|
|
240
|
+
* `node_modules/.bin`.
|
|
241
|
+
*
|
|
242
|
+
* Every form here is local-only, which is the point: bare `npx` and `bunx` fall
|
|
243
|
+
* back to downloading a package that isn't installed, so a skipped install step
|
|
244
|
+
* would migrate someone's database with an unpinned tool fetched mid-run instead
|
|
245
|
+
* of failing. `npx --no` refuses with "canceled due to missing packages", and
|
|
246
|
+
* `bun run` with "Script not found".
|
|
247
|
+
*
|
|
248
|
+
* `pnpm exec` and `yarn run` are already local-only — `dlx` is the fetching
|
|
249
|
+
* counterpart in both. Use those deliberately, not these, to run something the
|
|
250
|
+
* project does not depend on.
|
|
251
|
+
*
|
|
252
|
+
* `yarn run` over `yarn exec`, which is documented for Berry but not Classic.
|
|
253
|
+
* Like `bun run`, it prefers a package.json script of the same name, so a script
|
|
254
|
+
* called `prisma` would shadow the binary.
|
|
255
|
+
*/
|
|
256
|
+
const formatExecCommand = (pm, binary, args = []) => {
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
npm: "npx --no",
|
|
260
|
+
pnpm: "pnpm exec",
|
|
261
|
+
yarn: "yarn run",
|
|
262
|
+
bun: "bun run"
|
|
263
|
+
}[pm],
|
|
264
|
+
binary,
|
|
265
|
+
...args
|
|
266
|
+
].join(" ");
|
|
267
|
+
};
|
|
268
|
+
/**
|
|
269
|
+
* Belongs in the description of any agent step whose command came from
|
|
270
|
+
* {@link formatExecCommand}.
|
|
271
|
+
*
|
|
272
|
+
* npm's own refusal reads "npx canceled due to missing packages and no YES
|
|
273
|
+
* option", which hands the reader the exact flag that defeats the protection —
|
|
274
|
+
* and the same payload contains `npx -y` elsewhere, so `-y` looks like the house
|
|
275
|
+
* style. Without this, an agent that hits the guard plausibly retries with `-y`
|
|
276
|
+
* and runs an unpinned migration tool against the user's database.
|
|
277
|
+
*
|
|
278
|
+
* Worded against the outcome rather than a message, because the four managers
|
|
279
|
+
* word it differently ("canceled due to missing packages", "Command not found",
|
|
280
|
+
* "Script not found"), and against "install the dependencies" rather than "the
|
|
281
|
+
* step above", because the `--apply` and connected getting-started payloads
|
|
282
|
+
* carry no install step for it to point at.
|
|
283
|
+
*/
|
|
284
|
+
const MISSING_BINARY_HINT = "If this fails because the tool is not installed, install the project's dependencies — re-running the install step above if this payload has one — then retry this step. Do not add -y, and do not swap the runner for a fetching one (`npx` without `--no`, `pnpm dlx`, `yarn dlx`, `bunx`): those download an unpinned copy of the tool and run it against the user's database.";
|
|
285
|
+
/**
|
|
286
|
+
* Belongs in the description of any agent step whose command came from
|
|
287
|
+
* {@link formatInstallCommand}. The command already names the project's package
|
|
288
|
+
* manager, and an agent normalising it to npm is the bug this module exists to
|
|
289
|
+
* prevent.
|
|
290
|
+
*/
|
|
291
|
+
const DO_NOT_SUBSTITUTE_HINT = "Run this step's `command` exactly as written — it already uses this project's package manager. Do not rewrite it to npm or any other manager.";
|
|
292
|
+
/**
|
|
293
|
+
* A shell-ready install line for agents and "next steps" hints — e.g.
|
|
294
|
+
* `pnpm add @neon/config @neon/env`, or `pnpm install` with no packages.
|
|
295
|
+
* All four managers spell the whole-manifest install `install`.
|
|
296
|
+
*/
|
|
297
|
+
const formatInstallCommand = (pm, packages, options) => `${pm} ${installArgs(pm, packages, options).join(" ")}`;
|
|
298
|
+
/**
|
|
299
|
+
* Run a command inheriting our stdio so the user sees install / link output
|
|
300
|
+
* live and can answer any prompts the child raises. Resolves to whether it
|
|
301
|
+
* exited cleanly; a non-zero exit is reported but never throws — the caller
|
|
302
|
+
* decides whether to treat it as fatal.
|
|
303
|
+
*/
|
|
304
|
+
const runCommand = (cmd, args, cwd, env) => new Promise((resolvePromise) => {
|
|
305
|
+
const child = spawn(cmd, args, {
|
|
306
|
+
cwd,
|
|
307
|
+
stdio: "inherit",
|
|
308
|
+
shell: process.platform === "win32",
|
|
309
|
+
...env ? { env: {
|
|
310
|
+
...process.env,
|
|
311
|
+
...env
|
|
312
|
+
} } : {}
|
|
313
|
+
});
|
|
314
|
+
child.on("error", (err) => {
|
|
315
|
+
log.warning("Could not run `%s %s`: %s", cmd, args.join(" "), err instanceof Error ? err.message : String(err));
|
|
316
|
+
resolvePromise(false);
|
|
317
|
+
});
|
|
318
|
+
child.on("close", (code) => {
|
|
319
|
+
if (code !== 0) log.warning("`%s %s` exited with code %d.", cmd, args.join(" "), code);
|
|
320
|
+
resolvePromise(code === 0);
|
|
321
|
+
});
|
|
122
322
|
});
|
|
323
|
+
//#endregion
|
|
324
|
+
export { DO_NOT_SUBSTITUTE_HINT, MISSING_BINARY_HINT, detectInvokingPackageManager, detectProjectPackageManager, formatExecCommand, formatInstallCommand, globalInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolveInvokingPackageManager, resolvePackageManager, runCommand };
|