neonctl 2.27.1 → 2.29.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 +35 -3
- package/dist/analytics.js +52 -34
- package/dist/api.js +643 -13
- package/dist/auth.js +50 -44
- package/dist/cli.js +8 -1
- package/dist/commands/auth.js +64 -51
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +160 -150
- package/dist/commands/bucket.js +183 -146
- package/dist/commands/checkout.js +51 -51
- package/dist/commands/config.js +228 -82
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +100 -101
- package/dist/commands/databases.js +29 -26
- package/dist/commands/deploy.js +12 -12
- package/dist/commands/dev.js +114 -114
- package/dist/commands/env.js +43 -43
- package/dist/commands/functions.js +101 -104
- package/dist/commands/index.js +27 -25
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +232 -182
- package/dist/commands/neon_auth.js +385 -370
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +103 -101
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +27 -24
- package/dist/commands/schema_diff.js +25 -26
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +5 -5
- package/dist/commands/vpc_endpoints.js +50 -50
- package/dist/config.js +7 -7
- package/dist/config_format.js +5 -5
- package/dist/context.js +37 -14
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +33 -33
- package/dist/dev/functions.js +4 -4
- package/dist/dev/inputs.js +6 -6
- package/dist/dev/runtime.js +25 -25
- package/dist/env.js +14 -14
- package/dist/env_file.js +13 -13
- package/dist/errors.js +68 -5
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +110 -107
- package/dist/log.js +2 -2
- package/dist/parameters.gen.js +14 -14
- package/dist/pkg.js +5 -5
- package/dist/psql/cli.js +4 -2
- package/dist/psql/command/cmd_cond.js +61 -61
- package/dist/psql/command/cmd_connect.js +159 -154
- package/dist/psql/command/cmd_copy.js +107 -97
- package/dist/psql/command/cmd_describe.js +368 -363
- package/dist/psql/command/cmd_format.js +276 -263
- package/dist/psql/command/cmd_io.js +269 -263
- package/dist/psql/command/cmd_lo.js +74 -66
- package/dist/psql/command/cmd_meta.js +148 -148
- package/dist/psql/command/cmd_misc.js +17 -17
- package/dist/psql/command/cmd_pipeline.js +142 -135
- package/dist/psql/command/cmd_restrict.js +25 -25
- package/dist/psql/command/cmd_show.js +183 -168
- package/dist/psql/command/dispatch.js +26 -26
- package/dist/psql/command/shared.js +14 -14
- package/dist/psql/complete/filenames.js +16 -16
- package/dist/psql/complete/index.js +4 -4
- package/dist/psql/complete/matcher.js +33 -32
- package/dist/psql/complete/psqlVars.js +173 -173
- package/dist/psql/complete/queries.js +5 -3
- package/dist/psql/complete/rules.js +900 -863
- package/dist/psql/core/common.js +136 -133
- package/dist/psql/core/help.js +343 -343
- package/dist/psql/core/mainloop.js +160 -153
- package/dist/psql/core/prompt.js +126 -123
- package/dist/psql/core/settings.js +111 -111
- package/dist/psql/core/sqlHelp.js +150 -150
- package/dist/psql/core/startup.js +211 -205
- package/dist/psql/core/syncVars.js +14 -14
- package/dist/psql/core/variables.js +24 -24
- package/dist/psql/describe/formatters.js +302 -289
- package/dist/psql/describe/processNamePattern.js +28 -28
- package/dist/psql/describe/queries.js +656 -651
- package/dist/psql/index.js +436 -411
- package/dist/psql/io/history.js +36 -36
- package/dist/psql/io/input.js +15 -15
- package/dist/psql/io/lineEditor/buffer.js +27 -25
- package/dist/psql/io/lineEditor/complete.js +15 -15
- package/dist/psql/io/lineEditor/filename.js +22 -22
- package/dist/psql/io/lineEditor/index.js +65 -62
- package/dist/psql/io/lineEditor/keymap.js +325 -318
- package/dist/psql/io/lineEditor/vt100.js +60 -60
- package/dist/psql/io/pgpass.js +18 -18
- package/dist/psql/io/pgservice.js +14 -14
- package/dist/psql/io/psqlrc.js +46 -46
- package/dist/psql/print/aligned.js +175 -166
- package/dist/psql/print/asciidoc.js +51 -51
- package/dist/psql/print/crosstab.js +34 -31
- package/dist/psql/print/csv.js +25 -22
- package/dist/psql/print/html.js +54 -54
- package/dist/psql/print/json.js +12 -12
- package/dist/psql/print/latex.js +118 -118
- package/dist/psql/print/pager.js +28 -26
- package/dist/psql/print/troff.js +48 -48
- package/dist/psql/print/unaligned.js +15 -14
- package/dist/psql/print/units.js +17 -17
- package/dist/psql/scanner/slash.js +48 -46
- package/dist/psql/scanner/sql.js +88 -84
- package/dist/psql/scanner/stringutils.js +21 -17
- package/dist/psql/types/index.js +7 -7
- package/dist/psql/types/scanner.js +8 -8
- package/dist/psql/wire/connection.js +341 -327
- package/dist/psql/wire/copy.js +7 -7
- package/dist/psql/wire/pipeline.js +26 -24
- package/dist/psql/wire/protocol.js +102 -102
- package/dist/psql/wire/sasl.js +62 -62
- package/dist/psql/wire/tls.js +79 -73
- package/dist/storage_api.js +22 -23
- package/dist/test_utils/fixtures.js +74 -41
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/branch_notice.js +5 -5
- package/dist/utils/branch_picker.js +26 -26
- package/dist/utils/compute_units.js +4 -4
- package/dist/utils/enrichers.js +28 -16
- package/dist/utils/esbuild.js +28 -28
- package/dist/utils/formats.js +1 -1
- package/dist/utils/middlewares.js +3 -3
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +12 -12
- package/dist/utils/psql.js +30 -30
- package/dist/utils/string.js +2 -2
- package/dist/utils/ui.js +9 -9
- package/dist/utils/zip.js +1 -1
- package/dist/writer.js +17 -17
- package/package.json +10 -12
|
@@ -1,77 +1,76 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import { log } from '../log.js';
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, relative, resolve } from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { BootstrapInputError, ensureTargetUsable, FALLBACK_TEMPLATES, fetchTemplates, findTemplate, scaffoldTemplate, templateIds, } from "neon-init/bootstrap";
|
|
5
|
+
import prompts from "prompts";
|
|
6
|
+
import { isCi } from "../env.js";
|
|
7
|
+
import { log } from "../log.js";
|
|
8
|
+
import { detectPackageManager, installedPackageManagers, runCommand, } from "../utils/package_manager.js";
|
|
10
9
|
// The directory positional is optional: omitting it in an interactive terminal
|
|
11
10
|
// prompts for one. In a non-interactive context a missing directory is an error.
|
|
12
|
-
export const command =
|
|
13
|
-
export const describe =
|
|
11
|
+
export const command = "bootstrap [directory]";
|
|
12
|
+
export const describe = "Scaffold a new project from a Neon starter template";
|
|
14
13
|
export const builder = (argv) => argv
|
|
15
|
-
.usage(
|
|
16
|
-
.positional(
|
|
14
|
+
.usage("$0 bootstrap [directory] [options]")
|
|
15
|
+
.positional("directory", {
|
|
17
16
|
describe: 'Directory to scaffold into. Use "." for the current directory. Omit to be prompted.',
|
|
18
|
-
type:
|
|
17
|
+
type: "string",
|
|
19
18
|
})
|
|
20
19
|
.options({
|
|
21
20
|
template: {
|
|
22
|
-
describe:
|
|
23
|
-
type:
|
|
21
|
+
describe: "Template to use (skips the interactive picker). Run with --list-templates to see available templates.",
|
|
22
|
+
type: "string",
|
|
24
23
|
},
|
|
25
|
-
|
|
26
|
-
alias: [
|
|
27
|
-
describe:
|
|
28
|
-
type:
|
|
24
|
+
"list-templates": {
|
|
25
|
+
alias: ["list", "ls"],
|
|
26
|
+
describe: "List available templates and exit.",
|
|
27
|
+
type: "boolean",
|
|
29
28
|
default: false,
|
|
30
29
|
},
|
|
31
30
|
force: {
|
|
32
|
-
describe:
|
|
33
|
-
type:
|
|
31
|
+
describe: "Scaffold into the target directory even if it is not empty (colliding files are overwritten).",
|
|
32
|
+
type: "boolean",
|
|
34
33
|
default: false,
|
|
35
34
|
},
|
|
36
35
|
agent: {
|
|
37
|
-
describe:
|
|
38
|
-
type:
|
|
36
|
+
describe: "Emit a JSON state-machine response designed for AI agents instead of prompting. The output is a single JSON object with a discriminated `status` field describing the next step.",
|
|
37
|
+
type: "boolean",
|
|
39
38
|
default: false,
|
|
40
39
|
},
|
|
41
40
|
default: {
|
|
42
|
-
alias:
|
|
43
|
-
describe:
|
|
44
|
-
type:
|
|
41
|
+
alias: "y",
|
|
42
|
+
describe: "Quick start: scaffold the default template (or --template) and run the usual setup (install dependencies, git init) without prompting. Linking is left to you since it needs a project choice.",
|
|
43
|
+
type: "boolean",
|
|
45
44
|
default: false,
|
|
46
45
|
},
|
|
47
46
|
install: {
|
|
48
|
-
describe:
|
|
49
|
-
type:
|
|
47
|
+
describe: "Install dependencies after scaffolding. In interactive mode this is offered as a prompt; use --no-install to skip without being asked.",
|
|
48
|
+
type: "boolean",
|
|
50
49
|
default: true,
|
|
51
50
|
},
|
|
52
51
|
git: {
|
|
53
|
-
describe:
|
|
54
|
-
type:
|
|
52
|
+
describe: "Initialize a git repository after scaffolding. In interactive mode this is offered as a prompt; use --no-git to skip without being asked.",
|
|
53
|
+
type: "boolean",
|
|
55
54
|
default: true,
|
|
56
55
|
},
|
|
57
56
|
link: {
|
|
58
|
-
describe:
|
|
59
|
-
type:
|
|
57
|
+
describe: "Run `neon link` in the scaffolded directory after installing. In interactive mode this is offered as a prompt; use --no-link to skip without being asked.",
|
|
58
|
+
type: "boolean",
|
|
60
59
|
default: true,
|
|
61
60
|
},
|
|
62
61
|
})
|
|
63
|
-
.example(
|
|
64
|
-
.example(
|
|
65
|
-
.example(
|
|
66
|
-
.example(
|
|
62
|
+
.example("$0 bootstrap my-app", "Create ./my-app from an interactively chosen template")
|
|
63
|
+
.example("$0 bootstrap . --template hono", "Scaffold the Hono template into the current directory")
|
|
64
|
+
.example("$0 bootstrap my-app --default", "Quick start: scaffold the default template and run setup without prompting")
|
|
65
|
+
.example("$0 bootstrap my-app --template hono --agent", "Scaffold without prompting and emit the JSON state machine for AI agents")
|
|
67
66
|
.strict();
|
|
68
67
|
export const handler = async (props) => {
|
|
69
68
|
if (props.listTemplates) {
|
|
70
69
|
const templates = await fetchTemplates();
|
|
71
70
|
for (const t of templates) {
|
|
72
71
|
const services = t.services && t.services.length > 0
|
|
73
|
-
? ` [${t.services.join(
|
|
74
|
-
:
|
|
72
|
+
? ` [${t.services.join(" · ")}]`
|
|
73
|
+
: "";
|
|
75
74
|
process.stdout.write(`${t.id} — ${t.description}${services}\n`);
|
|
76
75
|
}
|
|
77
76
|
return;
|
|
@@ -112,7 +111,7 @@ const formatTemplateTitle = (template) => {
|
|
|
112
111
|
if (!template.services || template.services.length === 0) {
|
|
113
112
|
return template.title;
|
|
114
113
|
}
|
|
115
|
-
return `${template.title} ${chalk.dim.italic(template.services.join(
|
|
114
|
+
return `${template.title} ${chalk.dim.italic(template.services.join(" · "))}`;
|
|
116
115
|
};
|
|
117
116
|
const resolveSelectedTemplate = async (props, interactive, templates) => {
|
|
118
117
|
if (props.template) {
|
|
@@ -127,7 +126,7 @@ const resolveSelectedTemplate = async (props, interactive, templates) => {
|
|
|
127
126
|
if (props.default) {
|
|
128
127
|
const fallback = templates[0];
|
|
129
128
|
if (!fallback) {
|
|
130
|
-
throw new Error(
|
|
129
|
+
throw new Error("No templates available to scaffold from.");
|
|
131
130
|
}
|
|
132
131
|
return fallback;
|
|
133
132
|
}
|
|
@@ -136,9 +135,9 @@ const resolveSelectedTemplate = async (props, interactive, templates) => {
|
|
|
136
135
|
}
|
|
137
136
|
const { id } = await prompts({
|
|
138
137
|
onState: onPromptState,
|
|
139
|
-
type:
|
|
140
|
-
name:
|
|
141
|
-
message:
|
|
138
|
+
type: "select",
|
|
139
|
+
name: "id",
|
|
140
|
+
message: "Which template would you like to use?",
|
|
142
141
|
choices: templates.map((template) => ({
|
|
143
142
|
title: formatTemplateTitle(template),
|
|
144
143
|
value: template.id,
|
|
@@ -147,7 +146,7 @@ const resolveSelectedTemplate = async (props, interactive, templates) => {
|
|
|
147
146
|
});
|
|
148
147
|
const template = findTemplate(templates, id);
|
|
149
148
|
if (!template) {
|
|
150
|
-
throw new Error(
|
|
149
|
+
throw new Error("No template selected.");
|
|
151
150
|
}
|
|
152
151
|
return template;
|
|
153
152
|
};
|
|
@@ -164,9 +163,9 @@ const resolveTargetDir = async (props, interactive, template) => {
|
|
|
164
163
|
}
|
|
165
164
|
const { value } = await prompts({
|
|
166
165
|
onState: onPromptState,
|
|
167
|
-
type:
|
|
168
|
-
name:
|
|
169
|
-
message:
|
|
166
|
+
type: "text",
|
|
167
|
+
name: "value",
|
|
168
|
+
message: "Where should we scaffold your project?",
|
|
170
169
|
initial: defaultDirName(template),
|
|
171
170
|
validate: (input) => input && input.trim().length > 0
|
|
172
171
|
? true
|
|
@@ -174,9 +173,9 @@ const resolveTargetDir = async (props, interactive, template) => {
|
|
|
174
173
|
});
|
|
175
174
|
dir = String(value).trim();
|
|
176
175
|
}
|
|
177
|
-
return resolve(process.cwd(), dir ===
|
|
176
|
+
return resolve(process.cwd(), dir === "." ? "" : dir);
|
|
178
177
|
};
|
|
179
|
-
const defaultDirName = (template) => template.source.subdir.split(
|
|
178
|
+
const defaultDirName = (template) => template.source.subdir.split("/").pop() || template.id;
|
|
180
179
|
/**
|
|
181
180
|
* Download and materialize the template into `targetDir`. The actual
|
|
182
181
|
* download/extract/write lives in the shared `neon-init/bootstrap` core
|
|
@@ -190,7 +189,7 @@ const scaffold = async (template, targetDir) => {
|
|
|
190
189
|
log.warning(message);
|
|
191
190
|
},
|
|
192
191
|
});
|
|
193
|
-
log.info(
|
|
192
|
+
log.info("Scaffolded %d files into %s.", filesWritten, targetDir);
|
|
194
193
|
return filesWritten;
|
|
195
194
|
};
|
|
196
195
|
// ----------------------------------------------------------------------------
|
|
@@ -208,11 +207,11 @@ const scaffold = async (template, targetDir) => {
|
|
|
208
207
|
const runPostScaffoldSteps = async (props, targetDir, interactive) => {
|
|
209
208
|
const detected = detectPackageManager();
|
|
210
209
|
if (props.default) {
|
|
211
|
-
await runDefaultSteps(props, targetDir, detected ??
|
|
210
|
+
await runDefaultSteps(props, targetDir, detected ?? "npm");
|
|
212
211
|
return;
|
|
213
212
|
}
|
|
214
213
|
if (!interactive) {
|
|
215
|
-
printNextSteps(targetDir, detected ??
|
|
214
|
+
printNextSteps(targetDir, detected ?? "npm", {
|
|
216
215
|
installed: false,
|
|
217
216
|
suggestLink: true,
|
|
218
217
|
});
|
|
@@ -221,15 +220,15 @@ const runPostScaffoldSteps = async (props, targetDir, interactive) => {
|
|
|
221
220
|
// The package manager used for the install (and shown in the closing hint).
|
|
222
221
|
// When we couldn't infer it from the invocation we ask, so a globally
|
|
223
222
|
// installed `neon` doesn't silently force npm on a bun/pnpm user.
|
|
224
|
-
let pm = detected ??
|
|
223
|
+
let pm = detected ?? "npm";
|
|
225
224
|
let installed = false;
|
|
226
225
|
if (props.install && (await confirm(installPrompt(detected)))) {
|
|
227
226
|
pm = detected ?? (await selectPackageManager());
|
|
228
|
-
installed = await runCommand(pm, [
|
|
227
|
+
installed = await runCommand(pm, ["install"], targetDir);
|
|
229
228
|
}
|
|
230
229
|
if (props.git &&
|
|
231
230
|
!isGitRepo(targetDir) &&
|
|
232
|
-
(await confirm(
|
|
231
|
+
(await confirm("Initialize a git repository?"))) {
|
|
233
232
|
await initGitRepo(targetDir);
|
|
234
233
|
}
|
|
235
234
|
// `neon link` pulls env vars, which loads this project's neon.ts — and that
|
|
@@ -240,9 +239,9 @@ const runPostScaffoldSteps = async (props, targetDir, interactive) => {
|
|
|
240
239
|
if (!installed && hasNeonConfig(targetDir)) {
|
|
241
240
|
log.info("Skipping the Neon link step: `neon link` reads this project's neon.ts " +
|
|
242
241
|
`to pull env vars, which needs its dependencies. Run \`${pm} install\`, ` +
|
|
243
|
-
|
|
242
|
+
"then `neon link`.");
|
|
244
243
|
}
|
|
245
|
-
else if (await confirm(
|
|
244
|
+
else if (await confirm("Link this project to a Neon project now? (runs neon link)")) {
|
|
246
245
|
await runNeonLink(props, targetDir);
|
|
247
246
|
// link prints its own summary (and pulls env), so end with just the run hint.
|
|
248
247
|
printNextSteps(targetDir, pm, { installed, suggestLink: false });
|
|
@@ -251,7 +250,9 @@ const runPostScaffoldSteps = async (props, targetDir, interactive) => {
|
|
|
251
250
|
}
|
|
252
251
|
printNextSteps(targetDir, pm, { installed, suggestLink: true });
|
|
253
252
|
};
|
|
254
|
-
const installPrompt = (detected) => detected
|
|
253
|
+
const installPrompt = (detected) => detected
|
|
254
|
+
? `Install dependencies with ${detected}?`
|
|
255
|
+
: "Install dependencies?";
|
|
255
256
|
/**
|
|
256
257
|
* `--default` quick start: run install + git init without prompting, honoring
|
|
257
258
|
* --no-install / --no-git. Linking is intentionally skipped — it needs an
|
|
@@ -259,20 +260,20 @@ const installPrompt = (detected) => detected ? `Install dependencies with ${dete
|
|
|
259
260
|
* closing hint instead.
|
|
260
261
|
*/
|
|
261
262
|
const runDefaultSteps = async (props, targetDir, pm) => {
|
|
262
|
-
log.info(
|
|
263
|
+
log.info("Quick start (--default): running setup without prompting.");
|
|
263
264
|
let installed = false;
|
|
264
265
|
if (props.install) {
|
|
265
|
-
installed = await runCommand(pm, [
|
|
266
|
+
installed = await runCommand(pm, ["install"], targetDir);
|
|
266
267
|
}
|
|
267
268
|
if (props.git && !isGitRepo(targetDir)) {
|
|
268
269
|
await initGitRepo(targetDir);
|
|
269
270
|
}
|
|
270
271
|
printNextSteps(targetDir, pm, { installed, suggestLink: true });
|
|
271
272
|
};
|
|
272
|
-
const isGitRepo = (dir) => existsSync(join(dir,
|
|
273
|
-
// Config filenames the runtime loads (mirrors @
|
|
273
|
+
const isGitRepo = (dir) => existsSync(join(dir, ".git"));
|
|
274
|
+
// Config filenames the runtime loads (mirrors @neon/config). A scaffold
|
|
274
275
|
// that ships one makes `neon link`'s env pull evaluate it — which needs deps.
|
|
275
|
-
const NEON_CONFIG_FILENAMES = [
|
|
276
|
+
const NEON_CONFIG_FILENAMES = ["neon.ts", "neon.mts", "neon.js", "neon.mjs"];
|
|
276
277
|
const hasNeonConfig = (dir) => NEON_CONFIG_FILENAMES.some((name) => existsSync(join(dir, name)));
|
|
277
278
|
/**
|
|
278
279
|
* Initialize a git repository in the scaffolded directory. Just `git init` — we
|
|
@@ -280,43 +281,18 @@ const hasNeonConfig = (dir) => NEON_CONFIG_FILENAMES.some((name) => existsSync(j
|
|
|
280
281
|
* git identity configured and to leave the first commit to the user.
|
|
281
282
|
*/
|
|
282
283
|
const initGitRepo = async (dir) => {
|
|
283
|
-
await runCommand(
|
|
284
|
+
await runCommand("git", ["init"], dir);
|
|
284
285
|
};
|
|
285
286
|
const confirm = async (message) => {
|
|
286
287
|
const { value } = await prompts({
|
|
287
288
|
onState: onPromptState,
|
|
288
|
-
type:
|
|
289
|
-
name:
|
|
289
|
+
type: "confirm",
|
|
290
|
+
name: "value",
|
|
290
291
|
message,
|
|
291
292
|
initial: true,
|
|
292
293
|
});
|
|
293
294
|
return value === true;
|
|
294
295
|
};
|
|
295
|
-
// npm first so it's the default/preselected choice; the rest follow in rough
|
|
296
|
-
// popularity order.
|
|
297
|
-
const PACKAGE_MANAGERS = ['npm', 'pnpm', 'yarn', 'bun'];
|
|
298
|
-
/**
|
|
299
|
-
* The package manager the CLI was invoked through, read from the
|
|
300
|
-
* `npm_config_user_agent` npm sets for `npm exec`/`npx`, `pnpm dlx`, `yarn
|
|
301
|
-
* dlx`, and `bunx` (so `pnpm dlx neonctl bootstrap` installs with pnpm).
|
|
302
|
-
* Returns undefined when there's nothing to infer from — e.g. a
|
|
303
|
-
* globally-installed `neon`/`neonctl` — so the caller can ask instead of
|
|
304
|
-
* silently assuming npm.
|
|
305
|
-
*/
|
|
306
|
-
const detectPackageManager = () => {
|
|
307
|
-
const ua = process.env.npm_config_user_agent ?? '';
|
|
308
|
-
if (ua.startsWith('pnpm'))
|
|
309
|
-
return 'pnpm';
|
|
310
|
-
if (ua.startsWith('yarn'))
|
|
311
|
-
return 'yarn';
|
|
312
|
-
if (ua.startsWith('bun'))
|
|
313
|
-
return 'bun';
|
|
314
|
-
if (ua.startsWith('npm'))
|
|
315
|
-
return 'npm';
|
|
316
|
-
return undefined;
|
|
317
|
-
};
|
|
318
|
-
/** The package managers actually on PATH, in {@link PACKAGE_MANAGERS} order. */
|
|
319
|
-
const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.sync(pm, { nothrow: true }) !== null);
|
|
320
296
|
/**
|
|
321
297
|
* Ask which package manager to install with when we couldn't infer one from the
|
|
322
298
|
* invocation. Offers the managers actually installed (npm preselected); with
|
|
@@ -326,45 +302,21 @@ const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.syn
|
|
|
326
302
|
const selectPackageManager = async () => {
|
|
327
303
|
const installed = installedPackageManagers();
|
|
328
304
|
if (installed.length <= 1) {
|
|
329
|
-
return installed[0] ??
|
|
305
|
+
return installed[0] ?? "npm";
|
|
330
306
|
}
|
|
331
307
|
const { pm } = await prompts({
|
|
332
308
|
onState: onPromptState,
|
|
333
|
-
type:
|
|
334
|
-
name:
|
|
335
|
-
message:
|
|
309
|
+
type: "select",
|
|
310
|
+
name: "pm",
|
|
311
|
+
message: "Which package manager should we use?",
|
|
336
312
|
choices: installed.map((manager) => ({
|
|
337
313
|
title: manager,
|
|
338
314
|
value: manager,
|
|
339
315
|
})),
|
|
340
|
-
initial: Math.max(0, installed.indexOf(
|
|
316
|
+
initial: Math.max(0, installed.indexOf("npm")),
|
|
341
317
|
});
|
|
342
|
-
return pm ??
|
|
318
|
+
return pm ?? "npm";
|
|
343
319
|
};
|
|
344
|
-
/**
|
|
345
|
-
* Run a command inheriting our stdio so the user sees install / link output
|
|
346
|
-
* live and can answer any prompts the child raises. Resolves to whether it
|
|
347
|
-
* exited cleanly; a non-zero exit is reported but never aborts bootstrap — the
|
|
348
|
-
* scaffold already succeeded, so we let the user retry the step by hand.
|
|
349
|
-
*/
|
|
350
|
-
const runCommand = (cmd, args, cwd) => new Promise((resolvePromise) => {
|
|
351
|
-
// npm/pnpm/yarn ship as .cmd shims on Windows, which need a shell to run.
|
|
352
|
-
const child = spawn(cmd, args, {
|
|
353
|
-
cwd,
|
|
354
|
-
stdio: 'inherit',
|
|
355
|
-
shell: process.platform === 'win32',
|
|
356
|
-
});
|
|
357
|
-
child.on('error', (err) => {
|
|
358
|
-
log.warning('Could not run `%s %s`: %s', cmd, args.join(' '), err instanceof Error ? err.message : String(err));
|
|
359
|
-
resolvePromise(false);
|
|
360
|
-
});
|
|
361
|
-
child.on('close', (code) => {
|
|
362
|
-
if (code !== 0) {
|
|
363
|
-
log.warning('`%s %s` exited with code %d.', cmd, args.join(' '), code);
|
|
364
|
-
}
|
|
365
|
-
resolvePromise(code === 0);
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
320
|
/**
|
|
369
321
|
* Re-invoke this same CLI as `neon link` inside the scaffolded directory, so the
|
|
370
322
|
* new project's `.neon` context (and pulled `.env`) land in the right place and
|
|
@@ -373,16 +325,18 @@ const runCommand = (cmd, args, cwd) => new Promise((resolvePromise) => {
|
|
|
373
325
|
* to the target dir, which is where its env pull writes.
|
|
374
326
|
*/
|
|
375
327
|
const runNeonLink = async (props, targetDir) => {
|
|
376
|
-
const args = [process.argv[1],
|
|
328
|
+
const args = [process.argv[1], "link"];
|
|
377
329
|
if (props.apiKey) {
|
|
378
|
-
args.push(
|
|
330
|
+
args.push("--api-key", props.apiKey);
|
|
379
331
|
}
|
|
380
|
-
args.push(
|
|
332
|
+
args.push("--api-host", props.apiHost, "--output", props.output);
|
|
381
333
|
await runCommand(process.execPath, args, targetDir);
|
|
382
334
|
};
|
|
383
335
|
const printScaffolded = (template, targetDir) => {
|
|
384
|
-
log.info(
|
|
385
|
-
log.info('Done. Scaffolded "%s" into %s.', template.title, isCurrentDir(targetDir)
|
|
336
|
+
log.info("");
|
|
337
|
+
log.info('Done. Scaffolded "%s" into %s.', template.title, isCurrentDir(targetDir)
|
|
338
|
+
? "the current directory"
|
|
339
|
+
: displayDir(targetDir));
|
|
386
340
|
};
|
|
387
341
|
/**
|
|
388
342
|
* The closing "Next steps" hint. Skips `cd` for the current directory, omits
|
|
@@ -390,19 +344,19 @@ const printScaffolded = (template, targetDir) => {
|
|
|
390
344
|
* wasn't already offered/run — so the user never sees a step they just did.
|
|
391
345
|
*/
|
|
392
346
|
const printNextSteps = (targetDir, pm, opts) => {
|
|
393
|
-
log.info(
|
|
394
|
-
log.info(
|
|
347
|
+
log.info("");
|
|
348
|
+
log.info("Next steps:");
|
|
395
349
|
if (!isCurrentDir(targetDir)) {
|
|
396
|
-
log.info(
|
|
350
|
+
log.info(" cd %s", displayDir(targetDir));
|
|
397
351
|
}
|
|
398
352
|
if (!opts.installed) {
|
|
399
|
-
log.info(
|
|
353
|
+
log.info(" %s install", pm);
|
|
400
354
|
}
|
|
401
355
|
if (opts.suggestLink) {
|
|
402
|
-
log.info(
|
|
356
|
+
log.info(" neon link");
|
|
403
357
|
}
|
|
404
|
-
log.info(
|
|
405
|
-
log.info(
|
|
358
|
+
log.info(" See the README to run it.");
|
|
359
|
+
log.info("");
|
|
406
360
|
};
|
|
407
361
|
const runAgentSafely = async (props) => {
|
|
408
362
|
try {
|
|
@@ -425,55 +379,55 @@ const runAgent = async (props) => {
|
|
|
425
379
|
if (!props.template) {
|
|
426
380
|
const templates = await fetchTemplates();
|
|
427
381
|
emitAgent({
|
|
428
|
-
status:
|
|
429
|
-
instruction: `Ask the user which template to scaffold, then re-run the next_command_template with the chosen --template value${props.directory ?
|
|
382
|
+
status: "needs_template",
|
|
383
|
+
instruction: `Ask the user which template to scaffold, then re-run the next_command_template with the chosen --template value${props.directory ? "" : " and a target directory"}.`,
|
|
430
384
|
options: templates.map((template) => ({
|
|
431
385
|
id: template.id,
|
|
432
386
|
title: template.title,
|
|
433
387
|
description: template.description,
|
|
434
388
|
...(template.services ? { services: template.services } : {}),
|
|
435
389
|
})),
|
|
436
|
-
next_command_template: `neon bootstrap --agent ${props.directory ? shellArg(props.directory) :
|
|
390
|
+
next_command_template: `neon bootstrap --agent ${props.directory ? shellArg(props.directory) : "<directory>"} --template <template_id>`,
|
|
437
391
|
});
|
|
438
392
|
return;
|
|
439
393
|
}
|
|
440
394
|
const templates = await resolveTemplateList(props);
|
|
441
395
|
const template = findTemplate(templates, props.template);
|
|
442
396
|
if (!template) {
|
|
443
|
-
throw new BootstrapInputError(`Unknown template "${props.template}". Available templates: ${templateIds(templates)}.`,
|
|
397
|
+
throw new BootstrapInputError(`Unknown template "${props.template}". Available templates: ${templateIds(templates)}.`, "UNKNOWN_TEMPLATE");
|
|
444
398
|
}
|
|
445
399
|
if (props.directory === undefined) {
|
|
446
400
|
emitAgent({
|
|
447
|
-
status:
|
|
401
|
+
status: "needs_directory",
|
|
448
402
|
instruction: 'Ask the user which directory to scaffold into (use "." for the current directory), then re-run the next_command_template with it.',
|
|
449
403
|
next_command_template: `neon bootstrap --agent <directory> --template ${shellArg(template.id)}`,
|
|
450
404
|
});
|
|
451
405
|
return;
|
|
452
406
|
}
|
|
453
|
-
const targetDir = resolve(process.cwd(), props.directory ===
|
|
407
|
+
const targetDir = resolve(process.cwd(), props.directory === "." ? "" : props.directory);
|
|
454
408
|
ensureTargetUsable(targetDir, props.force);
|
|
455
409
|
const filesWritten = await scaffold(template, targetDir);
|
|
456
410
|
const dir = displayDir(targetDir);
|
|
457
|
-
const runIn = isCurrentDir(targetDir) ?
|
|
411
|
+
const runIn = isCurrentDir(targetDir) ? "" : `cd ${shellArg(dir)} && `;
|
|
458
412
|
emitAgent({
|
|
459
|
-
status:
|
|
413
|
+
status: "scaffolded",
|
|
460
414
|
directory: targetDir,
|
|
461
415
|
template: { id: template.id, title: template.title },
|
|
462
416
|
files_written: filesWritten,
|
|
463
417
|
next_steps: [
|
|
464
418
|
{
|
|
465
|
-
action:
|
|
466
|
-
instruction:
|
|
419
|
+
action: "install_dependencies",
|
|
420
|
+
instruction: "Ask the user whether to install dependencies, then run this in the project directory.",
|
|
467
421
|
command: `${runIn}npm install`,
|
|
468
422
|
},
|
|
469
423
|
{
|
|
470
|
-
action:
|
|
471
|
-
instruction:
|
|
424
|
+
action: "initialize_git",
|
|
425
|
+
instruction: "Ask the user whether to initialize a git repository in the project directory.",
|
|
472
426
|
command: `${runIn}git init`,
|
|
473
427
|
},
|
|
474
428
|
{
|
|
475
|
-
action:
|
|
476
|
-
instruction:
|
|
429
|
+
action: "link_neon_project",
|
|
430
|
+
instruction: "Ask the user whether to link the project to a Neon project now. This runs the link state machine — follow its JSON output for the next step.",
|
|
477
431
|
command: `${runIn}neon link --agent`,
|
|
478
432
|
},
|
|
479
433
|
],
|
|
@@ -485,17 +439,21 @@ const emitAgent = (response) => {
|
|
|
485
439
|
};
|
|
486
440
|
const toAgentError = (err) => {
|
|
487
441
|
if (err instanceof BootstrapInputError) {
|
|
488
|
-
return { status:
|
|
442
|
+
return { status: "error", code: err.agentCode, message: err.message };
|
|
489
443
|
}
|
|
490
444
|
if (err instanceof Error) {
|
|
491
|
-
return {
|
|
445
|
+
return {
|
|
446
|
+
status: "error",
|
|
447
|
+
code: "INTERNAL_ERROR",
|
|
448
|
+
message: err.message,
|
|
449
|
+
};
|
|
492
450
|
}
|
|
493
|
-
return { status:
|
|
451
|
+
return { status: "error", code: "INTERNAL_ERROR", message: String(err) };
|
|
494
452
|
};
|
|
495
453
|
// ----------------------------------------------------------------------------
|
|
496
454
|
// Path display helpers
|
|
497
455
|
// ----------------------------------------------------------------------------
|
|
498
|
-
const isCurrentDir = (targetDir) => relative(process.cwd(), targetDir) ===
|
|
456
|
+
const isCurrentDir = (targetDir) => relative(process.cwd(), targetDir) === "";
|
|
499
457
|
/**
|
|
500
458
|
* The path to show the user: the bare relative path for the common
|
|
501
459
|
* `bootstrap my-app` case, the absolute path when the target sits outside the
|
|
@@ -503,10 +461,10 @@ const isCurrentDir = (targetDir) => relative(process.cwd(), targetDir) === '';
|
|
|
503
461
|
*/
|
|
504
462
|
const displayDir = (targetDir) => {
|
|
505
463
|
const rel = relative(process.cwd(), targetDir);
|
|
506
|
-
if (rel ===
|
|
507
|
-
return
|
|
464
|
+
if (rel === "") {
|
|
465
|
+
return ".";
|
|
508
466
|
}
|
|
509
|
-
return rel.startsWith(
|
|
467
|
+
return rel.startsWith("..") ? targetDir : rel;
|
|
510
468
|
};
|
|
511
469
|
const shellArg = (value) => {
|
|
512
470
|
if (/^[A-Za-z0-9._:/-]+$/.test(value)) {
|
|
@@ -516,8 +474,8 @@ const shellArg = (value) => {
|
|
|
516
474
|
};
|
|
517
475
|
const onPromptState = (state) => {
|
|
518
476
|
if (state.aborted) {
|
|
519
|
-
process.stdout.write(
|
|
520
|
-
process.stdout.write(
|
|
477
|
+
process.stdout.write("\x1B[?25h");
|
|
478
|
+
process.stdout.write("\n");
|
|
521
479
|
process.exit(1);
|
|
522
480
|
}
|
|
523
481
|
};
|