neon 8.11.1 → 8.11.2
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 +1 -1
- package/dist/_chunks/api-GbEaBZJ5.js +388 -0
- package/dist/_chunks/api_keys-CNnnugbQ.js +281 -0
- package/dist/_chunks/ask-DVwy0eUL.js +155 -0
- package/dist/_chunks/auth-CIUtXByB.js +495 -0
- package/dist/_chunks/bootstrap-wPZWa1sT.js +499 -0
- package/dist/_chunks/branches-BNa1xX1L.js +460 -0
- package/dist/_chunks/bucket-BCdryTkD.js +409 -0
- package/dist/_chunks/checkout-CXIOydqZ.js +348 -0
- package/dist/_chunks/claim-DqAvAvDm.js +473 -0
- package/dist/_chunks/config-BH_qRHiG.js +1113 -0
- package/dist/_chunks/connection_string-DB4awPzZ.js +154 -0
- package/dist/_chunks/credentials-BSjE9fRR.js +146 -0
- package/dist/_chunks/data_api-Djxj5ZQD.js +263 -0
- package/dist/_chunks/databases-Dok4wru7.js +84 -0
- package/dist/_chunks/deploy-D7B0Xfjg.js +33 -0
- package/dist/_chunks/dev-CSvxAecE.js +945 -0
- package/dist/_chunks/diff-DhxxbdQE.js +178 -0
- package/dist/_chunks/docs-CrVybkTP.js +197 -0
- package/dist/_chunks/env-5HS5qQGd.js +284 -0
- package/dist/_chunks/feedback-CMayyTa8.js +88 -0
- package/dist/_chunks/functions-Q0zuvtnk.js +464 -0
- package/dist/_chunks/git-H32TNt6M.js +356 -0
- package/dist/_chunks/init-BkA6RilG.js +111 -0
- package/dist/_chunks/inspect-g0wekylj.js +90 -0
- package/dist/_chunks/ip_allow-5lLFvIuE.js +106 -0
- package/dist/_chunks/link-BgtkOBws.js +936 -0
- package/dist/_chunks/logs-DWkpjzIH.js +291 -0
- package/dist/_chunks/mcp-Bo-7TKc7.js +253 -0
- package/dist/_chunks/neon_auth-CBs1_3qu.js +822 -0
- package/dist/_chunks/open-CX3BT3Tp.js +33 -0
- package/dist/_chunks/operations-D4I-AxVs.js +37 -0
- package/dist/_chunks/orgs-xGfANwHQ.js +52 -0
- package/dist/_chunks/plugins-DqD7BDO1.js +169 -0
- package/dist/_chunks/profile-yXOiKk9R.js +672 -0
- package/dist/_chunks/projects-BwI2Uo0z.js +439 -0
- package/dist/_chunks/psql-BM-doe8e.js +64 -0
- package/dist/_chunks/realtime-BRqIyGR-.js +79 -0
- package/dist/_chunks/roles-B4w3YT_7.js +79 -0
- package/dist/_chunks/set_context-DbrwoImy.js +38 -0
- package/dist/_chunks/skills-DyiBXKS3.js +255 -0
- package/dist/_chunks/snapshots-RJBzW8GM.js +338 -0
- package/dist/_chunks/status-DV8Z2kw3.js +44 -0
- package/dist/_chunks/triggers-BrRo1z4b.js +302 -0
- package/dist/_chunks/user-dlOGXbX9.js +60 -0
- package/dist/_chunks/vpc_endpoints-CNb0YfMo.js +105 -0
- package/dist/auth_recovery.js +1 -1
- package/dist/commands/api.js +1 -388
- package/dist/commands/api_keys.js +1 -281
- package/dist/commands/ask.js +1 -155
- package/dist/commands/auth.js +1 -495
- package/dist/commands/bootstrap.js +1 -499
- package/dist/commands/branches.js +1 -460
- package/dist/commands/bucket.js +1 -409
- package/dist/commands/checkout.js +1 -348
- package/dist/commands/claim.js +1 -473
- package/dist/commands/config.js +1 -1113
- package/dist/commands/connection_string.js +1 -154
- package/dist/commands/credentials.js +1 -146
- package/dist/commands/data_api.js +1 -263
- package/dist/commands/databases.js +1 -84
- package/dist/commands/deploy.js +1 -33
- package/dist/commands/dev.js +1 -945
- package/dist/commands/diff.js +1 -178
- package/dist/commands/docs.js +1 -197
- package/dist/commands/env.js +1 -284
- package/dist/commands/feedback.js +1 -88
- package/dist/commands/functions.js +1 -464
- package/dist/commands/functions_new.js +1 -1
- package/dist/commands/git.js +1 -356
- package/dist/commands/index.js +45 -45
- package/dist/commands/init.js +1 -111
- package/dist/commands/inspect.js +1 -90
- package/dist/commands/ip_allow.js +1 -106
- package/dist/commands/link.js +1 -936
- package/dist/commands/logs.js +1 -291
- package/dist/commands/mcp.js +1 -253
- package/dist/commands/neon_auth.js +1 -822
- package/dist/commands/open.js +1 -33
- package/dist/commands/operations.js +1 -37
- package/dist/commands/orgs.js +1 -52
- package/dist/commands/plugins.js +1 -169
- package/dist/commands/profile.js +1 -672
- package/dist/commands/projects.js +1 -439
- package/dist/commands/psql.js +1 -64
- package/dist/commands/realtime.js +1 -79
- package/dist/commands/roles.js +1 -79
- package/dist/commands/set_context.js +1 -38
- package/dist/commands/skills.js +1 -255
- package/dist/commands/snapshots.js +1 -338
- package/dist/commands/status.js +1 -44
- package/dist/commands/subscribe.js +1 -1
- package/dist/commands/triggers.js +1 -302
- package/dist/commands/user.js +1 -60
- package/dist/commands/vpc_endpoints.js +1 -105
- package/dist/functions/registry.js +29 -151
- package/dist/functions/source.js +42 -2
- package/dist/index.js +1 -1
- package/dist/init/auth.js +3 -3
- package/dist/init/link.js +2 -2
- package/dist/init/run.js +3 -3
- package/dist/init/tooling.js +3 -3
- package/dist/mcp/mint.js +1 -1
- package/dist/test_utils/registry.js +1 -2
- package/package.json +8 -6
package/dist/commands/link.js
CHANGED
|
@@ -1,936 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { isInteractiveTerminal } from "../env.js";
|
|
3
|
-
import { log } from "../log.js";
|
|
4
|
-
import { isNeonApiError, messageFromBody } from "../api.js";
|
|
5
|
-
import { r as defaultDir } from "../_chunks/paths-DMq0Lt7a.js";
|
|
6
|
-
import "../config.js";
|
|
7
|
-
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile } from "../context.js";
|
|
8
|
-
import { isClaimableEnvTarget } from "../claimable/state.js";
|
|
9
|
-
import { getCliName } from "../utils/cli_name.js";
|
|
10
|
-
import { helpEpilogue } from "../utils/help_text.js";
|
|
11
|
-
import { hasNeonConfigFile, projectDir } from "../project.js";
|
|
12
|
-
import { writer } from "../writer.js";
|
|
13
|
-
import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
|
|
14
|
-
import { looksLikeBranchId } from "../utils/formats.js";
|
|
15
|
-
import { listAllProjectBranches } from "../utils/enrichers.js";
|
|
16
|
-
import { autoPullEnvAfterPin } from "./env.js";
|
|
17
|
-
import { initCmd } from "./config.js";
|
|
18
|
-
import { formatLinkSummary } from "./link_output.js";
|
|
19
|
-
import { REGIONS } from "./projects.js";
|
|
20
|
-
import prompts from "prompts";
|
|
21
|
-
//#region src/commands/link.ts
|
|
22
|
-
var link_exports = /* @__PURE__ */ __exportAll({
|
|
23
|
-
builder: () => builder,
|
|
24
|
-
command: () => command,
|
|
25
|
-
describe: () => describe,
|
|
26
|
-
handler: () => handler,
|
|
27
|
-
quoteFlagValue: () => quoteFlagValue,
|
|
28
|
-
runLink: () => runLink,
|
|
29
|
-
shouldOfferConfigInit: () => shouldOfferConfigInit
|
|
30
|
-
});
|
|
31
|
-
const PROJECTS_LIST_LIMIT = 100;
|
|
32
|
-
const CREATE_NEW_SENTINEL = "__create_new__";
|
|
33
|
-
const canPromptInteractively = () => isInteractiveTerminal();
|
|
34
|
-
const nonInteractiveLinkCommands = () => {
|
|
35
|
-
const cli = getCliName();
|
|
36
|
-
return [
|
|
37
|
-
`${cli} link -y`,
|
|
38
|
-
`${cli} orgs list --output json`,
|
|
39
|
-
`${cli} projects list --org-id <org-id> --output json`,
|
|
40
|
-
`${cli} link --project-id <project-id> [--branch <name> | -y]`,
|
|
41
|
-
`${cli} link --org-id <org-id> --project-name <name> --region-id aws-us-east-2`
|
|
42
|
-
];
|
|
43
|
-
};
|
|
44
|
-
const nonInteractiveLinkHelp = () => nonInteractiveLinkCommands().map((command) => ` ${command}`).join("\n");
|
|
45
|
-
const orgScopedKeyHint = "Organization-scoped API keys cannot list orgs; pass --org-id.";
|
|
46
|
-
const command = "link";
|
|
47
|
-
const describe = "Link the current directory to a Neon project";
|
|
48
|
-
const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
49
|
-
"org-id": {
|
|
50
|
-
describe: "Organization ID to link to",
|
|
51
|
-
type: "string"
|
|
52
|
-
},
|
|
53
|
-
"project-id": {
|
|
54
|
-
describe: "Existing project ID to link to",
|
|
55
|
-
type: "string"
|
|
56
|
-
},
|
|
57
|
-
"project-name": {
|
|
58
|
-
describe: "Name for a new project to create and link to",
|
|
59
|
-
type: "string"
|
|
60
|
-
},
|
|
61
|
-
"region-id": {
|
|
62
|
-
describe: "Region ID for a new project (e.g. aws-us-east-2). Required with --project-name.",
|
|
63
|
-
type: "string"
|
|
64
|
-
},
|
|
65
|
-
branch: {
|
|
66
|
-
alias: "branch-id",
|
|
67
|
-
describe: "Branch name or ID to pin in the context (resolved to its name before writing).",
|
|
68
|
-
type: "string"
|
|
69
|
-
},
|
|
70
|
-
params: {
|
|
71
|
-
describe: "JSON object with link parameters, e.g. '{\"orgId\":\"...\",\"projectId\":\"...\"}' or '{\"orgId\":\"...\",\"projectName\":\"...\",\"regionId\":\"...\"}'. Flags take precedence over fields in --params.",
|
|
72
|
-
type: "string"
|
|
73
|
-
},
|
|
74
|
-
yes: {
|
|
75
|
-
alias: "y",
|
|
76
|
-
describe: "Skip prompts. Select the only organization and project, or print IDs and the flag to pass. Pin the default branch when several exist. Does not create a project unless --project-name and --region-id are set.",
|
|
77
|
-
type: "boolean",
|
|
78
|
-
default: false
|
|
79
|
-
},
|
|
80
|
-
clear: {
|
|
81
|
-
describe: "Remove the org/project/branch context (writes an empty context file) instead of linking.",
|
|
82
|
-
type: "boolean",
|
|
83
|
-
default: false
|
|
84
|
-
},
|
|
85
|
-
checks: {
|
|
86
|
-
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, --project-id, and --branch, and skips env pull.",
|
|
87
|
-
type: "boolean",
|
|
88
|
-
default: true
|
|
89
|
-
},
|
|
90
|
-
"env-pull": {
|
|
91
|
-
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.",
|
|
92
|
-
type: "boolean",
|
|
93
|
-
default: true
|
|
94
|
-
},
|
|
95
|
-
config: {
|
|
96
|
-
describe: "Offer to create neon.ts after interactive linking. Use --no-config to skip the offer",
|
|
97
|
-
type: "boolean",
|
|
98
|
-
default: true
|
|
99
|
-
}
|
|
100
|
-
}).example([
|
|
101
|
-
["$0 link -y", "Select the only organization and project, or print the --org-id / --project-id to pass"],
|
|
102
|
-
["$0 link --project-id polished-snowflake-12345678", "Link an existing project (org is inferred). Pins the only branch; several prompt in a TTY, or -y pins the default"],
|
|
103
|
-
["$0 link --project-id polished-snowflake-12345678 -y", "Same, pinning the project's default branch when several exist"],
|
|
104
|
-
["$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2", "Create a new project and link it"],
|
|
105
|
-
["$0 link --branch-id br-…", "Pin a branch in the already-linked project"],
|
|
106
|
-
["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678 --branch main", "Write the context offline (no API calls, no verification)"],
|
|
107
|
-
["$0 link --clear", "Forget the current org/project/branch context"]
|
|
108
|
-
]).epilogue(helpEpilogue("Non-interactive (CI, scripts, agents):", ...nonInteractiveLinkCommands().map((command) => ` ${command}`), orgScopedKeyHint)).strict();
|
|
109
|
-
const runLink = async (props) => {
|
|
110
|
-
if (props.clear) {
|
|
111
|
-
clearContext(props.contextFile);
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (!props.checks) {
|
|
115
|
-
runWithoutChecks(props);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
const inputs = parseInputs(props);
|
|
119
|
-
validateInputs(inputs);
|
|
120
|
-
const existing = readContextFile(props.contextFile);
|
|
121
|
-
if (props.yes && hasIncompleteCreationInputs(inputs)) throw incompleteCreationError(props, inputs);
|
|
122
|
-
if (canResolveNonInteractively(inputs, existing)) {
|
|
123
|
-
await runNonInteractive(props, inputs, existing);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
if (props.yes) {
|
|
127
|
-
const resolved = await resolveYesInputs(props, inputs);
|
|
128
|
-
await runNonInteractive(props, resolved, existing);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
if (!canPromptInteractively()) {
|
|
132
|
-
if (missingProjectForOrg(inputs)) throw orgNeedsProjectError(props, inputs);
|
|
133
|
-
throw new LinkInputError([
|
|
134
|
-
"Missing inputs and no interactive terminal for prompts.",
|
|
135
|
-
"",
|
|
136
|
-
"Use:",
|
|
137
|
-
nonInteractiveLinkHelp(),
|
|
138
|
-
orgScopedKeyHint
|
|
139
|
-
].join("\n"));
|
|
140
|
-
}
|
|
141
|
-
await runInteractive(props, inputs);
|
|
142
|
-
};
|
|
143
|
-
const handler = runLink;
|
|
144
|
-
const parseInputs = (props) => {
|
|
145
|
-
let fromParams = {};
|
|
146
|
-
if (props.params !== void 0 && props.params !== "") {
|
|
147
|
-
let parsed;
|
|
148
|
-
try {
|
|
149
|
-
parsed = JSON.parse(props.params);
|
|
150
|
-
} catch (err) {
|
|
151
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
-
throw new Error(`Failed to parse --params JSON: ${message}`);
|
|
153
|
-
}
|
|
154
|
-
fromParams = extractParams(parsed);
|
|
155
|
-
}
|
|
156
|
-
return {
|
|
157
|
-
orgId: props.orgId ?? fromParams.orgId,
|
|
158
|
-
projectId: props.projectId ?? fromParams.projectId,
|
|
159
|
-
projectName: props.projectName ?? fromParams.projectName,
|
|
160
|
-
regionId: props.regionId ?? fromParams.regionId,
|
|
161
|
-
branch: props.branch ?? fromParams.branch
|
|
162
|
-
};
|
|
163
|
-
};
|
|
164
|
-
const extractParams = (raw) => {
|
|
165
|
-
if (raw === null || typeof raw !== "object") throw new Error("--params must be a JSON object");
|
|
166
|
-
const obj = raw;
|
|
167
|
-
const pickString = (key) => {
|
|
168
|
-
const value = obj[key];
|
|
169
|
-
if (value === void 0 || value === null) return void 0;
|
|
170
|
-
if (typeof value !== "string") throw new Error(`--params.${key} must be a string`);
|
|
171
|
-
return value;
|
|
172
|
-
};
|
|
173
|
-
return {
|
|
174
|
-
orgId: pickString("orgId"),
|
|
175
|
-
projectId: pickString("projectId"),
|
|
176
|
-
projectName: pickString("projectName"),
|
|
177
|
-
regionId: pickString("regionId"),
|
|
178
|
-
branch: pickString("branch") ?? pickString("branchId")
|
|
179
|
-
};
|
|
180
|
-
};
|
|
181
|
-
const validateInputs = (inputs) => {
|
|
182
|
-
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.");
|
|
183
|
-
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>\`.`);
|
|
184
|
-
};
|
|
185
|
-
const hasIncompleteCreationInputs = (inputs) => Boolean(inputs.projectName) !== Boolean(inputs.regionId);
|
|
186
|
-
const missingProjectForOrg = (inputs) => Boolean(inputs.orgId) && !inputs.projectId && !inputs.projectName;
|
|
187
|
-
const orgNeedsProjectError = (props, inputs) => {
|
|
188
|
-
const orgId = inputs.orgId ?? "<org-id>";
|
|
189
|
-
const branchFlag = inputs.branch ? ` --branch ${quoteFlagValue(inputs.branch)}` : "";
|
|
190
|
-
const session = sessionRetryFlags(props);
|
|
191
|
-
return new LinkInputError([
|
|
192
|
-
"No project selected. Pass --project-id, or use -y to select the only project:",
|
|
193
|
-
` ${getCliName()} link -y --org-id ${quoteFlagValue(orgId)}${branchFlag}${session}`,
|
|
194
|
-
` ${getCliName()} link --org-id ${quoteFlagValue(orgId)} --project-id <project-id>${inputs.branch ? ` --branch ${quoteFlagValue(inputs.branch)}` : " --branch <name-or-id>"}${session}`
|
|
195
|
-
].join("\n"));
|
|
196
|
-
};
|
|
197
|
-
const quoteFlagValue = (value) => {
|
|
198
|
-
if (/^[A-Za-z0-9_./:@-]+$/.test(value)) return value;
|
|
199
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
200
|
-
};
|
|
201
|
-
const sessionRetryFlags = (props) => {
|
|
202
|
-
const flags = [];
|
|
203
|
-
if (props.contextFile) flags.push(`--context-file ${quoteFlagValue(props.contextFile)}`);
|
|
204
|
-
if (props.configDir && props.configDir !== defaultDir) flags.push(`--config-dir ${quoteFlagValue(props.configDir)}`);
|
|
205
|
-
if (props.output !== "table") flags.push(`--output ${props.output}`);
|
|
206
|
-
if (!props.envPull) flags.push("--no-env-pull");
|
|
207
|
-
if (props.profile) flags.push(`--profile ${quoteFlagValue(props.profile)}`);
|
|
208
|
-
return flags.length > 0 ? ` ${flags.join(" ")}` : "";
|
|
209
|
-
};
|
|
210
|
-
const incompleteCreationError = (props, inputs) => {
|
|
211
|
-
const orgFlag = inputs.orgId ? `--org-id ${quoteFlagValue(inputs.orgId)}` : "--org-id <org-id>";
|
|
212
|
-
const nameFlag = inputs.projectName ? `--project-name ${quoteFlagValue(inputs.projectName)}` : "--project-name <name>";
|
|
213
|
-
const regionFlag = inputs.regionId ? `--region-id ${quoteFlagValue(inputs.regionId)}` : "--region-id aws-us-east-2";
|
|
214
|
-
const example = `${getCliName()} link -y ${orgFlag} ${nameFlag} ${regionFlag}${sessionRetryFlags(props)}`;
|
|
215
|
-
if (inputs.projectName) return new LinkInputError(`--project-name requires --region-id. Example:\n ${example}`);
|
|
216
|
-
return new LinkInputError(`--region-id requires --project-name. Example:\n ${example}`);
|
|
217
|
-
};
|
|
218
|
-
/**
|
|
219
|
-
* Branch selection may still prompt after org and project resolve without the
|
|
220
|
-
* org/project wizard.
|
|
221
|
-
*/
|
|
222
|
-
const canResolveNonInteractively = (inputs, existing) => {
|
|
223
|
-
if (inputs.projectId) return true;
|
|
224
|
-
if (inputs.orgId && inputs.projectName && inputs.regionId) return true;
|
|
225
|
-
if (inputs.branch && existing.projectId) return true;
|
|
226
|
-
return false;
|
|
227
|
-
};
|
|
228
|
-
const clearContext = (contextFile) => {
|
|
229
|
-
updateContextFile(contextFile, {});
|
|
230
|
-
process.stdout.write(`Cleared ${contextFile}. The directory is no longer linked to a Neon org/project/branch.\n`);
|
|
231
|
-
};
|
|
232
|
-
/**
|
|
233
|
-
* `--no-checks`: write the context offline. Makes no API calls — so no org
|
|
234
|
-
* inference, no existence/access verification, and no env pull — which means
|
|
235
|
-
* the caller must supply `--org-id`, `--project-id`, and `--branch`. This is
|
|
236
|
-
* the CLI surface over {@link setContext}, useful for scripted/offline setups
|
|
237
|
-
* and for re-creating a `.neon` from values you already trust.
|
|
238
|
-
*/
|
|
239
|
-
const runWithoutChecks = (props) => {
|
|
240
|
-
const inputs = parseInputs(props);
|
|
241
|
-
validateInputs(inputs);
|
|
242
|
-
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.");
|
|
243
|
-
if (!inputs.orgId || !inputs.projectId || !inputs.branch) throw new Error("--no-checks requires --org-id, --project-id, and --branch because identifiers cannot be resolved offline.");
|
|
244
|
-
setContext(props.contextFile, {
|
|
245
|
-
orgId: inputs.orgId,
|
|
246
|
-
projectId: inputs.projectId,
|
|
247
|
-
branch: inputs.branch
|
|
248
|
-
});
|
|
249
|
-
printSummary(props, {
|
|
250
|
-
contextFile: props.contextFile,
|
|
251
|
-
orgId: inputs.orgId,
|
|
252
|
-
projectId: inputs.projectId,
|
|
253
|
-
branch: inputs.branch,
|
|
254
|
-
created: false,
|
|
255
|
-
noChecks: true
|
|
256
|
-
});
|
|
257
|
-
};
|
|
258
|
-
var LinkInputError = class extends Error {
|
|
259
|
-
constructor(message) {
|
|
260
|
-
super(message);
|
|
261
|
-
this.name = "LinkInputError";
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
const httpStatus = (err) => isNeonApiError(err) ? err.status : void 0;
|
|
265
|
-
/** 401 must reach the global handler so it can refresh credentials. */
|
|
266
|
-
const fetchProjectOrThrow = async (props, projectId) => {
|
|
267
|
-
try {
|
|
268
|
-
const { data } = await props.apiClient.getProject(projectId);
|
|
269
|
-
return data.project;
|
|
270
|
-
} catch (err) {
|
|
271
|
-
const status = httpStatus(err);
|
|
272
|
-
if (status === 401) throw err;
|
|
273
|
-
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.`);
|
|
274
|
-
if (status === 404) throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`);
|
|
275
|
-
throw err;
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
/**
|
|
279
|
-
* Confirm the org exists and is reachable with the current API key by listing
|
|
280
|
-
* its projects (allowed for both user and org-scoped keys). Maps 403/404 to a
|
|
281
|
-
* clear message; 401 is rethrown for credential refresh.
|
|
282
|
-
*/
|
|
283
|
-
const verifyOrgAccess = async (props, orgId) => {
|
|
284
|
-
try {
|
|
285
|
-
await props.apiClient.listProjects({
|
|
286
|
-
org_id: orgId,
|
|
287
|
-
limit: PROJECTS_LIST_LIMIT
|
|
288
|
-
});
|
|
289
|
-
} catch (err) {
|
|
290
|
-
const status = httpStatus(err);
|
|
291
|
-
if (status === 401) throw err;
|
|
292
|
-
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.`);
|
|
293
|
-
throw err;
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
const failWithBranchCandidates = (props, projectId, branches, reason) => {
|
|
297
|
-
writer(props).end(branches.map((b) => ({
|
|
298
|
-
id: b.id,
|
|
299
|
-
name: b.name ?? b.id
|
|
300
|
-
})), {
|
|
301
|
-
fields: ["id", "name"],
|
|
302
|
-
title: "Branches"
|
|
303
|
-
});
|
|
304
|
-
const orgFlag = props.orgId ? ` --org-id ${quoteFlagValue(props.orgId)}` : "";
|
|
305
|
-
throw new LinkInputError(`${reason}\n ${getCliName()} link -y${orgFlag} --project-id ${quoteFlagValue(projectId)} --branch <name-or-id>${sessionRetryFlags(props)}`);
|
|
306
|
-
};
|
|
307
|
-
const listAllBranches = async (props, projectId) => listAllProjectBranches(props.apiClient, projectId);
|
|
308
|
-
/**
|
|
309
|
-
* The branch listing for {@link resolvePinnedBranch}, started alongside the project check
|
|
310
|
-
* except on Claimable Neon, which does not complete concurrent Management API reads. Only
|
|
311
|
-
* the listing starts early: branch selection, which can prompt or create a branch, still
|
|
312
|
-
* waits for the check, and a listing error is reported only after the check passes.
|
|
313
|
-
*/
|
|
314
|
-
const branchListing = (props, projectId) => {
|
|
315
|
-
if (isClaimableEnvTarget({
|
|
316
|
-
apiHost: props.apiHost,
|
|
317
|
-
contextFile: props.contextFile,
|
|
318
|
-
configDir: props.configDir ?? ""
|
|
319
|
-
})) return () => listAllBranches(props, projectId);
|
|
320
|
-
const listing = listAllBranches(props, projectId);
|
|
321
|
-
listing.catch(() => void 0);
|
|
322
|
-
return () => listing;
|
|
323
|
-
};
|
|
324
|
-
/**
|
|
325
|
-
* Resolve a branch reference (name *or* id) to the matching branch, while
|
|
326
|
-
* confirming it actually exists in the project. Unlike the shared
|
|
327
|
-
* `branchIdResolve`, this also verifies references that already look like ids
|
|
328
|
-
* (so a typo'd `br-…` doesn't silently get written).
|
|
329
|
-
*/
|
|
330
|
-
const resolveBranchRef = (props, projectId, branchRef, branches) => {
|
|
331
|
-
const match = branches.find((b) => b.id === branchRef) ?? branches.find((b) => b.name === branchRef);
|
|
332
|
-
if (match) return match;
|
|
333
|
-
return failWithBranchCandidates(props, projectId, branches, `Branch '${branchRef}' not found in project '${projectId}'. Pass --branch with a name or ID from the list:`);
|
|
334
|
-
};
|
|
335
|
-
/**
|
|
336
|
-
* Persist the name when later commands will look it up as a name. A name that
|
|
337
|
-
* already looks like a branch id is trusted as an id without listing, so keep
|
|
338
|
-
* the real id in that case.
|
|
339
|
-
*/
|
|
340
|
-
const branchPersistValue = (branch) => {
|
|
341
|
-
if (branch.name && !looksLikeBranchId(branch.name)) return branch.name;
|
|
342
|
-
return branch.id;
|
|
343
|
-
};
|
|
344
|
-
/**
|
|
345
|
-
* Verify the project (and the org, when supplied) and resolve the org id to
|
|
346
|
-
* persist.
|
|
347
|
-
*
|
|
348
|
-
* The project is always fetched, which both validates it and yields its
|
|
349
|
-
* `org_id`. When `--org-id` is passed too: if the project reports an org it must
|
|
350
|
-
* match (else a clear mismatch error); if it reports none, fail rather than
|
|
351
|
-
* attaching an unrelated org. Without `--org-id` the project's own org is used.
|
|
352
|
-
* Projects on a personal account have no org, so `undefined` is a valid result —
|
|
353
|
-
* the field is simply omitted.
|
|
354
|
-
*/
|
|
355
|
-
const resolveOrgForProject = async (props, inputs, projectId) => {
|
|
356
|
-
const project = await fetchProjectOrThrow(props, projectId);
|
|
357
|
-
const projectOrg = project.org_id ?? void 0;
|
|
358
|
-
const projectName = project.name;
|
|
359
|
-
if (inputs.orgId) {
|
|
360
|
-
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.`);
|
|
361
|
-
if (!projectOrg) throw new LinkInputError(`Project '${projectId}' does not report an organization matching --org-id ${inputs.orgId}. Omit --org-id to link using the project's own context.`);
|
|
362
|
-
return {
|
|
363
|
-
orgId: inputs.orgId,
|
|
364
|
-
projectName
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
orgId: projectOrg,
|
|
369
|
-
projectName
|
|
370
|
-
};
|
|
371
|
-
};
|
|
372
|
-
const pinnedRef = (branch) => ({
|
|
373
|
-
branchId: branch.id,
|
|
374
|
-
branchName: branch.name ?? branch.id,
|
|
375
|
-
usedDefault: false
|
|
376
|
-
});
|
|
377
|
-
const resolution = (branch) => ({
|
|
378
|
-
branch: branchPersistValue(branch),
|
|
379
|
-
ref: pinnedRef(branch)
|
|
380
|
-
});
|
|
381
|
-
/**
|
|
382
|
-
* Keep a same-project pin only after it still resolves on the project.
|
|
383
|
-
*/
|
|
384
|
-
const resolvePinnedBranch = async (props, inputs, existing, projectId, listBranches) => {
|
|
385
|
-
const branches = await listBranches();
|
|
386
|
-
if (inputs.branch) return resolution(resolveBranchRef(props, projectId, inputs.branch, branches));
|
|
387
|
-
if (projectId === existing.projectId) {
|
|
388
|
-
const pinned = contextBranch(existing);
|
|
389
|
-
if (pinned) return resolution(resolveBranchRef(props, projectId, pinned, branches));
|
|
390
|
-
}
|
|
391
|
-
return resolveBranchFromList(props, projectId, branches);
|
|
392
|
-
};
|
|
393
|
-
const resolveBranchFromList = async (props, projectId, branches) => {
|
|
394
|
-
if (branches.length === 0) throw new LinkInputError(`Project '${projectId}' has no branches to link. Create or restore a branch before retrying, or select another project with --project-id.`);
|
|
395
|
-
if (branches.length === 1) {
|
|
396
|
-
const [only] = branches;
|
|
397
|
-
if (!only) throw new LinkInputError(`Project '${projectId}' has no branches to link. Create or restore a branch before retrying, or select another project with --project-id.`);
|
|
398
|
-
return resolution(only);
|
|
399
|
-
}
|
|
400
|
-
if (props.yes) {
|
|
401
|
-
const def = branches.find((b) => b.default);
|
|
402
|
-
if (!def) return failWithBranchCandidates(props, projectId, branches, `Project '${projectId}' has no default branch. Pass --branch with a name or ID from the list:`);
|
|
403
|
-
return resolution(def);
|
|
404
|
-
}
|
|
405
|
-
if (canPromptInteractively()) {
|
|
406
|
-
const picked = await pickBranchInteractively(branches, {
|
|
407
|
-
message: "Which branch would you like to link?",
|
|
408
|
-
nonInteractiveMessage: "No branch could be selected without an interactive terminal. Pass --branch <name-or-id>, or -y to pin the default branch."
|
|
409
|
-
});
|
|
410
|
-
if (picked.kind === "existing") {
|
|
411
|
-
const existing = branches.find((b) => b.id === picked.branchId);
|
|
412
|
-
return existing ? resolution(existing) : {
|
|
413
|
-
branch: picked.branchId,
|
|
414
|
-
ref: pinnedRef({ id: picked.branchId })
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
const createdBranch = {
|
|
418
|
-
id: await createBranch(props.apiClient, projectId, picked.name, branches),
|
|
419
|
-
name: picked.name
|
|
420
|
-
};
|
|
421
|
-
return {
|
|
422
|
-
branch: branchPersistValue(createdBranch),
|
|
423
|
-
ref: pinnedRef(createdBranch)
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
return failWithBranchCandidates(props, projectId, branches, `Project '${projectId}' has multiple branches. Pass --branch <name-or-id>, or -y to pin its default branch.`);
|
|
427
|
-
};
|
|
428
|
-
const runNonInteractive = async (props, inputs, existing) => {
|
|
429
|
-
if (inputs.projectName) {
|
|
430
|
-
const orgId = mustString(inputs.orgId, "orgId");
|
|
431
|
-
await verifyOrgAccess(props, orgId);
|
|
432
|
-
const created = await createProject(props, {
|
|
433
|
-
orgId,
|
|
434
|
-
name: inputs.projectName,
|
|
435
|
-
regionId: mustString(inputs.regionId, "regionId")
|
|
436
|
-
});
|
|
437
|
-
applyContext(props.contextFile, {
|
|
438
|
-
orgId,
|
|
439
|
-
projectId: created.project.id,
|
|
440
|
-
branch: created.branchName
|
|
441
|
-
});
|
|
442
|
-
await finalizeLink(props, {
|
|
443
|
-
contextFile: props.contextFile,
|
|
444
|
-
orgId,
|
|
445
|
-
projectId: created.project.id,
|
|
446
|
-
branch: created.branchName,
|
|
447
|
-
branchRef: pinnedRef({
|
|
448
|
-
id: created.branchId,
|
|
449
|
-
name: created.branchName
|
|
450
|
-
}),
|
|
451
|
-
created: true,
|
|
452
|
-
projectName: created.project.name,
|
|
453
|
-
regionId: created.project.region_id
|
|
454
|
-
});
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
if (inputs.projectId) {
|
|
458
|
-
const listBranches = branchListing(props, inputs.projectId);
|
|
459
|
-
const { orgId, projectName } = await resolveOrgForProject(props, inputs, inputs.projectId);
|
|
460
|
-
const resolved = await resolvePinnedBranch(props, inputs, existing, inputs.projectId, listBranches);
|
|
461
|
-
applyContext(props.contextFile, {
|
|
462
|
-
orgId,
|
|
463
|
-
projectId: inputs.projectId,
|
|
464
|
-
branch: resolved.branch
|
|
465
|
-
});
|
|
466
|
-
await finalizeLink(props, {
|
|
467
|
-
contextFile: props.contextFile,
|
|
468
|
-
orgId,
|
|
469
|
-
projectId: inputs.projectId,
|
|
470
|
-
projectName,
|
|
471
|
-
branch: resolved.branch,
|
|
472
|
-
branchRef: resolved.ref,
|
|
473
|
-
created: false
|
|
474
|
-
});
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
if (inputs.branch && existing.projectId) {
|
|
478
|
-
const projectId = existing.projectId;
|
|
479
|
-
const listBranches = branchListing(props, projectId);
|
|
480
|
-
const { orgId, projectName } = await resolveOrgForProject(props, inputs, projectId);
|
|
481
|
-
const resolved = await resolvePinnedBranch(props, inputs, existing, projectId, listBranches);
|
|
482
|
-
applyContext(props.contextFile, {
|
|
483
|
-
orgId,
|
|
484
|
-
projectId,
|
|
485
|
-
branch: resolved.branch
|
|
486
|
-
});
|
|
487
|
-
await finalizeLink(props, {
|
|
488
|
-
contextFile: props.contextFile,
|
|
489
|
-
orgId,
|
|
490
|
-
projectId,
|
|
491
|
-
projectName,
|
|
492
|
-
branch: resolved.branch,
|
|
493
|
-
branchRef: resolved.ref,
|
|
494
|
-
created: false
|
|
495
|
-
});
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
throw orgNeedsProjectError(props, inputs);
|
|
499
|
-
};
|
|
500
|
-
const runInteractive = async (props, inputs) => {
|
|
501
|
-
if (!props.yes) {
|
|
502
|
-
if (!await confirmRelinkIfNeeded(props)) return;
|
|
503
|
-
}
|
|
504
|
-
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
505
|
-
let orgId;
|
|
506
|
-
if (orgResolution.kind === "resolved") {
|
|
507
|
-
orgId = orgResolution.orgId;
|
|
508
|
-
if (orgResolution.autoDetected) log.info(`Detected organization ${orgId} from your existing projects (organization-scoped API key).`);
|
|
509
|
-
} 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).");
|
|
510
|
-
else orgId = await promptOrgFromList(orgResolution.orgs);
|
|
511
|
-
if (inputs.projectName && inputs.regionId) {
|
|
512
|
-
if (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>\`.`);
|
|
513
|
-
const created = await createProject(props, {
|
|
514
|
-
orgId,
|
|
515
|
-
name: inputs.projectName,
|
|
516
|
-
regionId: inputs.regionId
|
|
517
|
-
});
|
|
518
|
-
applyContext(props.contextFile, {
|
|
519
|
-
orgId,
|
|
520
|
-
projectId: created.project.id,
|
|
521
|
-
branch: created.branchName
|
|
522
|
-
});
|
|
523
|
-
await finalizeInteractiveLink(props, {
|
|
524
|
-
contextFile: props.contextFile,
|
|
525
|
-
orgId,
|
|
526
|
-
projectId: created.project.id,
|
|
527
|
-
branch: created.branchName,
|
|
528
|
-
branchRef: pinnedRef({
|
|
529
|
-
id: created.branchId,
|
|
530
|
-
name: created.branchName
|
|
531
|
-
}),
|
|
532
|
-
created: true,
|
|
533
|
-
projectName: created.project.name,
|
|
534
|
-
regionId: created.project.region_id
|
|
535
|
-
});
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
const projects = await listAllProjects(props, orgId);
|
|
539
|
-
const action = await promptProjectChoice(projects, inputs.projectName, { allowCreate: !inputs.branch });
|
|
540
|
-
if (action.type === "existing") {
|
|
541
|
-
const existing = readContextFile(props.contextFile);
|
|
542
|
-
const resolved = await resolvePinnedBranch(props, inputs, existing, action.projectId, () => listAllBranches(props, action.projectId));
|
|
543
|
-
applyContext(props.contextFile, {
|
|
544
|
-
orgId,
|
|
545
|
-
projectId: action.projectId,
|
|
546
|
-
branch: resolved.branch
|
|
547
|
-
});
|
|
548
|
-
await finalizeInteractiveLink(props, {
|
|
549
|
-
contextFile: props.contextFile,
|
|
550
|
-
orgId,
|
|
551
|
-
projectId: action.projectId,
|
|
552
|
-
branch: resolved.branch,
|
|
553
|
-
branchRef: resolved.ref,
|
|
554
|
-
created: false,
|
|
555
|
-
projectName: action.name,
|
|
556
|
-
regionId: action.regionId
|
|
557
|
-
});
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
|
-
if (inputs.branch) throw new Error(`Conflicting inputs: --branch pins a branch of an existing project, but creating a project starts from its default branch. Link an existing project, or omit --branch and create first.`);
|
|
561
|
-
const projectName = inputs.projectName ?? await promptProjectName(action.suggestedName);
|
|
562
|
-
const regionId = inputs.regionId ?? await promptRegion(props);
|
|
563
|
-
const created = await createProject(props, {
|
|
564
|
-
orgId,
|
|
565
|
-
name: projectName,
|
|
566
|
-
regionId
|
|
567
|
-
});
|
|
568
|
-
applyContext(props.contextFile, {
|
|
569
|
-
orgId,
|
|
570
|
-
projectId: created.project.id,
|
|
571
|
-
branch: created.branchName
|
|
572
|
-
});
|
|
573
|
-
await finalizeInteractiveLink(props, {
|
|
574
|
-
contextFile: props.contextFile,
|
|
575
|
-
orgId,
|
|
576
|
-
projectId: created.project.id,
|
|
577
|
-
branch: created.branchName,
|
|
578
|
-
branchRef: pinnedRef({
|
|
579
|
-
id: created.branchId,
|
|
580
|
-
name: created.branchName
|
|
581
|
-
}),
|
|
582
|
-
created: true,
|
|
583
|
-
projectName: created.project.name,
|
|
584
|
-
regionId: created.project.region_id
|
|
585
|
-
});
|
|
586
|
-
};
|
|
587
|
-
const confirmRelinkIfNeeded = async (props) => {
|
|
588
|
-
const existing = readContextFile(props.contextFile);
|
|
589
|
-
if (!existing.projectId) return true;
|
|
590
|
-
const { proceed } = await prompts({
|
|
591
|
-
onState: onPromptState,
|
|
592
|
-
type: "confirm",
|
|
593
|
-
name: "proceed",
|
|
594
|
-
message: existing.orgId ? `${props.contextFile} is already linked to project ${existing.projectId} (org ${existing.orgId}). Re-link?` : `${props.contextFile} is already linked to project ${existing.projectId}. Re-link?`,
|
|
595
|
-
initial: true
|
|
596
|
-
});
|
|
597
|
-
if (!proceed) {
|
|
598
|
-
process.stdout.write("Aborted. Existing link preserved.\n");
|
|
599
|
-
return false;
|
|
600
|
-
}
|
|
601
|
-
return true;
|
|
602
|
-
};
|
|
603
|
-
const promptOrgFromList = async (orgs) => {
|
|
604
|
-
if (!orgs.length) throw new Error(`You don't belong to any organizations. Create one in the Neon Console first: https://console.neon.tech/`);
|
|
605
|
-
if (orgs.length === 1) {
|
|
606
|
-
const [only] = orgs;
|
|
607
|
-
log.info(`Linking organization ${only.name} (${only.id}).`);
|
|
608
|
-
return only.id;
|
|
609
|
-
}
|
|
610
|
-
const { orgId } = await prompts({
|
|
611
|
-
onState: onPromptState,
|
|
612
|
-
type: "select",
|
|
613
|
-
name: "orgId",
|
|
614
|
-
message: "Which organization would you like to link?",
|
|
615
|
-
choices: orgs.map((org) => ({
|
|
616
|
-
title: `${org.name} (${org.id})`,
|
|
617
|
-
value: org.id
|
|
618
|
-
})),
|
|
619
|
-
initial: 0
|
|
620
|
-
});
|
|
621
|
-
return orgId;
|
|
622
|
-
};
|
|
623
|
-
const promptProjectChoice = async (projects, suggestedName, opts) => {
|
|
624
|
-
const choices = [...opts.allowCreate ? [{
|
|
625
|
-
title: "+ Create new project…",
|
|
626
|
-
value: CREATE_NEW_SENTINEL
|
|
627
|
-
}] : [], ...projects.map((project) => ({
|
|
628
|
-
title: `${project.name} (${project.id})`,
|
|
629
|
-
value: project.id
|
|
630
|
-
}))];
|
|
631
|
-
if (choices.length === 0) throw new LinkInputError("No projects are available to link. Omit --branch to create a project, or pass --project-id for an existing one.");
|
|
632
|
-
const createOffset = opts.allowCreate ? 1 : 0;
|
|
633
|
-
const { selection } = await prompts({
|
|
634
|
-
onState: onPromptState,
|
|
635
|
-
type: "select",
|
|
636
|
-
name: "selection",
|
|
637
|
-
message: "Which project would you like to link?",
|
|
638
|
-
choices,
|
|
639
|
-
initial: projects.length > 0 ? createOffset : 0
|
|
640
|
-
});
|
|
641
|
-
if (selection === CREATE_NEW_SENTINEL) return {
|
|
642
|
-
type: "create",
|
|
643
|
-
suggestedName
|
|
644
|
-
};
|
|
645
|
-
const project = projects.find((p) => p.id === selection);
|
|
646
|
-
return {
|
|
647
|
-
type: "existing",
|
|
648
|
-
projectId: selection,
|
|
649
|
-
name: project?.name,
|
|
650
|
-
regionId: project?.region_id
|
|
651
|
-
};
|
|
652
|
-
};
|
|
653
|
-
const promptProjectName = async (suggestedName) => {
|
|
654
|
-
const { name } = await prompts({
|
|
655
|
-
onState: onPromptState,
|
|
656
|
-
type: "text",
|
|
657
|
-
name: "name",
|
|
658
|
-
message: "Name for the new project:",
|
|
659
|
-
initial: suggestedName,
|
|
660
|
-
validate: (value) => value && value.trim().length > 0 ? true : "Project name is required"
|
|
661
|
-
});
|
|
662
|
-
return String(name).trim();
|
|
663
|
-
};
|
|
664
|
-
const promptRegion = async (props) => {
|
|
665
|
-
const regions = await fetchRegions(props);
|
|
666
|
-
const defaultIndex = Math.max(0, regions.findIndex((r) => r.default));
|
|
667
|
-
const { regionId } = await prompts({
|
|
668
|
-
onState: onPromptState,
|
|
669
|
-
type: "select",
|
|
670
|
-
name: "regionId",
|
|
671
|
-
message: "Which region should the new project run in?",
|
|
672
|
-
choices: regions.map((region) => ({
|
|
673
|
-
title: `${region.name} (${region.region_id})`,
|
|
674
|
-
value: region.region_id
|
|
675
|
-
})),
|
|
676
|
-
initial: defaultIndex
|
|
677
|
-
});
|
|
678
|
-
return regionId;
|
|
679
|
-
};
|
|
680
|
-
const ORG_KEY_LIMITED_FRAGMENT = "not allowed for organization API keys";
|
|
681
|
-
const isOrgKeyLimitedError = (err) => {
|
|
682
|
-
if (!isNeonApiError(err)) return false;
|
|
683
|
-
const message = messageFromBody(err.data);
|
|
684
|
-
return typeof message === "string" && message.includes(ORG_KEY_LIMITED_FRAGMENT);
|
|
685
|
-
};
|
|
686
|
-
const fetchOrganizations = async (props) => {
|
|
687
|
-
const { data } = await props.apiClient.getCurrentUserOrganizations();
|
|
688
|
-
return data.organizations ?? [];
|
|
689
|
-
};
|
|
690
|
-
/**
|
|
691
|
-
* Resolves the org id from the explicit flag, falling back to listing user orgs.
|
|
692
|
-
*
|
|
693
|
-
* For organization-scoped API keys, `getCurrentUserOrganizations` is forbidden;
|
|
694
|
-
* in that case we try to auto-detect the org from the first existing project
|
|
695
|
-
* (since all projects of an org key live in the same org). If no project exists
|
|
696
|
-
* yet, we return `needs_selection` with `orgKeyLimited: true` so callers can
|
|
697
|
-
* give a precise instruction to the user.
|
|
698
|
-
*/
|
|
699
|
-
const resolveOrg = async (props, given) => {
|
|
700
|
-
if (given) return {
|
|
701
|
-
kind: "resolved",
|
|
702
|
-
orgId: given,
|
|
703
|
-
autoDetected: false
|
|
704
|
-
};
|
|
705
|
-
try {
|
|
706
|
-
return {
|
|
707
|
-
kind: "needs_selection",
|
|
708
|
-
orgs: await fetchOrganizations(props),
|
|
709
|
-
orgKeyLimited: false
|
|
710
|
-
};
|
|
711
|
-
} catch (err) {
|
|
712
|
-
if (!isOrgKeyLimitedError(err)) throw err;
|
|
713
|
-
log.debug("getCurrentUserOrganizations not allowed (org-scoped API key); attempting to derive org from existing projects.");
|
|
714
|
-
}
|
|
715
|
-
const detected = await detectOrgIdFromProjects(props);
|
|
716
|
-
if (detected) return {
|
|
717
|
-
kind: "resolved",
|
|
718
|
-
orgId: detected,
|
|
719
|
-
autoDetected: true
|
|
720
|
-
};
|
|
721
|
-
return {
|
|
722
|
-
kind: "needs_selection",
|
|
723
|
-
orgs: [],
|
|
724
|
-
orgKeyLimited: true
|
|
725
|
-
};
|
|
726
|
-
};
|
|
727
|
-
const detectOrgIdFromProjects = async (props) => {
|
|
728
|
-
try {
|
|
729
|
-
const { data } = await props.apiClient.listProjects({ limit: 1 });
|
|
730
|
-
return data.projects[0]?.org_id ?? void 0;
|
|
731
|
-
} catch (err) {
|
|
732
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
733
|
-
log.debug("detectOrgIdFromProjects failed: %s", message);
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
const listAllProjects = async (props, orgId) => {
|
|
738
|
-
const result = [];
|
|
739
|
-
let cursor;
|
|
740
|
-
while (true) {
|
|
741
|
-
const { data } = await props.apiClient.listProjects({
|
|
742
|
-
limit: PROJECTS_LIST_LIMIT,
|
|
743
|
-
org_id: orgId,
|
|
744
|
-
cursor
|
|
745
|
-
});
|
|
746
|
-
result.push(...data.projects);
|
|
747
|
-
cursor = data.pagination?.cursor;
|
|
748
|
-
if (data.projects.length < PROJECTS_LIST_LIMIT) break;
|
|
749
|
-
}
|
|
750
|
-
return result;
|
|
751
|
-
};
|
|
752
|
-
const CANDIDATE_FIELDS = ["id", "name"];
|
|
753
|
-
const extraYesFlags = (props, inputs) => {
|
|
754
|
-
const flags = [];
|
|
755
|
-
if (inputs.projectName) flags.push(`--project-name ${quoteFlagValue(inputs.projectName)}`);
|
|
756
|
-
if (inputs.regionId) flags.push(`--region-id ${quoteFlagValue(inputs.regionId)}`);
|
|
757
|
-
if (inputs.branch) flags.push(`--branch ${quoteFlagValue(inputs.branch)}`);
|
|
758
|
-
return `${flags.length > 0 ? ` ${flags.join(" ")}` : ""}${sessionRetryFlags(props)}`;
|
|
759
|
-
};
|
|
760
|
-
const yesProjectIdCommand = (props, inputs, orgId = inputs.orgId) => {
|
|
761
|
-
const orgFlag = orgId ? ` --org-id ${quoteFlagValue(orgId)}` : "";
|
|
762
|
-
const branchFlag = inputs.branch ? ` --branch ${quoteFlagValue(inputs.branch)}` : "";
|
|
763
|
-
return `${getCliName()} link -y${orgFlag} --project-id <project-id>${branchFlag}${sessionRetryFlags(props)}`;
|
|
764
|
-
};
|
|
765
|
-
const printNamedCandidates = (props, title, items) => {
|
|
766
|
-
writer(props).end(items, {
|
|
767
|
-
fields: CANDIDATE_FIELDS,
|
|
768
|
-
title
|
|
769
|
-
});
|
|
770
|
-
};
|
|
771
|
-
const orgScopedKeyNeedsOrgId = () => new LinkInputError("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).");
|
|
772
|
-
const resolveYesOrgId = async (props, inputs) => {
|
|
773
|
-
const orgResolution = await resolveOrg(props, inputs.orgId);
|
|
774
|
-
if (orgResolution.kind === "resolved") {
|
|
775
|
-
if (orgResolution.autoDetected) log.info(`Detected organization ${orgResolution.orgId} from your existing projects (organization-scoped API key).`);
|
|
776
|
-
return orgResolution.orgId;
|
|
777
|
-
}
|
|
778
|
-
if (orgResolution.orgKeyLimited) throw orgScopedKeyNeedsOrgId();
|
|
779
|
-
const orgs = orgResolution.orgs;
|
|
780
|
-
if (orgs.length === 0) throw new LinkInputError(["No organizations were returned for this account. Pass --project-id for a project you can access:", ` ${yesProjectIdCommand(props, inputs)}`].join("\n"));
|
|
781
|
-
if (orgs.length === 1) {
|
|
782
|
-
const [only] = orgs;
|
|
783
|
-
return only.id;
|
|
784
|
-
}
|
|
785
|
-
printNamedCandidates(props, "Organizations", orgs.map((org) => ({
|
|
786
|
-
id: org.id,
|
|
787
|
-
name: org.name
|
|
788
|
-
})));
|
|
789
|
-
throw new LinkInputError(["Multiple organizations are available. Pass --org-id with an ID from the list:", ` ${getCliName()} link -y --org-id <org-id>${extraYesFlags(props, inputs)}`].join("\n"));
|
|
790
|
-
};
|
|
791
|
-
const resolveYesInputs = async (props, inputs) => {
|
|
792
|
-
const orgId = await resolveYesOrgId(props, inputs);
|
|
793
|
-
if (inputs.projectName && inputs.regionId) return {
|
|
794
|
-
...inputs,
|
|
795
|
-
orgId
|
|
796
|
-
};
|
|
797
|
-
const projects = await listAllProjects(props, orgId);
|
|
798
|
-
if (projects.length === 0) throw new LinkInputError([
|
|
799
|
-
`No projects are available in organization '${orgId}'.`,
|
|
800
|
-
"To create and link a project, pass --project-name and --region-id:",
|
|
801
|
-
` ${getCliName()} link -y --org-id ${quoteFlagValue(orgId)} --project-name <name> --region-id aws-us-east-2${sessionRetryFlags(props)}`
|
|
802
|
-
].join("\n"));
|
|
803
|
-
if (projects.length === 1) {
|
|
804
|
-
const [only] = projects;
|
|
805
|
-
return {
|
|
806
|
-
...inputs,
|
|
807
|
-
orgId,
|
|
808
|
-
projectId: only.id
|
|
809
|
-
};
|
|
810
|
-
}
|
|
811
|
-
printNamedCandidates(props, "Projects", projects.map((project) => ({
|
|
812
|
-
id: project.id,
|
|
813
|
-
name: project.name
|
|
814
|
-
})));
|
|
815
|
-
throw new LinkInputError([`Multiple projects are available in organization '${orgId}'. Pass --project-id with an ID from the list:`, ` ${yesProjectIdCommand(props, inputs, orgId)}`].join("\n"));
|
|
816
|
-
};
|
|
817
|
-
const fetchRegions = async (props) => {
|
|
818
|
-
try {
|
|
819
|
-
const { data } = await props.apiClient.getActiveRegions();
|
|
820
|
-
if (data.regions && data.regions.length > 0) return data.regions;
|
|
821
|
-
} catch (err) {
|
|
822
|
-
if (isNeonApiError(err)) log.debug("getActiveRegions failed (%s), falling back to the static region list.", err.status ?? err.code ?? err.message);
|
|
823
|
-
else {
|
|
824
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
825
|
-
log.debug("getActiveRegions failed (%s), falling back to the static region list.", message);
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return staticRegionsFallback();
|
|
829
|
-
};
|
|
830
|
-
const staticRegionsFallback = () => REGIONS.map((id) => ({
|
|
831
|
-
region_id: id,
|
|
832
|
-
name: id,
|
|
833
|
-
default: id === "aws-us-east-2",
|
|
834
|
-
geo_lat: "",
|
|
835
|
-
geo_long: ""
|
|
836
|
-
}));
|
|
837
|
-
const createProject = async (props, args) => {
|
|
838
|
-
const project = {
|
|
839
|
-
name: args.name,
|
|
840
|
-
region_id: args.regionId,
|
|
841
|
-
org_id: args.orgId,
|
|
842
|
-
branch: {}
|
|
843
|
-
};
|
|
844
|
-
const { data } = await props.apiClient.createProject({ project });
|
|
845
|
-
if (!data.branch?.id) throw new Error("Project was created but the API response did not include a default branch id.");
|
|
846
|
-
return {
|
|
847
|
-
project: {
|
|
848
|
-
id: data.project.id,
|
|
849
|
-
name: data.project.name,
|
|
850
|
-
region_id: data.project.region_id
|
|
851
|
-
},
|
|
852
|
-
branchId: data.branch.id,
|
|
853
|
-
branchName: data.branch.name ?? data.branch.id
|
|
854
|
-
};
|
|
855
|
-
};
|
|
856
|
-
const printSummary = (props, summary) => {
|
|
857
|
-
process.stdout.write(`${formatLinkSummary(summary, props.cwd ?? process.cwd())}\n`);
|
|
858
|
-
};
|
|
859
|
-
/**
|
|
860
|
-
* Print the link summary, then run the bundled `env pull` so a completed `link` ends with
|
|
861
|
-
* the pinned branch's connection string on disk. `--no-checks` skips the pull because
|
|
862
|
-
* nothing was verified. `--no-env-pull` opts out (env pull's own status / skip hint is
|
|
863
|
-
* logged to stderr).
|
|
864
|
-
*/
|
|
865
|
-
const finalizeLink = async (props, summary) => {
|
|
866
|
-
printSummary(props, summary);
|
|
867
|
-
if (!summary.branch || !summary.projectId || summary.noChecks || props.deferEnvPull === true) return;
|
|
868
|
-
const { config: _offerConfig, ...rest } = props;
|
|
869
|
-
await autoPullEnvAfterPin({
|
|
870
|
-
...rest,
|
|
871
|
-
...props.cwd ? { cwd: props.cwd } : {},
|
|
872
|
-
projectId: summary.projectId,
|
|
873
|
-
branch: summary.branch,
|
|
874
|
-
envPull: props.envPull
|
|
875
|
-
}, summary.branchRef);
|
|
876
|
-
};
|
|
877
|
-
/**
|
|
878
|
-
* Interactive `link` finalize: the shared {@link finalizeLink} (summary + env
|
|
879
|
-
* pull), then — as the last step — offer to manage the project's Neon setup as
|
|
880
|
-
* code with a `neon.ts`. Kept out of {@link finalizeLink} so the non-interactive
|
|
881
|
-
* paths never prompt.
|
|
882
|
-
*/
|
|
883
|
-
const finalizeInteractiveLink = async (props, summary) => {
|
|
884
|
-
await finalizeLink(props, summary);
|
|
885
|
-
await maybeOfferConfigInit(props, summary);
|
|
886
|
-
};
|
|
887
|
-
/**
|
|
888
|
-
* Interactive `link` offers neon.ts only when the directory has none and the
|
|
889
|
-
* caller did not pass --no-config. Init always passes --no-config so it can ask
|
|
890
|
-
* once after linking.
|
|
891
|
-
*/
|
|
892
|
-
const shouldOfferConfigInit = (input) => !input.hasConfig && input.offer;
|
|
893
|
-
const maybeOfferConfigInit = async (props, summary) => {
|
|
894
|
-
const dir = projectDir({
|
|
895
|
-
...props.cwd ? { cwd: props.cwd } : {},
|
|
896
|
-
contextFile: props.contextFile
|
|
897
|
-
});
|
|
898
|
-
if (!shouldOfferConfigInit({
|
|
899
|
-
hasConfig: hasNeonConfigFile(dir),
|
|
900
|
-
offer: props.config !== false
|
|
901
|
-
})) return;
|
|
902
|
-
const { value } = await prompts({
|
|
903
|
-
onState: onPromptState,
|
|
904
|
-
type: "confirm",
|
|
905
|
-
name: "value",
|
|
906
|
-
message: "Manage this project's Neon setup as code? Adds a neon.ts you can edit and apply with `neon config apply`.",
|
|
907
|
-
initial: true
|
|
908
|
-
});
|
|
909
|
-
if (value !== true) return;
|
|
910
|
-
await initCmd({
|
|
911
|
-
cwd: dir,
|
|
912
|
-
install: true
|
|
913
|
-
});
|
|
914
|
-
if (summary.branch && summary.projectId) {
|
|
915
|
-
const { config: _offerConfig, ...rest } = props;
|
|
916
|
-
await autoPullEnvAfterPin({
|
|
917
|
-
...rest,
|
|
918
|
-
projectId: summary.projectId,
|
|
919
|
-
branch: summary.branch,
|
|
920
|
-
envPull: props.envPull
|
|
921
|
-
}, summary.branchRef);
|
|
922
|
-
}
|
|
923
|
-
};
|
|
924
|
-
const onPromptState = (state) => {
|
|
925
|
-
if (state.aborted) {
|
|
926
|
-
process.stdout.write("\x1B[?25h");
|
|
927
|
-
process.stdout.write("\n");
|
|
928
|
-
process.exit(1);
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
const mustString = (value, name) => {
|
|
932
|
-
if (value === void 0) throw new Error(`Internal error: expected ${name} to be set.`);
|
|
933
|
-
return value;
|
|
934
|
-
};
|
|
935
|
-
//#endregion
|
|
936
|
-
export { builder, command, describe, handler, quoteFlagValue, runLink, shouldOfferConfigInit, link_exports as t };
|
|
1
|
+
export { t as builder, n as command, r as describe, i as handler, o as quoteFlagValue, s as runLink, c as shouldOfferConfigInit } from "../_chunks/link-BgtkOBws.js";
|