neon 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -5
- package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
- package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
- package/dist/_chunks/credentials-MYdHdKah.js +188 -0
- package/dist/_chunks/env-NbA61JR3.js +585 -0
- package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
- package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
- package/dist/_chunks/profiles-Ir29rqns.js +217 -0
- package/dist/_chunks/psql-DWH-kc69.js +2169 -0
- package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
- package/dist/_chunks/secure_file-BucZj4yQ.js +39 -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 +573 -690
- 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 +639 -719
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +255 -305
- package/dist/commands/functions.js +275 -355
- 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 +615 -846
- 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 +2 -446
- package/dist/dev/functions.js +54 -68
- 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 +2 -51
- 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 +728 -984
- 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 +2 -2193
- 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 +2 -1148
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +260 -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 +2 -1253
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1303
- 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 +2 -846
- 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 +2 -2030
- 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 +8 -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 +130 -189
- 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 +39 -45
- 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 +11 -6
- package/dist/_shared/auth_selection.js +0 -86
- package/dist/_shared/credentials.js +0 -209
- package/dist/_shared/env-core/env.js +0 -558
- package/dist/_shared/env-core/reuse-secrets.js +0 -223
- package/dist/_shared/paths.js +0 -148
- package/dist/_shared/profiles.js +0 -276
- package/dist/_shared/secure_file.js +0 -43
package/dist/commands/link.js
CHANGED
|
@@ -1,1122 +1,977 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
3
|
-
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile, } from "../context.js";
|
|
1
|
+
import { t as __exportAll } from "../_chunks/rolldown-runtime-D7D4PA-g.js";
|
|
4
2
|
import { isCi } from "../env.js";
|
|
5
3
|
import { log } from "../log.js";
|
|
6
|
-
import {
|
|
4
|
+
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
5
|
+
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile } from "../context.js";
|
|
7
6
|
import { getCliName } from "../utils/cli_name.js";
|
|
8
|
-
import {
|
|
7
|
+
import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
|
|
9
8
|
import { autoPullEnvAfterPin, renderAgentPullNote } from "./env.js";
|
|
9
|
+
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
10
10
|
import { REGIONS } from "./projects.js";
|
|
11
|
+
import prompts from "prompts";
|
|
12
|
+
//#region src/commands/link.ts
|
|
13
|
+
var link_exports = /* @__PURE__ */ __exportAll({
|
|
14
|
+
builder: () => builder,
|
|
15
|
+
command: () => command,
|
|
16
|
+
describe: () => describe,
|
|
17
|
+
handler: () => handler
|
|
18
|
+
});
|
|
11
19
|
const PROJECTS_LIST_LIMIT = 100;
|
|
12
20
|
const CREATE_NEW_SENTINEL = "__create_new__";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
type: "boolean",
|
|
74
|
-
default: true,
|
|
75
|
-
},
|
|
76
|
-
})
|
|
77
|
-
.example([
|
|
78
|
-
[
|
|
79
|
-
"$0 link --project-id polished-snowflake-12345678",
|
|
80
|
-
`Link an existing project (org is inferred); pin a branch later with '${getCliName()} checkout'`,
|
|
81
|
-
],
|
|
82
|
-
[
|
|
83
|
-
"$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2",
|
|
84
|
-
"Create a new project and link it",
|
|
85
|
-
],
|
|
86
|
-
[
|
|
87
|
-
"$0 link --branch-id br-…",
|
|
88
|
-
"Pin a branch in the already-linked project",
|
|
89
|
-
],
|
|
90
|
-
[
|
|
91
|
-
"$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678",
|
|
92
|
-
"Write the context offline (no API calls, no verification)",
|
|
93
|
-
],
|
|
94
|
-
[
|
|
95
|
-
"$0 link --clear",
|
|
96
|
-
"Forget the current org/project/branch context",
|
|
97
|
-
],
|
|
21
|
+
const command = "link";
|
|
22
|
+
const describe = "Link the current directory to a Neon project";
|
|
23
|
+
const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
24
|
+
"org-id": {
|
|
25
|
+
describe: "Organization ID to link to",
|
|
26
|
+
type: "string"
|
|
27
|
+
},
|
|
28
|
+
"project-id": {
|
|
29
|
+
describe: "Existing project ID to link to",
|
|
30
|
+
type: "string"
|
|
31
|
+
},
|
|
32
|
+
"project-name": {
|
|
33
|
+
describe: "Name for a new project to create and link to",
|
|
34
|
+
type: "string"
|
|
35
|
+
},
|
|
36
|
+
"region-id": {
|
|
37
|
+
describe: "Region ID for a new project (e.g. aws-us-east-2). Required with --project-name.",
|
|
38
|
+
type: "string"
|
|
39
|
+
},
|
|
40
|
+
branch: {
|
|
41
|
+
alias: "branch-id",
|
|
42
|
+
describe: `Branch name or ID to pin in the context (resolved to its ID before writing). Without it, link only resolves the org and project — pin a branch with \`${getCliName()} checkout <branch>\` (link never guesses a default).`,
|
|
43
|
+
type: "string"
|
|
44
|
+
},
|
|
45
|
+
params: {
|
|
46
|
+
describe: "JSON object with link parameters, e.g. '{\"orgId\":\"...\",\"projectId\":\"...\"}' or '{\"orgId\":\"...\",\"projectName\":\"...\",\"regionId\":\"...\"}'. Flags take precedence over fields in --params.",
|
|
47
|
+
type: "string"
|
|
48
|
+
},
|
|
49
|
+
agent: {
|
|
50
|
+
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.",
|
|
51
|
+
type: "boolean",
|
|
52
|
+
default: false
|
|
53
|
+
},
|
|
54
|
+
yes: {
|
|
55
|
+
alias: "y",
|
|
56
|
+
describe: "Skip the \"already linked\" confirmation in interactive mode and re-link anyway.",
|
|
57
|
+
type: "boolean",
|
|
58
|
+
default: false
|
|
59
|
+
},
|
|
60
|
+
clear: {
|
|
61
|
+
describe: "Remove the org/project/branch context (writes an empty context file) instead of linking.",
|
|
62
|
+
type: "boolean",
|
|
63
|
+
default: false
|
|
64
|
+
},
|
|
65
|
+
checks: {
|
|
66
|
+
describe: "Verify the org/project/branch exist (and resolve the org from the project) before writing. On by default; use --no-checks to write the context offline with no API calls — it then requires --org-id and --project-id (--branch optional) and skips env pull.",
|
|
67
|
+
type: "boolean",
|
|
68
|
+
default: true
|
|
69
|
+
},
|
|
70
|
+
"env-pull": {
|
|
71
|
+
describe: "Pull the linked branch's Neon env vars (DATABASE_URL, …) into a local .env after linking. On by default; use --no-env-pull to skip (e.g. when injecting env at runtime with `neon-env run` / `neon dev`). Only runs when a branch is pinned.",
|
|
72
|
+
type: "boolean",
|
|
73
|
+
default: true
|
|
74
|
+
}
|
|
75
|
+
}).example([
|
|
76
|
+
["$0 link --project-id polished-snowflake-12345678", `Link an existing project (org is inferred); pin a branch later with '${getCliName()} checkout'`],
|
|
77
|
+
["$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2", "Create a new project and link it"],
|
|
78
|
+
["$0 link --branch-id br-…", "Pin a branch in the already-linked project"],
|
|
79
|
+
["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678", "Write the context offline (no API calls, no verification)"],
|
|
80
|
+
["$0 link --clear", "Forget the current org/project/branch context"]
|
|
98
81
|
]);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
82
|
+
const handler = async (props) => {
|
|
83
|
+
if (props.clear) {
|
|
84
|
+
clearContext(props.contextFile);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!props.checks) {
|
|
88
|
+
runWithoutChecks(props);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (props.agent) {
|
|
92
|
+
await runAgentSafely(props);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const inputs = parseInputs(props);
|
|
96
|
+
validateInputs(inputs);
|
|
97
|
+
const existing = readContextFile(props.contextFile);
|
|
98
|
+
if (canResolveNonInteractively(inputs, existing)) {
|
|
99
|
+
await runNonInteractive(props, inputs, existing);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (isCi()) {
|
|
103
|
+
log.error([
|
|
104
|
+
"Missing inputs and CI environment detected (no TTY for prompts).",
|
|
105
|
+
"",
|
|
106
|
+
"Use one of:",
|
|
107
|
+
` ${getCliName()} link --agent (JSON state machine for agents)`,
|
|
108
|
+
` ${getCliName()} link --project-id <project> (link to an existing project; org is inferred)`,
|
|
109
|
+
` ${getCliName()} link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)`
|
|
110
|
+
].join("\n"));
|
|
111
|
+
process.exit(1);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await runInteractive(props, inputs);
|
|
132
115
|
};
|
|
133
|
-
// ----------------------------------------------------------------------------
|
|
134
|
-
// Input parsing & validation
|
|
135
|
-
// ----------------------------------------------------------------------------
|
|
136
116
|
const parseInputs = (props) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
};
|
|
117
|
+
let fromParams = {};
|
|
118
|
+
if (props.params !== void 0 && props.params !== "") {
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(props.params);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
124
|
+
throw new Error(`Failed to parse --params JSON: ${message}`);
|
|
125
|
+
}
|
|
126
|
+
fromParams = extractParams(parsed);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
orgId: props.orgId ?? fromParams.orgId,
|
|
130
|
+
projectId: props.projectId ?? fromParams.projectId,
|
|
131
|
+
projectName: props.projectName ?? fromParams.projectName,
|
|
132
|
+
regionId: props.regionId ?? fromParams.regionId,
|
|
133
|
+
branch: props.branch ?? fromParams.branch
|
|
134
|
+
};
|
|
156
135
|
};
|
|
157
136
|
const extractParams = (raw) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
projectId: pickString("projectId"),
|
|
174
|
-
projectName: pickString("projectName"),
|
|
175
|
-
regionId: pickString("regionId"),
|
|
176
|
-
branch: pickString("branch") ?? pickString("branchId"),
|
|
177
|
-
};
|
|
137
|
+
if (raw === null || typeof raw !== "object") throw new Error("--params must be a JSON object");
|
|
138
|
+
const obj = raw;
|
|
139
|
+
const pickString = (key) => {
|
|
140
|
+
const value = obj[key];
|
|
141
|
+
if (value === void 0 || value === null) return void 0;
|
|
142
|
+
if (typeof value !== "string") throw new Error(`--params.${key} must be a string`);
|
|
143
|
+
return value;
|
|
144
|
+
};
|
|
145
|
+
return {
|
|
146
|
+
orgId: pickString("orgId"),
|
|
147
|
+
projectId: pickString("projectId"),
|
|
148
|
+
projectName: pickString("projectName"),
|
|
149
|
+
regionId: pickString("regionId"),
|
|
150
|
+
branch: pickString("branch") ?? pickString("branchId")
|
|
151
|
+
};
|
|
178
152
|
};
|
|
179
153
|
const validateInputs = (inputs) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
if (inputs.projectName && inputs.branch) {
|
|
184
|
-
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 \`${getCliName()} checkout <branch>\`.`);
|
|
185
|
-
}
|
|
154
|
+
if (inputs.projectId && (inputs.projectName || inputs.regionId)) throw new Error("Conflicting inputs: --project-id selects an existing project; --project-name and --region-id describe a new one. Pass only one set.");
|
|
155
|
+
if (inputs.projectName && inputs.branch) 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 \`${getCliName()} checkout <branch>\`.`);
|
|
186
156
|
};
|
|
187
157
|
/**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
158
|
+
* Whether the inputs (combined with the existing `.neon`) fully determine what
|
|
159
|
+
* to write without prompting the user. Everything else falls back to the
|
|
160
|
+
* interactive picker (TTY) or the CI guard.
|
|
161
|
+
*
|
|
162
|
+
* - `--project-id` is always enough: the org is inferred from the project and
|
|
163
|
+
* the branch is left to an explicit `checkout` (never auto-defaulted).
|
|
164
|
+
* - `--org-id --project-name --region-id` fully describes a project to create.
|
|
165
|
+
* - `--branch-id` needs a project, which it takes from the existing `.neon`.
|
|
166
|
+
* - `--org-id` on its own just records the default org (merged into any
|
|
167
|
+
* existing context).
|
|
168
|
+
*/
|
|
199
169
|
const canResolveNonInteractively = (inputs, existing) => {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
return true;
|
|
206
|
-
if (inputs.orgId && !inputs.projectName && !inputs.branch)
|
|
207
|
-
return true;
|
|
208
|
-
return false;
|
|
170
|
+
if (inputs.projectId) return true;
|
|
171
|
+
if (inputs.orgId && inputs.projectName && inputs.regionId) return true;
|
|
172
|
+
if (inputs.branch && existing.projectId) return true;
|
|
173
|
+
if (inputs.orgId && !inputs.projectName && !inputs.branch) return true;
|
|
174
|
+
return false;
|
|
209
175
|
};
|
|
210
|
-
// ----------------------------------------------------------------------------
|
|
211
|
-
// Context helpers
|
|
212
|
-
// ----------------------------------------------------------------------------
|
|
213
176
|
const clearContext = (contextFile) => {
|
|
214
|
-
|
|
215
|
-
|
|
177
|
+
updateContextFile(contextFile, {});
|
|
178
|
+
process.stdout.write(`Cleared ${contextFile}. The directory is no longer linked to a Neon org/project/branch.\n`);
|
|
216
179
|
};
|
|
217
180
|
/**
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
181
|
+
* `--no-checks`: write the context offline. Makes no API calls — so no org
|
|
182
|
+
* inference, no existence/access verification, and no env pull — which means
|
|
183
|
+
* the caller must supply both `--org-id` and `--project-id` (the org can't be
|
|
184
|
+
* inferred without the network). `--branch` stays optional. This is the CLI
|
|
185
|
+
* surface over {@link setContext}, useful for scripted/offline setups and for
|
|
186
|
+
* re-creating a `.neon` from values you already trust.
|
|
187
|
+
*/
|
|
225
188
|
const runWithoutChecks = (props) => {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
branch: inputs.branch,
|
|
258
|
-
created: false,
|
|
259
|
-
noChecks: true,
|
|
260
|
-
});
|
|
189
|
+
const inputs = parseInputs(props);
|
|
190
|
+
validateInputs(inputs);
|
|
191
|
+
if (inputs.projectName) 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.");
|
|
192
|
+
if (!inputs.orgId || !inputs.projectId) throw new Error("--no-checks writes the context with no API calls, so it needs both --org-id and --project-id (--branch is optional).");
|
|
193
|
+
setContext(props.contextFile, {
|
|
194
|
+
orgId: inputs.orgId,
|
|
195
|
+
projectId: inputs.projectId,
|
|
196
|
+
branch: inputs.branch
|
|
197
|
+
});
|
|
198
|
+
if (props.agent) {
|
|
199
|
+
emitAgent({
|
|
200
|
+
status: "linked",
|
|
201
|
+
context_file: props.contextFile,
|
|
202
|
+
context: {
|
|
203
|
+
orgId: inputs.orgId,
|
|
204
|
+
projectId: inputs.projectId,
|
|
205
|
+
branch: inputs.branch
|
|
206
|
+
},
|
|
207
|
+
project: { id: inputs.projectId },
|
|
208
|
+
message: `Wrote ${props.contextFile} without checks (org ${inputs.orgId}, project ${inputs.projectId}${inputs.branch ? `, branch ${inputs.branch}` : ""}). No verification or env pull was performed.`
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
printSummary(props, {
|
|
213
|
+
contextFile: props.contextFile,
|
|
214
|
+
orgId: inputs.orgId,
|
|
215
|
+
projectId: inputs.projectId,
|
|
216
|
+
branch: inputs.branch,
|
|
217
|
+
created: false,
|
|
218
|
+
noChecks: true
|
|
219
|
+
});
|
|
261
220
|
};
|
|
262
221
|
/**
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
const httpStatus = (err) => isNeonApiError(err) ? err.status :
|
|
222
|
+
* A bad user-supplied identifier (project/org/branch that doesn't exist or
|
|
223
|
+
* isn't accessible). Carries an `agentCode` so `--agent` mode can report a
|
|
224
|
+
* precise `status: error` code instead of a generic INTERNAL_ERROR, while the
|
|
225
|
+
* human path just prints the clear `message`.
|
|
226
|
+
*/
|
|
227
|
+
var LinkInputError = class extends Error {
|
|
228
|
+
constructor(message, agentCode) {
|
|
229
|
+
super(message);
|
|
230
|
+
this.name = "LinkInputError";
|
|
231
|
+
this.agentCode = agentCode;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const httpStatus = (err) => isNeonApiError(err) ? err.status : void 0;
|
|
276
235
|
/**
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
236
|
+
* Fetch a project, turning the common failure modes into clear, actionable
|
|
237
|
+
* errors. 401 is rethrown so the global handler can refresh credentials;
|
|
238
|
+
* everything else surfaces as a `LinkInputError` the user (or agent) can act on.
|
|
239
|
+
*/
|
|
281
240
|
const fetchProjectOrThrow = async (props, projectId) => {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
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");
|
|
293
|
-
}
|
|
294
|
-
if (status === 404) {
|
|
295
|
-
throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`, "NOT_FOUND");
|
|
296
|
-
}
|
|
297
|
-
throw err;
|
|
298
|
-
}
|
|
241
|
+
try {
|
|
242
|
+
const { data } = await props.apiClient.getProject(projectId);
|
|
243
|
+
return data.project;
|
|
244
|
+
} catch (err) {
|
|
245
|
+
const status = httpStatus(err);
|
|
246
|
+
if (status === 401) throw err;
|
|
247
|
+
if (status === 403) 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");
|
|
248
|
+
if (status === 404) throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`, "NOT_FOUND");
|
|
249
|
+
throw err;
|
|
250
|
+
}
|
|
299
251
|
};
|
|
300
252
|
/**
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
253
|
+
* Confirm the org exists and is reachable with the current API key by listing
|
|
254
|
+
* its projects (allowed for both user and org-scoped keys). Maps 403/404 to a
|
|
255
|
+
* clear message; 401 is rethrown for credential refresh.
|
|
256
|
+
*/
|
|
305
257
|
const verifyOrgAccess = async (props, orgId) => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (status === 403 || status === 404) {
|
|
318
|
-
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");
|
|
319
|
-
}
|
|
320
|
-
throw err;
|
|
321
|
-
}
|
|
258
|
+
try {
|
|
259
|
+
await props.apiClient.listProjects({
|
|
260
|
+
org_id: orgId,
|
|
261
|
+
limit: PROJECTS_LIST_LIMIT
|
|
262
|
+
});
|
|
263
|
+
} catch (err) {
|
|
264
|
+
const status = httpStatus(err);
|
|
265
|
+
if (status === 401) throw err;
|
|
266
|
+
if (status === 403 || status === 404) 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");
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
322
269
|
};
|
|
323
270
|
/**
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
271
|
+
* Resolve a branch reference (name *or* id) to the matching branch, while
|
|
272
|
+
* confirming it actually exists in the project. Unlike the shared
|
|
273
|
+
* `branchIdResolve`, this also verifies references that already look like ids
|
|
274
|
+
* (so a typo'd `br-…` doesn't silently get written), and surfaces the available
|
|
275
|
+
* branches when nothing matches so the user can correct it (or run `checkout`).
|
|
276
|
+
*/
|
|
330
277
|
const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return match;
|
|
336
|
-
}
|
|
337
|
-
const available = data.branches.length > 0
|
|
338
|
-
? data.branches
|
|
339
|
-
.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`)
|
|
340
|
-
.join(", ")
|
|
341
|
-
: "(none)";
|
|
342
|
-
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${available}. Pin one with \`${getCliName()} checkout <branch>\`.`, "NOT_FOUND");
|
|
278
|
+
const { data } = await props.apiClient.listProjectBranches({ projectId });
|
|
279
|
+
const match = data.branches.find((b) => b.id === branchRef) ?? data.branches.find((b) => b.name === branchRef);
|
|
280
|
+
if (match) return match;
|
|
281
|
+
throw new LinkInputError(`Branch '${branchRef}' not found in project '${projectId}'. Available branches: ${data.branches.length > 0 ? data.branches.map((b) => `${b.id}${b.name ? ` (${b.name})` : ""}`).join(", ") : "(none)"}. Pin one with \`${getCliName()} checkout <branch>\`.`, "NOT_FOUND");
|
|
343
282
|
};
|
|
344
283
|
/**
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
284
|
+
* The value to persist for a branch: prefer its human-readable **name** (nicer
|
|
285
|
+
* to read in `.neon`, and still resolvable by every command), falling back to
|
|
286
|
+
* the id when the branch has no name.
|
|
287
|
+
*/
|
|
349
288
|
const branchPersistValue = (branch) => branch.name ?? branch.id;
|
|
350
289
|
/**
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
290
|
+
* Verify the project (and the org, when supplied) and resolve the org id to
|
|
291
|
+
* persist.
|
|
292
|
+
*
|
|
293
|
+
* The project is always fetched, which both validates it and yields its
|
|
294
|
+
* `org_id`. When `--org-id` is passed too: if the project reports an org it must
|
|
295
|
+
* match (else a clear mismatch error); if it reports none, the supplied org is
|
|
296
|
+
* verified on its own. Without `--org-id` the project's own org is used, falling
|
|
297
|
+
* back to the org already recorded for the *same* project in `.neon`. Projects
|
|
298
|
+
* on a personal account have no org, so `undefined` is a valid result — the
|
|
299
|
+
* field is simply omitted.
|
|
300
|
+
*/
|
|
362
301
|
const resolveOrgForProject = async (props, inputs, existing, projectId) => {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
return inputs.orgId;
|
|
373
|
-
}
|
|
374
|
-
if (projectOrg) {
|
|
375
|
-
return projectOrg;
|
|
376
|
-
}
|
|
377
|
-
if (projectId === existing.projectId && existing.orgId) {
|
|
378
|
-
return existing.orgId;
|
|
379
|
-
}
|
|
380
|
-
return undefined;
|
|
302
|
+
const projectOrg = (await fetchProjectOrThrow(props, projectId)).org_id ?? void 0;
|
|
303
|
+
if (inputs.orgId) {
|
|
304
|
+
if (projectOrg && projectOrg !== inputs.orgId) 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");
|
|
305
|
+
if (!projectOrg) await verifyOrgAccess(props, inputs.orgId);
|
|
306
|
+
return inputs.orgId;
|
|
307
|
+
}
|
|
308
|
+
if (projectOrg) return projectOrg;
|
|
309
|
+
if (projectId === existing.projectId && existing.orgId) return existing.orgId;
|
|
381
310
|
};
|
|
382
311
|
/**
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
312
|
+
* Resolve the branch to persist alongside a project in non-interactive mode.
|
|
313
|
+
*
|
|
314
|
+
* `link` never guesses the project's default branch — that's `checkout`'s job —
|
|
315
|
+
* so the only sources are an explicit `--branch` (name or id, verified and
|
|
316
|
+
* normalized to its name) or a branch already pinned for the *same* project (so
|
|
317
|
+
* re-linking it doesn't drop your checked-out branch). Reading the existing
|
|
318
|
+
* branch via {@link contextBranch} also recovers a legacy `branchId` field.
|
|
319
|
+
*/
|
|
391
320
|
const resolvePinnedBranch = async (props, inputs, existing, projectId) => {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
return contextBranch(existing);
|
|
398
|
-
}
|
|
399
|
-
return undefined;
|
|
321
|
+
if (inputs.branch) {
|
|
322
|
+
const branch = await resolveBranchRef(props, projectId, inputs.branch);
|
|
323
|
+
return branchPersistValue(branch);
|
|
324
|
+
}
|
|
325
|
+
if (projectId === existing.projectId) return contextBranch(existing);
|
|
400
326
|
};
|
|
401
|
-
// ----------------------------------------------------------------------------
|
|
402
|
-
// Non-interactive flag-driven mode
|
|
403
|
-
// ----------------------------------------------------------------------------
|
|
404
327
|
const runNonInteractive = async (props, inputs, existing) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
328
|
+
if (inputs.projectName) {
|
|
329
|
+
const orgId = mustString(inputs.orgId, "orgId");
|
|
330
|
+
await verifyOrgAccess(props, orgId);
|
|
331
|
+
const created = await createProject(props, {
|
|
332
|
+
orgId,
|
|
333
|
+
name: inputs.projectName,
|
|
334
|
+
regionId: mustString(inputs.regionId, "regionId")
|
|
335
|
+
});
|
|
336
|
+
applyContext(props.contextFile, {
|
|
337
|
+
orgId,
|
|
338
|
+
projectId: created.project.id,
|
|
339
|
+
branch: created.branchName
|
|
340
|
+
});
|
|
341
|
+
await finalizeLink(props, {
|
|
342
|
+
contextFile: props.contextFile,
|
|
343
|
+
orgId,
|
|
344
|
+
projectId: created.project.id,
|
|
345
|
+
branch: created.branchName,
|
|
346
|
+
created: true,
|
|
347
|
+
projectName: created.project.name,
|
|
348
|
+
regionId: created.project.region_id
|
|
349
|
+
});
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (inputs.projectId) {
|
|
353
|
+
const orgId = await resolveOrgForProject(props, inputs, existing, inputs.projectId);
|
|
354
|
+
const branch = await resolvePinnedBranch(props, inputs, existing, inputs.projectId);
|
|
355
|
+
applyContext(props.contextFile, {
|
|
356
|
+
orgId,
|
|
357
|
+
projectId: inputs.projectId,
|
|
358
|
+
branch
|
|
359
|
+
});
|
|
360
|
+
await finalizeLink(props, {
|
|
361
|
+
contextFile: props.contextFile,
|
|
362
|
+
orgId,
|
|
363
|
+
projectId: inputs.projectId,
|
|
364
|
+
branch,
|
|
365
|
+
created: false
|
|
366
|
+
});
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (inputs.branch && existing.projectId) {
|
|
370
|
+
const projectId = existing.projectId;
|
|
371
|
+
const orgId = await resolveOrgForProject(props, inputs, existing, projectId);
|
|
372
|
+
const branch = await resolvePinnedBranch(props, inputs, existing, projectId);
|
|
373
|
+
applyContext(props.contextFile, {
|
|
374
|
+
orgId,
|
|
375
|
+
projectId,
|
|
376
|
+
branch
|
|
377
|
+
});
|
|
378
|
+
await finalizeLink(props, {
|
|
379
|
+
contextFile: props.contextFile,
|
|
380
|
+
orgId,
|
|
381
|
+
projectId,
|
|
382
|
+
branch,
|
|
383
|
+
created: false
|
|
384
|
+
});
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (inputs.orgId) {
|
|
388
|
+
const orgId = inputs.orgId;
|
|
389
|
+
await verifyOrgAccess(props, orgId);
|
|
390
|
+
const projectId = existing.projectId;
|
|
391
|
+
const branch = projectId ? contextBranch(existing) : void 0;
|
|
392
|
+
applyContext(props.contextFile, {
|
|
393
|
+
orgId,
|
|
394
|
+
projectId,
|
|
395
|
+
branch
|
|
396
|
+
});
|
|
397
|
+
printSummary(props, {
|
|
398
|
+
contextFile: props.contextFile,
|
|
399
|
+
orgId,
|
|
400
|
+
projectId,
|
|
401
|
+
branch,
|
|
402
|
+
created: false,
|
|
403
|
+
orgOnly: true
|
|
404
|
+
});
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
484
407
|
};
|
|
485
|
-
// ----------------------------------------------------------------------------
|
|
486
|
-
// Interactive mode (TTY)
|
|
487
|
-
// ----------------------------------------------------------------------------
|
|
488
408
|
const runInteractive = async (props, inputs) => {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
projectId: created.project.id,
|
|
561
|
-
branch: created.branchName,
|
|
562
|
-
});
|
|
563
|
-
await finalizeInteractiveLink(props, {
|
|
564
|
-
contextFile: props.contextFile,
|
|
565
|
-
orgId,
|
|
566
|
-
projectId: created.project.id,
|
|
567
|
-
branch: created.branchName,
|
|
568
|
-
created: true,
|
|
569
|
-
projectName: created.project.name,
|
|
570
|
-
regionId: created.project.region_id,
|
|
571
|
-
});
|
|
409
|
+
if (!props.yes) await confirmRelinkIfNeeded(props);
|
|
410
|
+
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
411
|
+
let orgId;
|
|
412
|
+
if (orgResolution.kind === "resolved") {
|
|
413
|
+
orgId = orgResolution.orgId;
|
|
414
|
+
if (orgResolution.autoDetected) log.info(`Detected organization ${orgId} from your existing projects (organization-scoped API key).`);
|
|
415
|
+
} else if (orgResolution.orgKeyLimited) throw new Error("This API key is organization-scoped, so the CLI cannot list your organizations, and no existing project was found in this org to auto-detect the ID. Re-run with `--org-id <your_org_id>` (find it in the Neon Console under Settings).");
|
|
416
|
+
else orgId = await promptOrgFromList(orgResolution.orgs);
|
|
417
|
+
if (inputs.projectName && inputs.regionId) {
|
|
418
|
+
const created = await createProject(props, {
|
|
419
|
+
orgId,
|
|
420
|
+
name: inputs.projectName,
|
|
421
|
+
regionId: inputs.regionId
|
|
422
|
+
});
|
|
423
|
+
applyContext(props.contextFile, {
|
|
424
|
+
orgId,
|
|
425
|
+
projectId: created.project.id,
|
|
426
|
+
branch: created.branchName
|
|
427
|
+
});
|
|
428
|
+
await finalizeInteractiveLink(props, {
|
|
429
|
+
contextFile: props.contextFile,
|
|
430
|
+
orgId,
|
|
431
|
+
projectId: created.project.id,
|
|
432
|
+
branch: created.branchName,
|
|
433
|
+
created: true,
|
|
434
|
+
projectName: created.project.name,
|
|
435
|
+
regionId: created.project.region_id
|
|
436
|
+
});
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const projects = await listAllProjects(props, orgId);
|
|
440
|
+
const action = await promptProjectChoice(projects, inputs.projectName);
|
|
441
|
+
if (action.type === "existing") {
|
|
442
|
+
const branch = await resolveInteractiveBranch(props, action.projectId);
|
|
443
|
+
applyContext(props.contextFile, {
|
|
444
|
+
orgId,
|
|
445
|
+
projectId: action.projectId,
|
|
446
|
+
branch
|
|
447
|
+
});
|
|
448
|
+
await finalizeInteractiveLink(props, {
|
|
449
|
+
contextFile: props.contextFile,
|
|
450
|
+
orgId,
|
|
451
|
+
projectId: action.projectId,
|
|
452
|
+
branch,
|
|
453
|
+
created: false,
|
|
454
|
+
projectName: action.name,
|
|
455
|
+
regionId: action.regionId
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const projectName = inputs.projectName ?? await promptProjectName(action.suggestedName);
|
|
460
|
+
const regionId = inputs.regionId ?? await promptRegion(props);
|
|
461
|
+
const created = await createProject(props, {
|
|
462
|
+
orgId,
|
|
463
|
+
name: projectName,
|
|
464
|
+
regionId
|
|
465
|
+
});
|
|
466
|
+
applyContext(props.contextFile, {
|
|
467
|
+
orgId,
|
|
468
|
+
projectId: created.project.id,
|
|
469
|
+
branch: created.branchName
|
|
470
|
+
});
|
|
471
|
+
await finalizeInteractiveLink(props, {
|
|
472
|
+
contextFile: props.contextFile,
|
|
473
|
+
orgId,
|
|
474
|
+
projectId: created.project.id,
|
|
475
|
+
branch: created.branchName,
|
|
476
|
+
created: true,
|
|
477
|
+
projectName: created.project.name,
|
|
478
|
+
regionId: created.project.region_id
|
|
479
|
+
});
|
|
572
480
|
};
|
|
573
481
|
const confirmRelinkIfNeeded = async (props) => {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
process.exit(0);
|
|
588
|
-
}
|
|
482
|
+
const existing = readContextFile(props.contextFile);
|
|
483
|
+
if (!existing.orgId || !existing.projectId) return;
|
|
484
|
+
const { proceed } = await prompts({
|
|
485
|
+
onState: onPromptState,
|
|
486
|
+
type: "confirm",
|
|
487
|
+
name: "proceed",
|
|
488
|
+
message: `${props.contextFile} is already linked to project ${existing.projectId} (org ${existing.orgId}). Re-link?`,
|
|
489
|
+
initial: true
|
|
490
|
+
});
|
|
491
|
+
if (!proceed) {
|
|
492
|
+
process.stdout.write("Aborted. Existing link preserved.\n");
|
|
493
|
+
process.exit(0);
|
|
494
|
+
}
|
|
589
495
|
};
|
|
590
496
|
const promptOrgFromList = async (orgs) => {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
})),
|
|
610
|
-
initial: 0,
|
|
611
|
-
});
|
|
612
|
-
return orgId;
|
|
497
|
+
if (!orgs.length) throw new Error(`You don't belong to any organizations. Create one in the Neon Console first: https://console.neon.tech/`);
|
|
498
|
+
if (orgs.length === 1) {
|
|
499
|
+
const [only] = orgs;
|
|
500
|
+
log.info(`Linking organization ${only.name} (${only.id}).`);
|
|
501
|
+
return only.id;
|
|
502
|
+
}
|
|
503
|
+
const { orgId } = await prompts({
|
|
504
|
+
onState: onPromptState,
|
|
505
|
+
type: "select",
|
|
506
|
+
name: "orgId",
|
|
507
|
+
message: "Which organization would you like to link?",
|
|
508
|
+
choices: orgs.map((org) => ({
|
|
509
|
+
title: `${org.name} (${org.id})`,
|
|
510
|
+
value: org.id
|
|
511
|
+
})),
|
|
512
|
+
initial: 0
|
|
513
|
+
});
|
|
514
|
+
return orgId;
|
|
613
515
|
};
|
|
614
516
|
const promptProjectChoice = async (projects, suggestedName) => {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
};
|
|
517
|
+
const choices = [{
|
|
518
|
+
title: "+ Create new project…",
|
|
519
|
+
value: CREATE_NEW_SENTINEL
|
|
520
|
+
}, ...projects.map((project) => ({
|
|
521
|
+
title: `${project.name} (${project.id})`,
|
|
522
|
+
value: project.id
|
|
523
|
+
}))];
|
|
524
|
+
const { selection } = await prompts({
|
|
525
|
+
onState: onPromptState,
|
|
526
|
+
type: "select",
|
|
527
|
+
name: "selection",
|
|
528
|
+
message: "Which project would you like to link?",
|
|
529
|
+
choices,
|
|
530
|
+
initial: projects.length > 0 ? 1 : 0
|
|
531
|
+
});
|
|
532
|
+
if (selection === CREATE_NEW_SENTINEL) return {
|
|
533
|
+
type: "create",
|
|
534
|
+
suggestedName
|
|
535
|
+
};
|
|
536
|
+
const project = projects.find((p) => p.id === selection);
|
|
537
|
+
return {
|
|
538
|
+
type: "existing",
|
|
539
|
+
projectId: selection,
|
|
540
|
+
name: project?.name,
|
|
541
|
+
regionId: project?.region_id
|
|
542
|
+
};
|
|
642
543
|
};
|
|
643
544
|
const promptProjectName = async (suggestedName) => {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
});
|
|
654
|
-
return String(name).trim();
|
|
545
|
+
const { name } = await prompts({
|
|
546
|
+
onState: onPromptState,
|
|
547
|
+
type: "text",
|
|
548
|
+
name: "name",
|
|
549
|
+
message: "Name for the new project:",
|
|
550
|
+
initial: suggestedName,
|
|
551
|
+
validate: (value) => value && value.trim().length > 0 ? true : "Project name is required"
|
|
552
|
+
});
|
|
553
|
+
return String(name).trim();
|
|
655
554
|
};
|
|
656
555
|
const promptRegion = async (props) => {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
556
|
+
const regions = await fetchRegions(props);
|
|
557
|
+
const defaultIndex = Math.max(0, regions.findIndex((r) => r.default));
|
|
558
|
+
const { regionId } = await prompts({
|
|
559
|
+
onState: onPromptState,
|
|
560
|
+
type: "select",
|
|
561
|
+
name: "regionId",
|
|
562
|
+
message: "Which region should the new project run in?",
|
|
563
|
+
choices: regions.map((region) => ({
|
|
564
|
+
title: `${region.name} (${region.region_id})`,
|
|
565
|
+
value: region.region_id
|
|
566
|
+
})),
|
|
567
|
+
initial: defaultIndex
|
|
568
|
+
});
|
|
569
|
+
return regionId;
|
|
671
570
|
};
|
|
672
|
-
// ----------------------------------------------------------------------------
|
|
673
|
-
// Agent mode (JSON state machine)
|
|
674
|
-
// ----------------------------------------------------------------------------
|
|
675
571
|
const runAgentSafely = async (props) => {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
572
|
+
try {
|
|
573
|
+
const inputs = parseInputs(props);
|
|
574
|
+
validateInputs(inputs);
|
|
575
|
+
await runAgent(props, inputs);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
emitAgent(toAgentError(err));
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
685
580
|
};
|
|
686
581
|
const runAgent = async (props, inputs) => {
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
},
|
|
802
|
-
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`,
|
|
803
|
-
});
|
|
582
|
+
const { projectId, projectName, regionId, branch } = inputs;
|
|
583
|
+
const existing = readContextFile(props.contextFile);
|
|
584
|
+
if (projectId) {
|
|
585
|
+
const orgId = await resolveOrgForProject(props, inputs, existing, projectId);
|
|
586
|
+
const pinnedBranch = await resolvePinnedBranch(props, inputs, existing, projectId);
|
|
587
|
+
applyContext(props.contextFile, {
|
|
588
|
+
orgId,
|
|
589
|
+
projectId,
|
|
590
|
+
branch: pinnedBranch
|
|
591
|
+
});
|
|
592
|
+
const orgSuffix = orgId ? ` (org ${orgId})` : "";
|
|
593
|
+
if (pinnedBranch) {
|
|
594
|
+
const pullNote = renderAgentPullNote(await autoPullEnvAfterPin({
|
|
595
|
+
...props,
|
|
596
|
+
projectId,
|
|
597
|
+
branch: pinnedBranch,
|
|
598
|
+
envPull: props.envPull
|
|
599
|
+
}));
|
|
600
|
+
emitAgent({
|
|
601
|
+
status: "linked",
|
|
602
|
+
context_file: props.contextFile,
|
|
603
|
+
context: {
|
|
604
|
+
orgId,
|
|
605
|
+
projectId,
|
|
606
|
+
branch: pinnedBranch
|
|
607
|
+
},
|
|
608
|
+
project: { id: projectId },
|
|
609
|
+
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix} on branch ${pinnedBranch}.${pullNote}`
|
|
610
|
+
});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
emitAgent({
|
|
614
|
+
status: "linked",
|
|
615
|
+
context_file: props.contextFile,
|
|
616
|
+
context: {
|
|
617
|
+
orgId,
|
|
618
|
+
projectId
|
|
619
|
+
},
|
|
620
|
+
project: { id: projectId },
|
|
621
|
+
message: `Linked ${props.contextFile} to project ${projectId}${orgSuffix}. No branch pinned — run \`${getCliName()} checkout <branch>\` (omit the branch to list options) to pin one and pull its env vars.`
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
626
|
+
if (orgResolution.kind === "needs_selection") {
|
|
627
|
+
emitAgent(buildNeedsOrgResponse(orgResolution));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const orgId = orgResolution.orgId;
|
|
631
|
+
if (projectName && !regionId) {
|
|
632
|
+
const regions = await fetchRegions(props);
|
|
633
|
+
emitAgent({
|
|
634
|
+
status: "needs_project_details",
|
|
635
|
+
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.`,
|
|
636
|
+
regions: regions.map((region) => ({
|
|
637
|
+
id: region.region_id,
|
|
638
|
+
name: region.name,
|
|
639
|
+
default: region.default
|
|
640
|
+
})),
|
|
641
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name ${shellArg(projectName)} --region-id <region_id>`
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (projectName && regionId) {
|
|
646
|
+
await verifyOrgAccess(props, orgId);
|
|
647
|
+
const created = await createProject(props, {
|
|
648
|
+
orgId,
|
|
649
|
+
name: projectName,
|
|
650
|
+
regionId
|
|
651
|
+
});
|
|
652
|
+
applyContext(props.contextFile, {
|
|
653
|
+
orgId,
|
|
654
|
+
projectId: created.project.id,
|
|
655
|
+
branch: created.branchName
|
|
656
|
+
});
|
|
657
|
+
const pullNote = renderAgentPullNote(await autoPullEnvAfterPin({
|
|
658
|
+
...props,
|
|
659
|
+
projectId: created.project.id,
|
|
660
|
+
branch: created.branchName,
|
|
661
|
+
envPull: props.envPull
|
|
662
|
+
}));
|
|
663
|
+
emitAgent({
|
|
664
|
+
status: "linked",
|
|
665
|
+
context_file: props.contextFile,
|
|
666
|
+
context: {
|
|
667
|
+
orgId,
|
|
668
|
+
projectId: created.project.id,
|
|
669
|
+
branch: created.branchName
|
|
670
|
+
},
|
|
671
|
+
project: {
|
|
672
|
+
id: created.project.id,
|
|
673
|
+
name: created.project.name,
|
|
674
|
+
region_id: created.project.region_id
|
|
675
|
+
},
|
|
676
|
+
message: `Created project ${created.project.id} ("${created.project.name ?? projectName}") in ${created.project.region_id ?? regionId} and linked ${props.contextFile} on branch ${created.branchName}.${pullNote}`
|
|
677
|
+
});
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const projects = await listAllProjects(props, orgId);
|
|
681
|
+
const branchNote = branch ? ` 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 \`${getCliName()} checkout ${branch}\`.` : "";
|
|
682
|
+
emitAgent({
|
|
683
|
+
status: "needs_project",
|
|
684
|
+
instruction: (projects.length === 0 ? `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.` : `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).`) + branchNote,
|
|
685
|
+
options: projects.map((project) => ({
|
|
686
|
+
id: project.id,
|
|
687
|
+
name: project.name,
|
|
688
|
+
region_id: project.region_id
|
|
689
|
+
})),
|
|
690
|
+
create_option: {
|
|
691
|
+
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.",
|
|
692
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-name <name> --region-id <region_id>`
|
|
693
|
+
},
|
|
694
|
+
next_command_template: `${getCliName()} link --agent --org-id ${shellArg(orgId)} --project-id <project_id>`
|
|
695
|
+
});
|
|
804
696
|
};
|
|
805
697
|
const emitAgent = (response) => {
|
|
806
|
-
|
|
698
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
807
699
|
};
|
|
808
|
-
// ----------------------------------------------------------------------------
|
|
809
|
-
// API helpers
|
|
810
|
-
// ----------------------------------------------------------------------------
|
|
811
700
|
const ORG_KEY_LIMITED_FRAGMENT = "not allowed for organization API keys";
|
|
812
701
|
const isOrgKeyLimitedError = (err) => {
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
return (typeof message === "string" &&
|
|
817
|
-
message.includes(ORG_KEY_LIMITED_FRAGMENT));
|
|
702
|
+
if (!isNeonApiError(err)) return false;
|
|
703
|
+
const message = messageFromBody(err.data);
|
|
704
|
+
return typeof message === "string" && message.includes(ORG_KEY_LIMITED_FRAGMENT);
|
|
818
705
|
};
|
|
819
706
|
const fetchOrganizations = async (props) => {
|
|
820
|
-
|
|
821
|
-
|
|
707
|
+
const { data } = await props.apiClient.getCurrentUserOrganizations();
|
|
708
|
+
return data.organizations ?? [];
|
|
822
709
|
};
|
|
823
710
|
/**
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
711
|
+
* Resolves the org id from the explicit flag, falling back to listing user orgs.
|
|
712
|
+
*
|
|
713
|
+
* For organization-scoped API keys, `getCurrentUserOrganizations` is forbidden;
|
|
714
|
+
* in that case we try to auto-detect the org from the first existing project
|
|
715
|
+
* (since all projects of an org key live in the same org). If no project exists
|
|
716
|
+
* yet, we return `needs_selection` with `orgKeyLimited: true` so callers can
|
|
717
|
+
* give a precise instruction to the user.
|
|
718
|
+
*/
|
|
832
719
|
const resolveOrg = async (props, given) => {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
720
|
+
if (given) return {
|
|
721
|
+
kind: "resolved",
|
|
722
|
+
orgId: given,
|
|
723
|
+
autoDetected: false
|
|
724
|
+
};
|
|
725
|
+
try {
|
|
726
|
+
return {
|
|
727
|
+
kind: "needs_selection",
|
|
728
|
+
orgs: await fetchOrganizations(props),
|
|
729
|
+
orgKeyLimited: false
|
|
730
|
+
};
|
|
731
|
+
} catch (err) {
|
|
732
|
+
if (!isOrgKeyLimitedError(err)) throw err;
|
|
733
|
+
log.debug("getCurrentUserOrganizations not allowed (org-scoped API key); attempting to derive org from existing projects.");
|
|
734
|
+
}
|
|
735
|
+
const detected = await detectOrgIdFromProjects(props);
|
|
736
|
+
if (detected) return {
|
|
737
|
+
kind: "resolved",
|
|
738
|
+
orgId: detected,
|
|
739
|
+
autoDetected: true
|
|
740
|
+
};
|
|
741
|
+
return {
|
|
742
|
+
kind: "needs_selection",
|
|
743
|
+
orgs: [],
|
|
744
|
+
orgKeyLimited: true
|
|
745
|
+
};
|
|
851
746
|
};
|
|
852
747
|
const detectOrgIdFromProjects = async (props) => {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
}
|
|
748
|
+
try {
|
|
749
|
+
const { data } = await props.apiClient.listProjects({ limit: 1 });
|
|
750
|
+
return data.projects[0]?.org_id ?? void 0;
|
|
751
|
+
} catch (err) {
|
|
752
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
753
|
+
log.debug("detectOrgIdFromProjects failed: %s", message);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
862
756
|
};
|
|
863
757
|
const buildNeedsOrgResponse = (resolution) => {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
};
|
|
758
|
+
if (resolution.orgKeyLimited) return {
|
|
759
|
+
status: "needs_org",
|
|
760
|
+
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.",
|
|
761
|
+
options: [],
|
|
762
|
+
next_command_template: `${getCliName()} link --agent --org-id <org_id>`
|
|
763
|
+
};
|
|
764
|
+
const orgs = resolution.orgs;
|
|
765
|
+
return {
|
|
766
|
+
status: "needs_org",
|
|
767
|
+
instruction: orgs.length === 0 ? "The user does not belong to any organizations. Ask them to create one in the Neon Console (https://console.neon.tech/) before linking." : `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.`,
|
|
768
|
+
options: orgs.map((org) => ({
|
|
769
|
+
id: org.id,
|
|
770
|
+
name: org.name
|
|
771
|
+
})),
|
|
772
|
+
next_command_template: `${getCliName()} link --agent --org-id <org_id>`
|
|
773
|
+
};
|
|
881
774
|
};
|
|
882
775
|
const toAgentError = (err) => {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
}
|
|
914
|
-
return {
|
|
915
|
-
status: "error",
|
|
916
|
-
code: "INTERNAL_ERROR",
|
|
917
|
-
message: String(err),
|
|
918
|
-
};
|
|
776
|
+
if (err instanceof LinkInputError) return {
|
|
777
|
+
status: "error",
|
|
778
|
+
code: err.agentCode,
|
|
779
|
+
message: err.message
|
|
780
|
+
};
|
|
781
|
+
if (isNeonApiError(err)) {
|
|
782
|
+
const status = err.status;
|
|
783
|
+
const apiMessage = messageFromBody(err.data);
|
|
784
|
+
const message = apiMessage !== void 0 && apiMessage.length > 0 ? apiMessage : err.message;
|
|
785
|
+
let code = "API_ERROR";
|
|
786
|
+
if (status === 401 || status === 403) code = "AUTH_ERROR";
|
|
787
|
+
else if (status !== void 0 && status >= 400 && status < 500) code = "CLIENT_ERROR";
|
|
788
|
+
else if (status !== void 0 && status >= 500) code = "SERVER_ERROR";
|
|
789
|
+
else if (err.code === "ECONNABORTED") code = "TIMEOUT";
|
|
790
|
+
return {
|
|
791
|
+
status: "error",
|
|
792
|
+
code,
|
|
793
|
+
message
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
if (err instanceof Error) return {
|
|
797
|
+
status: "error",
|
|
798
|
+
code: "INTERNAL_ERROR",
|
|
799
|
+
message: err.message
|
|
800
|
+
};
|
|
801
|
+
return {
|
|
802
|
+
status: "error",
|
|
803
|
+
code: "INTERNAL_ERROR",
|
|
804
|
+
message: String(err)
|
|
805
|
+
};
|
|
919
806
|
};
|
|
920
807
|
const listAllProjects = async (props, orgId) => {
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
}
|
|
935
|
-
return result;
|
|
808
|
+
const result = [];
|
|
809
|
+
let cursor;
|
|
810
|
+
while (true) {
|
|
811
|
+
const { data } = await props.apiClient.listProjects({
|
|
812
|
+
limit: PROJECTS_LIST_LIMIT,
|
|
813
|
+
org_id: orgId,
|
|
814
|
+
cursor
|
|
815
|
+
});
|
|
816
|
+
result.push(...data.projects);
|
|
817
|
+
cursor = data.pagination?.cursor;
|
|
818
|
+
if (data.projects.length < PROJECTS_LIST_LIMIT) break;
|
|
819
|
+
}
|
|
820
|
+
return result;
|
|
936
821
|
};
|
|
937
822
|
/**
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
823
|
+
* Resolve which branch to pin for an interactively-chosen project, returned as the value to
|
|
824
|
+
* persist (its name when known, see {@link branchPersistValue}). When the project has a
|
|
825
|
+
* single branch there is nothing to choose, so we pin it silently. Otherwise we offer the
|
|
826
|
+
* shared branch picker (the same "+ Create a new branch…" + list as `neonctl checkout`),
|
|
827
|
+
* creating the branch when the user opts to. This makes interactive `link` a full org →
|
|
828
|
+
* project → branch flow; non-interactive `link` instead defers the branch to `checkout`.
|
|
829
|
+
*/
|
|
945
830
|
const resolveInteractiveBranch = async (props, projectId) => {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
}
|
|
964
|
-
// A freshly-created branch: we already know the name the user typed.
|
|
965
|
-
await createBranch(props.apiClient, projectId, picked.name, branches);
|
|
966
|
-
return picked.name;
|
|
831
|
+
const { data } = await props.apiClient.listProjectBranches({ projectId });
|
|
832
|
+
const branches = data.branches;
|
|
833
|
+
if (branches.length <= 1) {
|
|
834
|
+
const only = branches.find((b) => b.default) ?? branches[0];
|
|
835
|
+
if (!only) throw new Error(`Could not find a default branch for project ${projectId}.`);
|
|
836
|
+
return branchPersistValue(only);
|
|
837
|
+
}
|
|
838
|
+
const picked = await pickBranchInteractively(branches, {
|
|
839
|
+
message: "Which branch would you like to link?",
|
|
840
|
+
nonInteractiveMessage: `No branch could be selected without an interactive terminal. Re-run \`${getCliName()} link\` interactively, or \`${getCliName()} checkout <branch>\` to pin one.`
|
|
841
|
+
});
|
|
842
|
+
if (picked.kind === "existing") {
|
|
843
|
+
const existing = branches.find((b) => b.id === picked.branchId);
|
|
844
|
+
return existing ? branchPersistValue(existing) : picked.branchId;
|
|
845
|
+
}
|
|
846
|
+
await createBranch(props.apiClient, projectId, picked.name, branches);
|
|
847
|
+
return picked.name;
|
|
967
848
|
};
|
|
968
849
|
const fetchRegions = async (props) => {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
981
|
-
log.debug("getActiveRegions failed (%s), falling back to the static region list.", message);
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
return staticRegionsFallback();
|
|
850
|
+
try {
|
|
851
|
+
const { data } = await props.apiClient.getActiveRegions();
|
|
852
|
+
if (data.regions && data.regions.length > 0) return data.regions;
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (isNeonApiError(err)) log.debug("getActiveRegions failed (%s), falling back to the static region list.", err.status ?? err.code ?? err.message);
|
|
855
|
+
else {
|
|
856
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
857
|
+
log.debug("getActiveRegions failed (%s), falling back to the static region list.", message);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return staticRegionsFallback();
|
|
985
861
|
};
|
|
986
862
|
const staticRegionsFallback = () => REGIONS.map((id) => ({
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
863
|
+
region_id: id,
|
|
864
|
+
name: id,
|
|
865
|
+
default: id === "aws-us-east-2",
|
|
866
|
+
geo_lat: "",
|
|
867
|
+
geo_long: ""
|
|
992
868
|
}));
|
|
993
869
|
const createProject = async (props, args) => {
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
branchName: data.branch.name ?? data.branch.id,
|
|
1012
|
-
};
|
|
870
|
+
const project = {
|
|
871
|
+
name: args.name,
|
|
872
|
+
region_id: args.regionId,
|
|
873
|
+
org_id: args.orgId,
|
|
874
|
+
branch: {}
|
|
875
|
+
};
|
|
876
|
+
const { data } = await props.apiClient.createProject({ project });
|
|
877
|
+
if (!data.branch?.id) throw new Error("Project was created but the API response did not include a default branch id.");
|
|
878
|
+
return {
|
|
879
|
+
project: {
|
|
880
|
+
id: data.project.id,
|
|
881
|
+
name: data.project.name,
|
|
882
|
+
region_id: data.project.region_id
|
|
883
|
+
},
|
|
884
|
+
branchId: data.branch.id,
|
|
885
|
+
branchName: data.branch.name ?? data.branch.id
|
|
886
|
+
};
|
|
1013
887
|
};
|
|
1014
888
|
const printSummary = (_props, summary) => {
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
lines.push("");
|
|
1031
|
-
lines.push("Written offline (--no-checks): nothing was verified.");
|
|
1032
|
-
}
|
|
1033
|
-
else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
1034
|
-
lines.push("");
|
|
1035
|
-
lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
1036
|
-
}
|
|
1037
|
-
lines.push("");
|
|
1038
|
-
process.stdout.write(`${lines.join("\n")}\n`);
|
|
889
|
+
const lines = [];
|
|
890
|
+
if (summary.created) lines.push(`Created project ${summary.projectId}${summary.projectName ? ` ("${summary.projectName}")` : ""}${summary.regionId ? ` in ${summary.regionId}` : ""}.`);
|
|
891
|
+
lines.push(`${summary.orgOnly ? "Updated" : "Linked"} ${summary.contextFile}:`);
|
|
892
|
+
if (summary.orgId) lines.push(` orgId: ${summary.orgId}`);
|
|
893
|
+
if (summary.projectId) lines.push(` projectId: ${summary.projectId}`);
|
|
894
|
+
if (summary.branch) lines.push(` branch: ${summary.branch}`);
|
|
895
|
+
if (summary.noChecks) {
|
|
896
|
+
lines.push("");
|
|
897
|
+
lines.push("Written offline (--no-checks): nothing was verified.");
|
|
898
|
+
} else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
899
|
+
lines.push("");
|
|
900
|
+
lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
901
|
+
}
|
|
902
|
+
lines.push("");
|
|
903
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
1039
904
|
};
|
|
1040
905
|
/**
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
906
|
+
* Print the link summary, then run the bundled `env pull` so a human `link` that pinned a
|
|
907
|
+
* branch ends with the branch's connection string already on disk. When no branch was pinned
|
|
908
|
+
* there is nothing to pull, so env pull is skipped and the summary nudges `checkout` instead.
|
|
909
|
+
* `--no-env-pull` opts out (env pull's own status / skip hint is logged to stderr).
|
|
910
|
+
*/
|
|
1046
911
|
const finalizeLink = async (props, summary) => {
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
envPull: props.envPull,
|
|
1056
|
-
});
|
|
912
|
+
printSummary(props, summary);
|
|
913
|
+
if (!summary.branch || !summary.projectId) return;
|
|
914
|
+
await autoPullEnvAfterPin({
|
|
915
|
+
...props,
|
|
916
|
+
projectId: summary.projectId,
|
|
917
|
+
branch: summary.branch,
|
|
918
|
+
envPull: props.envPull
|
|
919
|
+
});
|
|
1057
920
|
};
|
|
1058
921
|
/**
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
922
|
+
* Interactive `link` finalize: the shared {@link finalizeLink} (summary + env
|
|
923
|
+
* pull), then — as the last step — offer to manage the project's Neon setup as
|
|
924
|
+
* code with a `neon.ts`. Kept out of {@link finalizeLink} so the non-interactive
|
|
925
|
+
* paths never prompt.
|
|
926
|
+
*/
|
|
1064
927
|
const finalizeInteractiveLink = async (props, summary) => {
|
|
1065
|
-
|
|
1066
|
-
|
|
928
|
+
await finalizeLink(props, summary);
|
|
929
|
+
await maybeOfferConfigInit(props, summary);
|
|
1067
930
|
};
|
|
1068
931
|
/**
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
932
|
+
* Offer to set up infrastructure-as-code at the end of an interactive `link` —
|
|
933
|
+
* the natural moment, since the project is now linked. Skipped when the project
|
|
934
|
+
* already has a `neon.ts` (nothing to scaffold). On yes, `config init` writes the
|
|
935
|
+
* starter `neon.ts` and installs the config packages, then env is pulled again so
|
|
936
|
+
* the local `.env` reflects the policy — the same pull `link` runs when a project
|
|
937
|
+
* already ships a `neon.ts`.
|
|
938
|
+
*/
|
|
1076
939
|
const maybeOfferConfigInit = async (props, summary) => {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
...props,
|
|
1098
|
-
projectId: summary.projectId,
|
|
1099
|
-
branch: summary.branch,
|
|
1100
|
-
envPull: props.envPull,
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
940
|
+
const cwd = process.cwd();
|
|
941
|
+
if (hasNeonConfigFile(cwd)) return;
|
|
942
|
+
const { value } = await prompts({
|
|
943
|
+
onState: onPromptState,
|
|
944
|
+
type: "confirm",
|
|
945
|
+
name: "value",
|
|
946
|
+
message: "Manage this project's Neon setup as code? Adds a neon.ts you can edit and apply with `neon config apply`.",
|
|
947
|
+
initial: true
|
|
948
|
+
});
|
|
949
|
+
if (value !== true) return;
|
|
950
|
+
await initCmd({
|
|
951
|
+
cwd,
|
|
952
|
+
install: true
|
|
953
|
+
});
|
|
954
|
+
if (summary.branch && summary.projectId) await autoPullEnvAfterPin({
|
|
955
|
+
...props,
|
|
956
|
+
projectId: summary.projectId,
|
|
957
|
+
branch: summary.branch,
|
|
958
|
+
envPull: props.envPull
|
|
959
|
+
});
|
|
1103
960
|
};
|
|
1104
961
|
const onPromptState = (state) => {
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
962
|
+
if (state.aborted) {
|
|
963
|
+
process.stdout.write("\x1B[?25h");
|
|
964
|
+
process.stdout.write("\n");
|
|
965
|
+
process.exit(1);
|
|
966
|
+
}
|
|
1110
967
|
};
|
|
1111
968
|
const shellArg = (value) => {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
}
|
|
1115
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
969
|
+
if (/^[A-Za-z0-9._:/-]+$/.test(value)) return value;
|
|
970
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
1116
971
|
};
|
|
1117
972
|
const mustString = (value, name) => {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
}
|
|
1121
|
-
return value;
|
|
973
|
+
if (value === void 0) throw new Error(`Internal error: expected ${name} to be set.`);
|
|
974
|
+
return value;
|
|
1122
975
|
};
|
|
976
|
+
//#endregion
|
|
977
|
+
export { builder, command, describe, handler, link_exports as t };
|