neonctl 2.28.0 → 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 +2 -2
- package/dist/analytics.js +35 -33
- package/dist/api.js +34 -34
- package/dist/auth.js +50 -44
- package/dist/cli.js +2 -2
- package/dist/commands/auth.js +58 -52
- package/dist/commands/bootstrap.js +115 -157
- package/dist/commands/branches.js +154 -147
- package/dist/commands/bucket.js +124 -118
- package/dist/commands/checkout.js +49 -49
- package/dist/commands/config.js +212 -88
- package/dist/commands/connection_string.js +62 -62
- package/dist/commands/data_api.js +96 -96
- package/dist/commands/databases.js +23 -23
- 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 +97 -98
- package/dist/commands/index.js +26 -26
- package/dist/commands/init.js +23 -22
- package/dist/commands/ip_allow.js +29 -29
- package/dist/commands/link.js +223 -166
- package/dist/commands/neon_auth.js +381 -363
- package/dist/commands/operations.js +11 -11
- package/dist/commands/orgs.js +8 -8
- package/dist/commands/projects.js +101 -99
- package/dist/commands/psql.js +31 -31
- package/dist/commands/roles.js +21 -21
- package/dist/commands/schema_diff.js +23 -23
- package/dist/commands/set_context.js +17 -17
- package/dist/commands/status.js +17 -17
- 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 +23 -16
- package/dist/current_branch_fast_path.js +6 -6
- 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 +19 -19
- package/dist/functions_api.js +10 -10
- package/dist/help.js +15 -15
- package/dist/index.js +94 -92
- package/dist/log.js +2 -2
- 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 +15 -15
- package/dist/test_utils/fixtures.js +34 -31
- package/dist/test_utils/oauth_server.js +5 -5
- package/dist/utils/api_enums.js +13 -13
- 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 +20 -15
- 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 +6 -7
package/dist/commands/link.js
CHANGED
|
@@ -1,95 +1,99 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile, } from
|
|
4
|
-
import { isCi } from
|
|
5
|
-
import { log } from
|
|
6
|
-
import { createBranch, pickBranchInteractively, } from
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
3
|
+
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile, } from "../context.js";
|
|
4
|
+
import { isCi } from "../env.js";
|
|
5
|
+
import { log } from "../log.js";
|
|
6
|
+
import { createBranch, pickBranchInteractively, } from "../utils/branch_picker.js";
|
|
7
|
+
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
8
|
+
import { autoPullEnvAfterPin, renderAgentPullNote } from "./env.js";
|
|
9
|
+
import { REGIONS } from "./projects.js";
|
|
9
10
|
const PROJECTS_LIST_LIMIT = 100;
|
|
10
|
-
const CREATE_NEW_SENTINEL =
|
|
11
|
-
export const command =
|
|
12
|
-
export const describe =
|
|
11
|
+
const CREATE_NEW_SENTINEL = "__create_new__";
|
|
12
|
+
export const command = "link";
|
|
13
|
+
export const describe = "Link the current directory to a Neon project";
|
|
13
14
|
export const builder = (argv) => argv
|
|
14
|
-
.usage(
|
|
15
|
+
.usage("$0 link [options]")
|
|
15
16
|
.options({
|
|
16
|
-
|
|
17
|
-
describe:
|
|
18
|
-
type:
|
|
17
|
+
"org-id": {
|
|
18
|
+
describe: "Organization ID to link to",
|
|
19
|
+
type: "string",
|
|
19
20
|
},
|
|
20
|
-
|
|
21
|
-
describe:
|
|
22
|
-
type:
|
|
21
|
+
"project-id": {
|
|
22
|
+
describe: "Existing project ID to link to",
|
|
23
|
+
type: "string",
|
|
23
24
|
},
|
|
24
|
-
|
|
25
|
-
describe:
|
|
26
|
-
type:
|
|
25
|
+
"project-name": {
|
|
26
|
+
describe: "Name for a new project to create and link to",
|
|
27
|
+
type: "string",
|
|
27
28
|
},
|
|
28
|
-
|
|
29
|
-
describe:
|
|
30
|
-
type:
|
|
29
|
+
"region-id": {
|
|
30
|
+
describe: "Region ID for a new project (e.g. aws-us-east-2). Required with --project-name.",
|
|
31
|
+
type: "string",
|
|
31
32
|
},
|
|
32
33
|
branch: {
|
|
33
|
-
alias:
|
|
34
|
-
describe:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type:
|
|
34
|
+
alias: "branch-id",
|
|
35
|
+
describe: "Branch name or ID to pin in the context (resolved to its ID before writing). " +
|
|
36
|
+
"Without it, link only resolves the org and project — pin a branch with " +
|
|
37
|
+
"`neonctl checkout <branch>` (link never guesses a default).",
|
|
38
|
+
type: "string",
|
|
38
39
|
},
|
|
39
40
|
params: {
|
|
40
41
|
describe: 'JSON object with link parameters, e.g. \'{"orgId":"...","projectId":"..."}\' or \'{"orgId":"...","projectName":"...","regionId":"..."}\'. Flags take precedence over fields in --params.',
|
|
41
|
-
type:
|
|
42
|
+
type: "string",
|
|
42
43
|
},
|
|
43
44
|
agent: {
|
|
44
|
-
describe:
|
|
45
|
-
type:
|
|
45
|
+
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.",
|
|
46
|
+
type: "boolean",
|
|
46
47
|
default: false,
|
|
47
48
|
},
|
|
48
49
|
yes: {
|
|
49
|
-
alias:
|
|
50
|
+
alias: "y",
|
|
50
51
|
describe: 'Skip the "already linked" confirmation in interactive mode and re-link anyway.',
|
|
51
|
-
type:
|
|
52
|
+
type: "boolean",
|
|
52
53
|
default: false,
|
|
53
54
|
},
|
|
54
55
|
clear: {
|
|
55
|
-
describe:
|
|
56
|
-
type:
|
|
56
|
+
describe: "Remove the org/project/branch context (writes an empty context file) instead of linking.",
|
|
57
|
+
type: "boolean",
|
|
57
58
|
default: false,
|
|
58
59
|
},
|
|
59
60
|
checks: {
|
|
60
|
-
describe:
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
type:
|
|
61
|
+
describe: "Verify the org/project/branch exist (and resolve the org from the project) before " +
|
|
62
|
+
"writing. On by default; use --no-checks to write the context offline with no API " +
|
|
63
|
+
"calls — it then requires --org-id and --project-id (--branch optional) and skips " +
|
|
64
|
+
"env pull.",
|
|
65
|
+
type: "boolean",
|
|
65
66
|
default: true,
|
|
66
67
|
},
|
|
67
|
-
|
|
68
|
+
"env-pull": {
|
|
68
69
|
describe: "Pull the linked branch's Neon env vars (DATABASE_URL, …) into a local .env after " +
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
type:
|
|
70
|
+
"linking. On by default; use --no-env-pull to skip (e.g. when injecting env at " +
|
|
71
|
+
"runtime with `neon-env run` / `neon dev`). Only runs when a branch is pinned.",
|
|
72
|
+
type: "boolean",
|
|
72
73
|
default: true,
|
|
73
74
|
},
|
|
74
75
|
})
|
|
75
76
|
.example([
|
|
76
77
|
[
|
|
77
|
-
|
|
78
|
+
"$0 link --project-id polished-snowflake-12345678",
|
|
78
79
|
"Link an existing project (org is inferred); pin a branch later with 'neonctl checkout'",
|
|
79
80
|
],
|
|
80
81
|
[
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
"$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2",
|
|
83
|
+
"Create a new project and link it",
|
|
83
84
|
],
|
|
84
85
|
[
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
"$0 link --branch-id br-…",
|
|
87
|
+
"Pin a branch in the already-linked project",
|
|
87
88
|
],
|
|
88
89
|
[
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
"$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678",
|
|
91
|
+
"Write the context offline (no API calls, no verification)",
|
|
92
|
+
],
|
|
93
|
+
[
|
|
94
|
+
"$0 link --clear",
|
|
95
|
+
"Forget the current org/project/branch context",
|
|
91
96
|
],
|
|
92
|
-
['$0 link --clear', 'Forget the current org/project/branch context'],
|
|
93
97
|
]);
|
|
94
98
|
export const handler = async (props) => {
|
|
95
99
|
if (props.clear) {
|
|
@@ -113,13 +117,13 @@ export const handler = async (props) => {
|
|
|
113
117
|
}
|
|
114
118
|
if (isCi()) {
|
|
115
119
|
log.error([
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
].join(
|
|
120
|
+
"Missing inputs and CI environment detected (no TTY for prompts).",
|
|
121
|
+
"",
|
|
122
|
+
"Use one of:",
|
|
123
|
+
" neonctl link --agent (JSON state machine for agents)",
|
|
124
|
+
" neonctl link --project-id <project> (link to an existing project; org is inferred)",
|
|
125
|
+
" neonctl link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)",
|
|
126
|
+
].join("\n"));
|
|
123
127
|
process.exit(1);
|
|
124
128
|
return;
|
|
125
129
|
}
|
|
@@ -130,7 +134,7 @@ export const handler = async (props) => {
|
|
|
130
134
|
// ----------------------------------------------------------------------------
|
|
131
135
|
const parseInputs = (props) => {
|
|
132
136
|
let fromParams = {};
|
|
133
|
-
if (props.params !== undefined && props.params !==
|
|
137
|
+
if (props.params !== undefined && props.params !== "") {
|
|
134
138
|
let parsed;
|
|
135
139
|
try {
|
|
136
140
|
parsed = JSON.parse(props.params);
|
|
@@ -150,33 +154,33 @@ const parseInputs = (props) => {
|
|
|
150
154
|
};
|
|
151
155
|
};
|
|
152
156
|
const extractParams = (raw) => {
|
|
153
|
-
if (raw === null || typeof raw !==
|
|
154
|
-
throw new Error(
|
|
157
|
+
if (raw === null || typeof raw !== "object") {
|
|
158
|
+
throw new Error("--params must be a JSON object");
|
|
155
159
|
}
|
|
156
160
|
const obj = raw;
|
|
157
161
|
const pickString = (key) => {
|
|
158
162
|
const value = obj[key];
|
|
159
163
|
if (value === undefined || value === null)
|
|
160
164
|
return undefined;
|
|
161
|
-
if (typeof value !==
|
|
165
|
+
if (typeof value !== "string") {
|
|
162
166
|
throw new Error(`--params.${key} must be a string`);
|
|
163
167
|
}
|
|
164
168
|
return value;
|
|
165
169
|
};
|
|
166
170
|
return {
|
|
167
|
-
orgId: pickString(
|
|
168
|
-
projectId: pickString(
|
|
169
|
-
projectName: pickString(
|
|
170
|
-
regionId: pickString(
|
|
171
|
-
branch: pickString(
|
|
171
|
+
orgId: pickString("orgId"),
|
|
172
|
+
projectId: pickString("projectId"),
|
|
173
|
+
projectName: pickString("projectName"),
|
|
174
|
+
regionId: pickString("regionId"),
|
|
175
|
+
branch: pickString("branch") ?? pickString("branchId"),
|
|
172
176
|
};
|
|
173
177
|
};
|
|
174
178
|
const validateInputs = (inputs) => {
|
|
175
179
|
if (inputs.projectId && (inputs.projectName || inputs.regionId)) {
|
|
176
|
-
throw new Error(
|
|
180
|
+
throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
|
|
177
181
|
}
|
|
178
182
|
if (inputs.projectName && inputs.branch) {
|
|
179
|
-
throw new Error(
|
|
183
|
+
throw new Error("Conflicting inputs: --branch pins a branch of an existing project, but --project-name creates a new one. Create the project first, then `neonctl checkout <branch>`.");
|
|
180
184
|
}
|
|
181
185
|
};
|
|
182
186
|
/**
|
|
@@ -224,7 +228,7 @@ const runWithoutChecks = (props) => {
|
|
|
224
228
|
throw new Error("--no-checks can't create a project (that needs API access). Pass --org-id and --project-id for an existing project, or drop --no-checks.");
|
|
225
229
|
}
|
|
226
230
|
if (!inputs.orgId || !inputs.projectId) {
|
|
227
|
-
throw new Error(
|
|
231
|
+
throw new Error("--no-checks writes the context with no API calls, so it needs both --org-id and --project-id (--branch is optional).");
|
|
228
232
|
}
|
|
229
233
|
setContext(props.contextFile, {
|
|
230
234
|
orgId: inputs.orgId,
|
|
@@ -233,7 +237,7 @@ const runWithoutChecks = (props) => {
|
|
|
233
237
|
});
|
|
234
238
|
if (props.agent) {
|
|
235
239
|
emitAgent({
|
|
236
|
-
status:
|
|
240
|
+
status: "linked",
|
|
237
241
|
context_file: props.contextFile,
|
|
238
242
|
context: {
|
|
239
243
|
orgId: inputs.orgId,
|
|
@@ -241,7 +245,7 @@ const runWithoutChecks = (props) => {
|
|
|
241
245
|
branch: inputs.branch,
|
|
242
246
|
},
|
|
243
247
|
project: { id: inputs.projectId },
|
|
244
|
-
message: `Wrote ${props.contextFile} without checks (org ${inputs.orgId}, project ${inputs.projectId}${inputs.branch ? `, branch ${inputs.branch}` :
|
|
248
|
+
message: `Wrote ${props.contextFile} without checks (org ${inputs.orgId}, project ${inputs.projectId}${inputs.branch ? `, branch ${inputs.branch}` : ""}). No verification or env pull was performed.`,
|
|
245
249
|
});
|
|
246
250
|
return;
|
|
247
251
|
}
|
|
@@ -263,7 +267,7 @@ const runWithoutChecks = (props) => {
|
|
|
263
267
|
class LinkInputError extends Error {
|
|
264
268
|
constructor(message, agentCode) {
|
|
265
269
|
super(message);
|
|
266
|
-
this.name =
|
|
270
|
+
this.name = "LinkInputError";
|
|
267
271
|
this.agentCode = agentCode;
|
|
268
272
|
}
|
|
269
273
|
}
|
|
@@ -284,10 +288,10 @@ const fetchProjectOrThrow = async (props, projectId) => {
|
|
|
284
288
|
throw err;
|
|
285
289
|
}
|
|
286
290
|
if (status === 403) {
|
|
287
|
-
throw new LinkInputError(`You don't have access to project '${projectId}'. Check that your API key's account or organization can see it.`,
|
|
291
|
+
throw new LinkInputError(`You don't have access to project '${projectId}'. Check that your API key's account or organization can see it.`, "NO_ACCESS");
|
|
288
292
|
}
|
|
289
293
|
if (status === 404) {
|
|
290
|
-
throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`,
|
|
294
|
+
throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`, "NOT_FOUND");
|
|
291
295
|
}
|
|
292
296
|
throw err;
|
|
293
297
|
}
|
|
@@ -310,7 +314,7 @@ const verifyOrgAccess = async (props, orgId) => {
|
|
|
310
314
|
throw err;
|
|
311
315
|
}
|
|
312
316
|
if (status === 403 || status === 404) {
|
|
313
|
-
throw new LinkInputError(`Organization '${orgId}' not found, or your API key doesn't have access to it. Find your org ID in the Neon Console under Settings.`, status === 403 ?
|
|
317
|
+
throw new LinkInputError(`Organization '${orgId}' not found, or your API key doesn't have access to it. Find your org ID in the Neon Console under Settings.`, status === 403 ? "NO_ACCESS" : "NOT_FOUND");
|
|
314
318
|
}
|
|
315
319
|
throw err;
|
|
316
320
|
}
|
|
@@ -331,10 +335,10 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
|
331
335
|
}
|
|
332
336
|
const available = data.branches.length > 0
|
|
333
337
|
? data.branches
|
|
334
|
-
.map((b) => `${b.id}${b.name ? ` (${b.name})` :
|
|
335
|
-
.join(
|
|
336
|
-
:
|
|
337
|
-
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`neonctl checkout <branch>\`.`,
|
|
338
|
+
.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`)
|
|
339
|
+
.join(", ")
|
|
340
|
+
: "(none)";
|
|
341
|
+
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`neonctl checkout <branch>\`.`, "NOT_FOUND");
|
|
338
342
|
};
|
|
339
343
|
/**
|
|
340
344
|
* The value to persist for a branch: prefer its human-readable **name** (nicer
|
|
@@ -359,7 +363,7 @@ const resolveOrgForProject = async (props, inputs, existing, projectId) => {
|
|
|
359
363
|
const projectOrg = project.org_id ?? undefined;
|
|
360
364
|
if (inputs.orgId) {
|
|
361
365
|
if (projectOrg && projectOrg !== inputs.orgId) {
|
|
362
|
-
throw new LinkInputError(`Project '${projectId}' belongs to organization '${projectOrg}', not '${inputs.orgId}'. Omit --org-id to use the project's own org, or pass the matching ID.`,
|
|
366
|
+
throw new LinkInputError(`Project '${projectId}' belongs to organization '${projectOrg}', not '${inputs.orgId}'. Omit --org-id to use the project's own org, or pass the matching ID.`, "ORG_MISMATCH");
|
|
363
367
|
}
|
|
364
368
|
if (!projectOrg) {
|
|
365
369
|
await verifyOrgAccess(props, inputs.orgId);
|
|
@@ -399,12 +403,12 @@ const resolvePinnedBranch = async (props, inputs, existing, projectId) => {
|
|
|
399
403
|
const runNonInteractive = async (props, inputs, existing) => {
|
|
400
404
|
// Create a new project and link it.
|
|
401
405
|
if (inputs.projectName) {
|
|
402
|
-
const orgId = mustString(inputs.orgId,
|
|
406
|
+
const orgId = mustString(inputs.orgId, "orgId");
|
|
403
407
|
await verifyOrgAccess(props, orgId);
|
|
404
408
|
const created = await createProject(props, {
|
|
405
409
|
orgId,
|
|
406
410
|
name: inputs.projectName,
|
|
407
|
-
regionId: mustString(inputs.regionId,
|
|
411
|
+
regionId: mustString(inputs.regionId, "regionId"),
|
|
408
412
|
});
|
|
409
413
|
applyContext(props.contextFile, {
|
|
410
414
|
orgId,
|
|
@@ -486,16 +490,16 @@ const runInteractive = async (props, inputs) => {
|
|
|
486
490
|
}
|
|
487
491
|
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
488
492
|
let orgId;
|
|
489
|
-
if (orgResolution.kind ===
|
|
493
|
+
if (orgResolution.kind === "resolved") {
|
|
490
494
|
orgId = orgResolution.orgId;
|
|
491
495
|
if (orgResolution.autoDetected) {
|
|
492
496
|
log.info(`Detected organization ${orgId} from your existing projects (organization-scoped API key).`);
|
|
493
497
|
}
|
|
494
498
|
}
|
|
495
499
|
else if (orgResolution.orgKeyLimited) {
|
|
496
|
-
throw new Error(
|
|
497
|
-
|
|
498
|
-
|
|
500
|
+
throw new Error("This API key is organization-scoped, so the CLI cannot list your organizations, " +
|
|
501
|
+
"and no existing project was found in this org to auto-detect the ID. " +
|
|
502
|
+
"Re-run with `--org-id <your_org_id>` (find it in the Neon Console under Settings).");
|
|
499
503
|
}
|
|
500
504
|
else {
|
|
501
505
|
orgId = await promptOrgFromList(orgResolution.orgs);
|
|
@@ -511,7 +515,7 @@ const runInteractive = async (props, inputs) => {
|
|
|
511
515
|
projectId: created.project.id,
|
|
512
516
|
branch: created.branchName,
|
|
513
517
|
});
|
|
514
|
-
await
|
|
518
|
+
await finalizeInteractiveLink(props, {
|
|
515
519
|
contextFile: props.contextFile,
|
|
516
520
|
orgId,
|
|
517
521
|
projectId: created.project.id,
|
|
@@ -525,14 +529,14 @@ const runInteractive = async (props, inputs) => {
|
|
|
525
529
|
// Need to ask: existing project or create a new one?
|
|
526
530
|
const projects = await listAllProjects(props, orgId);
|
|
527
531
|
const action = await promptProjectChoice(projects, inputs.projectName);
|
|
528
|
-
if (action.type ===
|
|
532
|
+
if (action.type === "existing") {
|
|
529
533
|
const branch = await resolveInteractiveBranch(props, action.projectId);
|
|
530
534
|
applyContext(props.contextFile, {
|
|
531
535
|
orgId,
|
|
532
536
|
projectId: action.projectId,
|
|
533
537
|
branch,
|
|
534
538
|
});
|
|
535
|
-
await
|
|
539
|
+
await finalizeInteractiveLink(props, {
|
|
536
540
|
contextFile: props.contextFile,
|
|
537
541
|
orgId,
|
|
538
542
|
projectId: action.projectId,
|
|
@@ -555,7 +559,7 @@ const runInteractive = async (props, inputs) => {
|
|
|
555
559
|
projectId: created.project.id,
|
|
556
560
|
branch: created.branchName,
|
|
557
561
|
});
|
|
558
|
-
await
|
|
562
|
+
await finalizeInteractiveLink(props, {
|
|
559
563
|
contextFile: props.contextFile,
|
|
560
564
|
orgId,
|
|
561
565
|
projectId: created.project.id,
|
|
@@ -572,13 +576,13 @@ const confirmRelinkIfNeeded = async (props) => {
|
|
|
572
576
|
}
|
|
573
577
|
const { proceed } = await prompts({
|
|
574
578
|
onState: onPromptState,
|
|
575
|
-
type:
|
|
576
|
-
name:
|
|
579
|
+
type: "confirm",
|
|
580
|
+
name: "proceed",
|
|
577
581
|
message: `${props.contextFile} is already linked to project ${existing.projectId} (org ${existing.orgId}). Re-link?`,
|
|
578
582
|
initial: true,
|
|
579
583
|
});
|
|
580
584
|
if (!proceed) {
|
|
581
|
-
process.stdout.write(
|
|
585
|
+
process.stdout.write("Aborted. Existing link preserved.\n");
|
|
582
586
|
process.exit(0);
|
|
583
587
|
}
|
|
584
588
|
};
|
|
@@ -595,9 +599,9 @@ const promptOrgFromList = async (orgs) => {
|
|
|
595
599
|
}
|
|
596
600
|
const { orgId } = await prompts({
|
|
597
601
|
onState: onPromptState,
|
|
598
|
-
type:
|
|
599
|
-
name:
|
|
600
|
-
message:
|
|
602
|
+
type: "select",
|
|
603
|
+
name: "orgId",
|
|
604
|
+
message: "Which organization would you like to link?",
|
|
601
605
|
choices: orgs.map((org) => ({
|
|
602
606
|
title: `${org.name} (${org.id})`,
|
|
603
607
|
value: org.id,
|
|
@@ -608,7 +612,7 @@ const promptOrgFromList = async (orgs) => {
|
|
|
608
612
|
};
|
|
609
613
|
const promptProjectChoice = async (projects, suggestedName) => {
|
|
610
614
|
const choices = [
|
|
611
|
-
{ title:
|
|
615
|
+
{ title: "+ Create new project…", value: CREATE_NEW_SENTINEL },
|
|
612
616
|
...projects.map((project) => ({
|
|
613
617
|
title: `${project.name} (${project.id})`,
|
|
614
618
|
value: project.id,
|
|
@@ -618,18 +622,18 @@ const promptProjectChoice = async (projects, suggestedName) => {
|
|
|
618
622
|
// is one; with no projects to show, the create option (index 0) is the only choice.
|
|
619
623
|
const { selection } = await prompts({
|
|
620
624
|
onState: onPromptState,
|
|
621
|
-
type:
|
|
622
|
-
name:
|
|
623
|
-
message:
|
|
625
|
+
type: "select",
|
|
626
|
+
name: "selection",
|
|
627
|
+
message: "Which project would you like to link?",
|
|
624
628
|
choices,
|
|
625
629
|
initial: projects.length > 0 ? 1 : 0,
|
|
626
630
|
});
|
|
627
631
|
if (selection === CREATE_NEW_SENTINEL) {
|
|
628
|
-
return { type:
|
|
632
|
+
return { type: "create", suggestedName };
|
|
629
633
|
}
|
|
630
634
|
const project = projects.find((p) => p.id === selection);
|
|
631
635
|
return {
|
|
632
|
-
type:
|
|
636
|
+
type: "existing",
|
|
633
637
|
projectId: selection,
|
|
634
638
|
name: project?.name,
|
|
635
639
|
regionId: project?.region_id,
|
|
@@ -638,11 +642,13 @@ const promptProjectChoice = async (projects, suggestedName) => {
|
|
|
638
642
|
const promptProjectName = async (suggestedName) => {
|
|
639
643
|
const { name } = await prompts({
|
|
640
644
|
onState: onPromptState,
|
|
641
|
-
type:
|
|
642
|
-
name:
|
|
643
|
-
message:
|
|
645
|
+
type: "text",
|
|
646
|
+
name: "name",
|
|
647
|
+
message: "Name for the new project:",
|
|
644
648
|
initial: suggestedName,
|
|
645
|
-
validate: (value) => value && value.trim().length > 0
|
|
649
|
+
validate: (value) => value && value.trim().length > 0
|
|
650
|
+
? true
|
|
651
|
+
: "Project name is required",
|
|
646
652
|
});
|
|
647
653
|
return String(name).trim();
|
|
648
654
|
};
|
|
@@ -651,9 +657,9 @@ const promptRegion = async (props) => {
|
|
|
651
657
|
const defaultIndex = Math.max(0, regions.findIndex((r) => r.default));
|
|
652
658
|
const { regionId } = await prompts({
|
|
653
659
|
onState: onPromptState,
|
|
654
|
-
type:
|
|
655
|
-
name:
|
|
656
|
-
message:
|
|
660
|
+
type: "select",
|
|
661
|
+
name: "regionId",
|
|
662
|
+
message: "Which region should the new project run in?",
|
|
657
663
|
choices: regions.map((region) => ({
|
|
658
664
|
title: `${region.name} (${region.region_id})`,
|
|
659
665
|
value: region.region_id,
|
|
@@ -689,7 +695,7 @@ const runAgent = async (props, inputs) => {
|
|
|
689
695
|
projectId,
|
|
690
696
|
branch: pinnedBranch,
|
|
691
697
|
});
|
|
692
|
-
const orgSuffix = orgId ? ` (org ${orgId})` :
|
|
698
|
+
const orgSuffix = orgId ? ` (org ${orgId})` : "";
|
|
693
699
|
if (pinnedBranch) {
|
|
694
700
|
const pullNote = renderAgentPullNote(await autoPullEnvAfterPin({
|
|
695
701
|
...props,
|
|
@@ -698,7 +704,7 @@ const runAgent = async (props, inputs) => {
|
|
|
698
704
|
envPull: props.envPull,
|
|
699
705
|
}));
|
|
700
706
|
emitAgent({
|
|
701
|
-
status:
|
|
707
|
+
status: "linked",
|
|
702
708
|
context_file: props.contextFile,
|
|
703
709
|
context: { orgId, projectId, branch: pinnedBranch },
|
|
704
710
|
project: { id: projectId },
|
|
@@ -707,7 +713,7 @@ const runAgent = async (props, inputs) => {
|
|
|
707
713
|
return;
|
|
708
714
|
}
|
|
709
715
|
emitAgent({
|
|
710
|
-
status:
|
|
716
|
+
status: "linked",
|
|
711
717
|
context_file: props.contextFile,
|
|
712
718
|
context: { orgId, projectId },
|
|
713
719
|
project: { id: projectId },
|
|
@@ -716,7 +722,7 @@ const runAgent = async (props, inputs) => {
|
|
|
716
722
|
return;
|
|
717
723
|
}
|
|
718
724
|
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
719
|
-
if (orgResolution.kind ===
|
|
725
|
+
if (orgResolution.kind === "needs_selection") {
|
|
720
726
|
emitAgent(buildNeedsOrgResponse(orgResolution));
|
|
721
727
|
return;
|
|
722
728
|
}
|
|
@@ -724,7 +730,7 @@ const runAgent = async (props, inputs) => {
|
|
|
724
730
|
if (projectName && !regionId) {
|
|
725
731
|
const regions = await fetchRegions(props);
|
|
726
732
|
emitAgent({
|
|
727
|
-
status:
|
|
733
|
+
status: "needs_project_details",
|
|
728
734
|
instruction: `Ask the user which region to create project "${projectName}" in. After they pick one, re-run the next_command_template with the chosen --region-id value.`,
|
|
729
735
|
regions: regions.map((region) => ({
|
|
730
736
|
id: region.region_id,
|
|
@@ -754,7 +760,7 @@ const runAgent = async (props, inputs) => {
|
|
|
754
760
|
envPull: props.envPull,
|
|
755
761
|
}));
|
|
756
762
|
emitAgent({
|
|
757
|
-
status:
|
|
763
|
+
status: "linked",
|
|
758
764
|
context_file: props.contextFile,
|
|
759
765
|
context: {
|
|
760
766
|
orgId,
|
|
@@ -776,9 +782,9 @@ const runAgent = async (props, inputs) => {
|
|
|
776
782
|
const projects = await listAllProjects(props, orgId);
|
|
777
783
|
const branchNote = branch
|
|
778
784
|
? ` A branch was requested (--branch ${branch}) but a branch can only be pinned once a project is chosen — re-run with --project-id first, then \`neonctl checkout ${branch}\`.`
|
|
779
|
-
:
|
|
785
|
+
: "";
|
|
780
786
|
emitAgent({
|
|
781
|
-
status:
|
|
787
|
+
status: "needs_project",
|
|
782
788
|
instruction: (projects.length === 0
|
|
783
789
|
? `Organization ${orgId} has no projects yet. Ask the user for a name for the new project, then re-run the create_option.next_command_template.`
|
|
784
790
|
: `Ask the user whether to link to one of these ${projects.length} existing projects (use next_command_template with --project-id) or create a new project (use create_option.next_command_template).`) +
|
|
@@ -789,7 +795,7 @@ const runAgent = async (props, inputs) => {
|
|
|
789
795
|
region_id: project.region_id,
|
|
790
796
|
})),
|
|
791
797
|
create_option: {
|
|
792
|
-
instruction:
|
|
798
|
+
instruction: "To create a new project, ask the user for a project name. The region can be omitted to receive a follow-up needs_project_details response that lists available regions.",
|
|
793
799
|
next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`,
|
|
794
800
|
},
|
|
795
801
|
next_command_template: `neonctl link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
|
|
@@ -801,12 +807,13 @@ const emitAgent = (response) => {
|
|
|
801
807
|
// ----------------------------------------------------------------------------
|
|
802
808
|
// API helpers
|
|
803
809
|
// ----------------------------------------------------------------------------
|
|
804
|
-
const ORG_KEY_LIMITED_FRAGMENT =
|
|
810
|
+
const ORG_KEY_LIMITED_FRAGMENT = "not allowed for organization API keys";
|
|
805
811
|
const isOrgKeyLimitedError = (err) => {
|
|
806
812
|
if (!isNeonApiError(err))
|
|
807
813
|
return false;
|
|
808
814
|
const message = messageFromBody(err.data);
|
|
809
|
-
return (typeof message ===
|
|
815
|
+
return (typeof message === "string" &&
|
|
816
|
+
message.includes(ORG_KEY_LIMITED_FRAGMENT));
|
|
810
817
|
};
|
|
811
818
|
const fetchOrganizations = async (props) => {
|
|
812
819
|
const { data } = await props.apiClient.getCurrentUserOrganizations();
|
|
@@ -823,23 +830,23 @@ const fetchOrganizations = async (props) => {
|
|
|
823
830
|
*/
|
|
824
831
|
const resolveOrg = async (props, given) => {
|
|
825
832
|
if (given) {
|
|
826
|
-
return { kind:
|
|
833
|
+
return { kind: "resolved", orgId: given, autoDetected: false };
|
|
827
834
|
}
|
|
828
835
|
try {
|
|
829
836
|
const orgs = await fetchOrganizations(props);
|
|
830
|
-
return { kind:
|
|
837
|
+
return { kind: "needs_selection", orgs, orgKeyLimited: false };
|
|
831
838
|
}
|
|
832
839
|
catch (err) {
|
|
833
840
|
if (!isOrgKeyLimitedError(err)) {
|
|
834
841
|
throw err;
|
|
835
842
|
}
|
|
836
|
-
log.debug(
|
|
843
|
+
log.debug("getCurrentUserOrganizations not allowed (org-scoped API key); attempting to derive org from existing projects.");
|
|
837
844
|
}
|
|
838
845
|
const detected = await detectOrgIdFromProjects(props);
|
|
839
846
|
if (detected) {
|
|
840
|
-
return { kind:
|
|
847
|
+
return { kind: "resolved", orgId: detected, autoDetected: true };
|
|
841
848
|
}
|
|
842
|
-
return { kind:
|
|
849
|
+
return { kind: "needs_selection", orgs: [], orgKeyLimited: true };
|
|
843
850
|
};
|
|
844
851
|
const detectOrgIdFromProjects = async (props) => {
|
|
845
852
|
try {
|
|
@@ -848,32 +855,32 @@ const detectOrgIdFromProjects = async (props) => {
|
|
|
848
855
|
}
|
|
849
856
|
catch (err) {
|
|
850
857
|
const message = err instanceof Error ? err.message : String(err);
|
|
851
|
-
log.debug(
|
|
858
|
+
log.debug("detectOrgIdFromProjects failed: %s", message);
|
|
852
859
|
return undefined;
|
|
853
860
|
}
|
|
854
861
|
};
|
|
855
862
|
const buildNeedsOrgResponse = (resolution) => {
|
|
856
863
|
if (resolution.orgKeyLimited) {
|
|
857
864
|
return {
|
|
858
|
-
status:
|
|
865
|
+
status: "needs_org",
|
|
859
866
|
instruction: "This Neon API key is organization-scoped, so the CLI cannot list the user's organizations and no existing project was found to auto-detect the org ID. Ask the user for their Neon organization ID (visible in the Neon Console under the org's Settings page, formatted like `org-bitter-breeze-12345678`) and re-run the next_command_template with that --org-id.",
|
|
860
867
|
options: [],
|
|
861
|
-
next_command_template:
|
|
868
|
+
next_command_template: "neonctl link --agent --org-id <org_id>",
|
|
862
869
|
};
|
|
863
870
|
}
|
|
864
871
|
const orgs = resolution.orgs;
|
|
865
872
|
return {
|
|
866
|
-
status:
|
|
873
|
+
status: "needs_org",
|
|
867
874
|
instruction: orgs.length === 0
|
|
868
|
-
?
|
|
869
|
-
: `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ?
|
|
875
|
+
? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking."
|
|
876
|
+
: `Ask the user which of these ${orgs.length} organization${orgs.length === 1 ? "" : "s"} they want to link the current directory to. After they pick one, re-run the next_command_template with the chosen --org-id value.`,
|
|
870
877
|
options: orgs.map((org) => ({ id: org.id, name: org.name })),
|
|
871
|
-
next_command_template:
|
|
878
|
+
next_command_template: "neonctl link --agent --org-id <org_id>",
|
|
872
879
|
};
|
|
873
880
|
};
|
|
874
881
|
const toAgentError = (err) => {
|
|
875
882
|
if (err instanceof LinkInputError) {
|
|
876
|
-
return { status:
|
|
883
|
+
return { status: "error", code: err.agentCode, message: err.message };
|
|
877
884
|
}
|
|
878
885
|
if (isNeonApiError(err)) {
|
|
879
886
|
const status = err.status;
|
|
@@ -881,27 +888,31 @@ const toAgentError = (err) => {
|
|
|
881
888
|
const message = apiMessage !== undefined && apiMessage.length > 0
|
|
882
889
|
? apiMessage
|
|
883
890
|
: err.message;
|
|
884
|
-
let code =
|
|
891
|
+
let code = "API_ERROR";
|
|
885
892
|
if (status === 401 || status === 403) {
|
|
886
|
-
code =
|
|
893
|
+
code = "AUTH_ERROR";
|
|
887
894
|
}
|
|
888
895
|
else if (status !== undefined && status >= 400 && status < 500) {
|
|
889
|
-
code =
|
|
896
|
+
code = "CLIENT_ERROR";
|
|
890
897
|
}
|
|
891
898
|
else if (status !== undefined && status >= 500) {
|
|
892
|
-
code =
|
|
899
|
+
code = "SERVER_ERROR";
|
|
893
900
|
}
|
|
894
|
-
else if (err.code ===
|
|
895
|
-
code =
|
|
901
|
+
else if (err.code === "ECONNABORTED") {
|
|
902
|
+
code = "TIMEOUT";
|
|
896
903
|
}
|
|
897
|
-
return { status:
|
|
904
|
+
return { status: "error", code, message };
|
|
898
905
|
}
|
|
899
906
|
if (err instanceof Error) {
|
|
900
|
-
return {
|
|
907
|
+
return {
|
|
908
|
+
status: "error",
|
|
909
|
+
code: "INTERNAL_ERROR",
|
|
910
|
+
message: err.message,
|
|
911
|
+
};
|
|
901
912
|
}
|
|
902
913
|
return {
|
|
903
|
-
status:
|
|
904
|
-
code:
|
|
914
|
+
status: "error",
|
|
915
|
+
code: "INTERNAL_ERROR",
|
|
905
916
|
message: String(err),
|
|
906
917
|
};
|
|
907
918
|
};
|
|
@@ -941,11 +952,11 @@ const resolveInteractiveBranch = async (props, projectId) => {
|
|
|
941
952
|
return branchPersistValue(only);
|
|
942
953
|
}
|
|
943
954
|
const picked = await pickBranchInteractively(branches, {
|
|
944
|
-
message:
|
|
945
|
-
nonInteractiveMessage:
|
|
946
|
-
|
|
955
|
+
message: "Which branch would you like to link?",
|
|
956
|
+
nonInteractiveMessage: "No branch could be selected without an interactive terminal. " +
|
|
957
|
+
"Re-run `neonctl link` interactively, or `neonctl checkout <branch>` to pin one.",
|
|
947
958
|
});
|
|
948
|
-
if (picked.kind ===
|
|
959
|
+
if (picked.kind === "existing") {
|
|
949
960
|
const existing = branches.find((b) => b.id === picked.branchId);
|
|
950
961
|
return existing ? branchPersistValue(existing) : picked.branchId;
|
|
951
962
|
}
|
|
@@ -962,11 +973,11 @@ const fetchRegions = async (props) => {
|
|
|
962
973
|
}
|
|
963
974
|
catch (err) {
|
|
964
975
|
if (isNeonApiError(err)) {
|
|
965
|
-
log.debug(
|
|
976
|
+
log.debug("getActiveRegions failed (%s), falling back to the static region list.", err.status ?? err.code ?? err.message);
|
|
966
977
|
}
|
|
967
978
|
else {
|
|
968
979
|
const message = err instanceof Error ? err.message : String(err);
|
|
969
|
-
log.debug(
|
|
980
|
+
log.debug("getActiveRegions failed (%s), falling back to the static region list.", message);
|
|
970
981
|
}
|
|
971
982
|
}
|
|
972
983
|
return staticRegionsFallback();
|
|
@@ -974,9 +985,9 @@ const fetchRegions = async (props) => {
|
|
|
974
985
|
const staticRegionsFallback = () => REGIONS.map((id) => ({
|
|
975
986
|
region_id: id,
|
|
976
987
|
name: id,
|
|
977
|
-
default: id ===
|
|
978
|
-
geo_lat:
|
|
979
|
-
geo_long:
|
|
988
|
+
default: id === "aws-us-east-2",
|
|
989
|
+
geo_lat: "",
|
|
990
|
+
geo_long: "",
|
|
980
991
|
}));
|
|
981
992
|
const createProject = async (props, args) => {
|
|
982
993
|
const project = {
|
|
@@ -987,7 +998,7 @@ const createProject = async (props, args) => {
|
|
|
987
998
|
};
|
|
988
999
|
const { data } = await props.apiClient.createProject({ project });
|
|
989
1000
|
if (!data.branch?.id) {
|
|
990
|
-
throw new Error(
|
|
1001
|
+
throw new Error("Project was created but the API response did not include a default branch id.");
|
|
991
1002
|
}
|
|
992
1003
|
return {
|
|
993
1004
|
project: {
|
|
@@ -1002,9 +1013,9 @@ const createProject = async (props, args) => {
|
|
|
1002
1013
|
const printSummary = (_props, summary) => {
|
|
1003
1014
|
const lines = [];
|
|
1004
1015
|
if (summary.created) {
|
|
1005
|
-
lines.push(`Created project ${summary.projectId}${summary.projectName ? ` ("${summary.projectName}")` :
|
|
1016
|
+
lines.push(`Created project ${summary.projectId}${summary.projectName ? ` ("${summary.projectName}")` : ""}${summary.regionId ? ` in ${summary.regionId}` : ""}.`);
|
|
1006
1017
|
}
|
|
1007
|
-
lines.push(`${summary.orgOnly ?
|
|
1018
|
+
lines.push(`${summary.orgOnly ? "Updated" : "Linked"} ${summary.contextFile}:`);
|
|
1008
1019
|
if (summary.orgId) {
|
|
1009
1020
|
lines.push(` orgId: ${summary.orgId}`);
|
|
1010
1021
|
}
|
|
@@ -1015,15 +1026,15 @@ const printSummary = (_props, summary) => {
|
|
|
1015
1026
|
lines.push(` branch: ${summary.branch}`);
|
|
1016
1027
|
}
|
|
1017
1028
|
if (summary.noChecks) {
|
|
1018
|
-
lines.push(
|
|
1019
|
-
lines.push(
|
|
1029
|
+
lines.push("");
|
|
1030
|
+
lines.push("Written offline (--no-checks): nothing was verified.");
|
|
1020
1031
|
}
|
|
1021
1032
|
else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
1022
|
-
lines.push(
|
|
1023
|
-
lines.push(
|
|
1033
|
+
lines.push("");
|
|
1034
|
+
lines.push("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
|
|
1024
1035
|
}
|
|
1025
|
-
lines.push(
|
|
1026
|
-
process.stdout.write(`${lines.join(
|
|
1036
|
+
lines.push("");
|
|
1037
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
1027
1038
|
};
|
|
1028
1039
|
/**
|
|
1029
1040
|
* Print the link summary, then run the bundled `env pull` so a human `link` that pinned a
|
|
@@ -1043,10 +1054,56 @@ const finalizeLink = async (props, summary) => {
|
|
|
1043
1054
|
envPull: props.envPull,
|
|
1044
1055
|
});
|
|
1045
1056
|
};
|
|
1057
|
+
/**
|
|
1058
|
+
* Interactive `link` finalize: the shared {@link finalizeLink} (summary + env
|
|
1059
|
+
* pull), then — as the last step — offer to manage the project's Neon setup as
|
|
1060
|
+
* code with a `neon.ts`. Kept out of {@link finalizeLink} so the non-interactive
|
|
1061
|
+
* paths never prompt.
|
|
1062
|
+
*/
|
|
1063
|
+
const finalizeInteractiveLink = async (props, summary) => {
|
|
1064
|
+
await finalizeLink(props, summary);
|
|
1065
|
+
await maybeOfferConfigInit(props, summary);
|
|
1066
|
+
};
|
|
1067
|
+
/**
|
|
1068
|
+
* Offer to set up infrastructure-as-code at the end of an interactive `link` —
|
|
1069
|
+
* the natural moment, since the project is now linked. Skipped when the project
|
|
1070
|
+
* already has a `neon.ts` (nothing to scaffold). On yes, `config init` writes the
|
|
1071
|
+
* starter `neon.ts` and installs the config packages, then env is pulled again so
|
|
1072
|
+
* the local `.env` reflects the policy — the same pull `link` runs when a project
|
|
1073
|
+
* already ships a `neon.ts`.
|
|
1074
|
+
*/
|
|
1075
|
+
const maybeOfferConfigInit = async (props, summary) => {
|
|
1076
|
+
const cwd = process.cwd();
|
|
1077
|
+
if (hasNeonConfigFile(cwd)) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const { value } = await prompts({
|
|
1081
|
+
onState: onPromptState,
|
|
1082
|
+
type: "confirm",
|
|
1083
|
+
name: "value",
|
|
1084
|
+
message: "Manage this project's Neon setup as code? Adds a neon.ts you can edit and apply with `neon config apply`.",
|
|
1085
|
+
initial: true,
|
|
1086
|
+
});
|
|
1087
|
+
if (value !== true) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await initCmd({ cwd, install: true });
|
|
1091
|
+
// The neon.ts (and its deps) now exist — pull env again so the local .env
|
|
1092
|
+
// reflects the policy, matching how `link` pulls when a project already ships
|
|
1093
|
+
// a neon.ts. Only meaningful when a branch was pinned (same guard as finalize).
|
|
1094
|
+
if (summary.branch && summary.projectId) {
|
|
1095
|
+
await autoPullEnvAfterPin({
|
|
1096
|
+
...props,
|
|
1097
|
+
projectId: summary.projectId,
|
|
1098
|
+
branch: summary.branch,
|
|
1099
|
+
envPull: props.envPull,
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
};
|
|
1046
1103
|
const onPromptState = (state) => {
|
|
1047
1104
|
if (state.aborted) {
|
|
1048
|
-
process.stdout.write(
|
|
1049
|
-
process.stdout.write(
|
|
1105
|
+
process.stdout.write("\x1B[?25h");
|
|
1106
|
+
process.stdout.write("\n");
|
|
1050
1107
|
process.exit(1);
|
|
1051
1108
|
}
|
|
1052
1109
|
};
|