neon 4.6.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -113
- package/dist/_chunks/{auth_selection-ktL6uoEe.js → auth_selection-pitMFzDe.js} +1 -1
- package/dist/_chunks/{credential_io-B1JxWELN.js → credential_io-YeAxg9Xn.js} +2 -2
- package/dist/_chunks/{paths-g3DRqJiD.js → paths-DMq0Lt7a.js} +1 -1
- package/dist/_chunks/{profiles-D95AeZ5v.js → profiles-CvnFEQyd.js} +1 -1
- package/dist/analytics.js +2 -2
- package/dist/auth_context.js +1 -1
- package/dist/commands/auth.js +12 -9
- package/dist/commands/bootstrap.js +72 -43
- package/dist/commands/claim.js +1 -1
- package/dist/commands/env.js +3 -20
- package/dist/commands/init.js +80 -79
- package/dist/commands/link.js +110 -292
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/plugins.js +3 -3
- package/dist/commands/profile.js +4 -4
- package/dist/config.js +1 -1
- package/dist/credential_io.js +1 -1
- package/dist/index.js +1 -1
- package/dist/init/child.js +36 -0
- package/dist/init/plan.js +47 -0
- package/dist/init/tooling.js +43 -0
- package/dist/init/wizard.js +42 -0
- package/dist/mcp/targets.js +1 -1
- package/dist/plugins/plan.js +1 -1
- package/dist/plugins/targets.js +5 -1
- package/dist/retire_credential.js +1 -1
- package/dist/utils/cli_name.js +1 -12
- package/dist/utils/middlewares.js +1 -1
- package/dist/utils/package_manager.js +10 -5
- package/package.json +13 -14
- package/dist/init/auth.js +0 -53
- package/dist/init/build_config.js +0 -4
- package/dist/init/detect_agent.js +0 -63
- package/dist/init/editors.js +0 -19
- package/dist/init/enrich_output.js +0 -61
- package/dist/init/extension.js +0 -154
- package/dist/init/inspect.js +0 -249
- package/dist/init/interactive.js +0 -529
- package/dist/init/neonctl.js +0 -139
- package/dist/init/orchestrate.js +0 -189
- package/dist/init/phases/auth.js +0 -195
- package/dist/init/phases/cleanup.js +0 -27
- package/dist/init/phases/db.js +0 -257
- package/dist/init/phases/getting_started.js +0 -218
- package/dist/init/phases/mcp.js +0 -219
- package/dist/init/phases/migrations.js +0 -250
- package/dist/init/phases/neon_auth.js +0 -116
- package/dist/init/phases/setup.js +0 -582
- package/dist/init/phases/skills.js +0 -86
- package/dist/init/phases/status.js +0 -82
- package/dist/init/profile_cli.js +0 -56
- package/dist/init/resolve_context.js +0 -105
- package/dist/init/route_command.js +0 -93
- package/dist/init/skills.js +0 -206
- package/dist/init/types.js +0 -1
- package/dist/init/vsix.js +0 -89
package/dist/commands/link.js
CHANGED
|
@@ -5,8 +5,9 @@ import { isNeonApiError, messageFromBody } from "../api.js";
|
|
|
5
5
|
import { applyContext, contextBranch, readContextFile, setContext, updateContextFile } from "../context.js";
|
|
6
6
|
import { getCliName } from "../utils/cli_name.js";
|
|
7
7
|
import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
|
|
8
|
-
import { autoPullEnvAfterPin
|
|
8
|
+
import { autoPullEnvAfterPin } from "./env.js";
|
|
9
9
|
import { hasNeonConfigFile, initCmd } from "./config.js";
|
|
10
|
+
import { helpEpilogue } from "../utils/help_text.js";
|
|
10
11
|
import { REGIONS } from "./projects.js";
|
|
11
12
|
import prompts from "prompts";
|
|
12
13
|
//#region src/commands/link.ts
|
|
@@ -18,6 +19,19 @@ var link_exports = /* @__PURE__ */ __exportAll({
|
|
|
18
19
|
});
|
|
19
20
|
const PROJECTS_LIST_LIMIT = 100;
|
|
20
21
|
const CREATE_NEW_SENTINEL = "__create_new__";
|
|
22
|
+
const canPromptInteractively = () => !isCi() && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
23
|
+
const nonInteractiveLinkCommands = () => {
|
|
24
|
+
const cli = getCliName();
|
|
25
|
+
return [
|
|
26
|
+
`${cli} orgs list --output json`,
|
|
27
|
+
`${cli} projects list --org-id <org-id> --output json`,
|
|
28
|
+
`${cli} link --project-id <project-id> [--branch <name> | -y]`,
|
|
29
|
+
`${cli} link --org-id <org-id> --project-name <name> --region-id aws-us-east-2`
|
|
30
|
+
];
|
|
31
|
+
};
|
|
32
|
+
const nonInteractiveLinkHelp = () => nonInteractiveLinkCommands().map((command) => ` ${command}`).join("\n");
|
|
33
|
+
const orgScopedKeyHint = "Organization-scoped API keys cannot list orgs; pass --org-id.";
|
|
34
|
+
const removedAgent = () => `\`${getCliName()} link --agent\` was removed.\nUse:\n${nonInteractiveLinkHelp()}\n${orgScopedKeyHint}`;
|
|
21
35
|
const command = "link";
|
|
22
36
|
const describe = "Link the current directory to a Neon project";
|
|
23
37
|
const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
@@ -39,7 +53,7 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
39
53
|
},
|
|
40
54
|
branch: {
|
|
41
55
|
alias: "branch-id",
|
|
42
|
-
describe:
|
|
56
|
+
describe: "Branch name or ID to pin in the context (resolved to its name before writing).",
|
|
43
57
|
type: "string"
|
|
44
58
|
},
|
|
45
59
|
params: {
|
|
@@ -47,13 +61,12 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
47
61
|
type: "string"
|
|
48
62
|
},
|
|
49
63
|
agent: {
|
|
50
|
-
|
|
51
|
-
type: "boolean"
|
|
52
|
-
default: false
|
|
64
|
+
hidden: true,
|
|
65
|
+
type: "boolean"
|
|
53
66
|
},
|
|
54
67
|
yes: {
|
|
55
68
|
alias: "y",
|
|
56
|
-
describe: "Skip the \"already linked\" confirmation
|
|
69
|
+
describe: "Skip the \"already linked\" confirmation, and pin the project's default branch when linking a project that has more than one.",
|
|
57
70
|
type: "boolean",
|
|
58
71
|
default: false
|
|
59
72
|
},
|
|
@@ -73,12 +86,16 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
|
|
|
73
86
|
default: true
|
|
74
87
|
}
|
|
75
88
|
}).example([
|
|
76
|
-
["$0 link --project-id polished-snowflake-12345678",
|
|
89
|
+
["$0 link --project-id polished-snowflake-12345678", "Link an existing project (org is inferred). Pins the only branch; several prompt in a TTY"],
|
|
90
|
+
["$0 link --project-id polished-snowflake-12345678 -y", "Same, pinning the project's default branch when several exist"],
|
|
77
91
|
["$0 link --org-id org-… --project-name my-app --region-id aws-us-east-2", "Create a new project and link it"],
|
|
78
92
|
["$0 link --branch-id br-…", "Pin a branch in the already-linked project"],
|
|
79
93
|
["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678", "Write the context offline (no API calls, no verification)"],
|
|
80
94
|
["$0 link --clear", "Forget the current org/project/branch context"]
|
|
81
|
-
])
|
|
95
|
+
]).epilogue(helpEpilogue("Non-interactive (CI, scripts, agents):", ...nonInteractiveLinkCommands().map((command) => ` ${command}`), orgScopedKeyHint)).check((argv) => {
|
|
96
|
+
if (argv.agent === true) throw new Error(removedAgent());
|
|
97
|
+
return true;
|
|
98
|
+
});
|
|
82
99
|
const handler = async (props) => {
|
|
83
100
|
if (props.clear) {
|
|
84
101
|
clearContext(props.contextFile);
|
|
@@ -88,10 +105,6 @@ const handler = async (props) => {
|
|
|
88
105
|
runWithoutChecks(props);
|
|
89
106
|
return;
|
|
90
107
|
}
|
|
91
|
-
if (props.agent) {
|
|
92
|
-
await runAgentSafely(props);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
108
|
const inputs = parseInputs(props);
|
|
96
109
|
validateInputs(inputs);
|
|
97
110
|
const existing = readContextFile(props.contextFile);
|
|
@@ -99,14 +112,13 @@ const handler = async (props) => {
|
|
|
99
112
|
await runNonInteractive(props, inputs, existing);
|
|
100
113
|
return;
|
|
101
114
|
}
|
|
102
|
-
if (
|
|
115
|
+
if (!canPromptInteractively()) {
|
|
103
116
|
log.error([
|
|
104
|
-
"Missing inputs and
|
|
117
|
+
"Missing inputs and no interactive terminal for prompts.",
|
|
105
118
|
"",
|
|
106
|
-
"Use
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
` ${getCliName()} link --org-id <org> --project-name <name> --region-id <region> (create a new project and link)`
|
|
119
|
+
"Use:",
|
|
120
|
+
nonInteractiveLinkHelp(),
|
|
121
|
+
orgScopedKeyHint
|
|
110
122
|
].join("\n"));
|
|
111
123
|
process.exit(1);
|
|
112
124
|
return;
|
|
@@ -155,16 +167,8 @@ const validateInputs = (inputs) => {
|
|
|
155
167
|
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>\`.`);
|
|
156
168
|
};
|
|
157
169
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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).
|
|
170
|
+
* Branch selection may still prompt after org and project resolve without the
|
|
171
|
+
* org/project wizard.
|
|
168
172
|
*/
|
|
169
173
|
const canResolveNonInteractively = (inputs, existing) => {
|
|
170
174
|
if (inputs.projectId) return true;
|
|
@@ -195,20 +199,6 @@ const runWithoutChecks = (props) => {
|
|
|
195
199
|
projectId: inputs.projectId,
|
|
196
200
|
branch: inputs.branch
|
|
197
201
|
});
|
|
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
202
|
printSummary(props, {
|
|
213
203
|
contextFile: props.contextFile,
|
|
214
204
|
orgId: inputs.orgId,
|
|
@@ -218,25 +208,14 @@ const runWithoutChecks = (props) => {
|
|
|
218
208
|
noChecks: true
|
|
219
209
|
});
|
|
220
210
|
};
|
|
221
|
-
/**
|
|
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
211
|
var LinkInputError = class extends Error {
|
|
228
|
-
constructor(message
|
|
212
|
+
constructor(message) {
|
|
229
213
|
super(message);
|
|
230
214
|
this.name = "LinkInputError";
|
|
231
|
-
this.agentCode = agentCode;
|
|
232
215
|
}
|
|
233
216
|
};
|
|
234
217
|
const httpStatus = (err) => isNeonApiError(err) ? err.status : void 0;
|
|
235
|
-
/**
|
|
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
|
-
*/
|
|
218
|
+
/** 401 must reach the global handler so it can refresh credentials. */
|
|
240
219
|
const fetchProjectOrThrow = async (props, projectId) => {
|
|
241
220
|
try {
|
|
242
221
|
const { data } = await props.apiClient.getProject(projectId);
|
|
@@ -244,8 +223,8 @@ const fetchProjectOrThrow = async (props, projectId) => {
|
|
|
244
223
|
} catch (err) {
|
|
245
224
|
const status = httpStatus(err);
|
|
246
225
|
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
|
|
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
|
|
226
|
+
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.`);
|
|
227
|
+
if (status === 404) throw new LinkInputError(`Project '${projectId}' not found. Double-check the project ID — or that your API key has access to it.`);
|
|
249
228
|
throw err;
|
|
250
229
|
}
|
|
251
230
|
};
|
|
@@ -263,7 +242,7 @@ const verifyOrgAccess = async (props, orgId) => {
|
|
|
263
242
|
} catch (err) {
|
|
264
243
|
const status = httpStatus(err);
|
|
265
244
|
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
|
|
245
|
+
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.`);
|
|
267
246
|
throw err;
|
|
268
247
|
}
|
|
269
248
|
};
|
|
@@ -278,7 +257,7 @@ const resolveBranchRef = async (props, projectId, branchRef) => {
|
|
|
278
257
|
const { data } = await props.apiClient.listProjectBranches({ projectId });
|
|
279
258
|
const match = data.branches.find((b) => b.id === branchRef) ?? data.branches.find((b) => b.name === branchRef);
|
|
280
259
|
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
|
|
260
|
+
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>\`.`);
|
|
282
261
|
};
|
|
283
262
|
/**
|
|
284
263
|
* The value to persist for a branch: prefer its human-readable **name** (nicer
|
|
@@ -301,7 +280,7 @@ const branchPersistValue = (branch) => branch.name ?? branch.id;
|
|
|
301
280
|
const resolveOrgForProject = async (props, inputs, existing, projectId) => {
|
|
302
281
|
const projectOrg = (await fetchProjectOrThrow(props, projectId)).org_id ?? void 0;
|
|
303
282
|
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
|
|
283
|
+
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.`);
|
|
305
284
|
if (!projectOrg) await verifyOrgAccess(props, inputs.orgId);
|
|
306
285
|
return inputs.orgId;
|
|
307
286
|
}
|
|
@@ -309,20 +288,65 @@ const resolveOrgForProject = async (props, inputs, existing, projectId) => {
|
|
|
309
288
|
if (projectId === existing.projectId && existing.orgId) return existing.orgId;
|
|
310
289
|
};
|
|
311
290
|
/**
|
|
312
|
-
*
|
|
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.
|
|
291
|
+
* Keep an existing pin when re-linking the same project so `-y` does not reset
|
|
292
|
+
* a checkout.
|
|
319
293
|
*/
|
|
320
294
|
const resolvePinnedBranch = async (props, inputs, existing, projectId) => {
|
|
321
295
|
if (inputs.branch) {
|
|
322
296
|
const branch = await resolveBranchRef(props, projectId, inputs.branch);
|
|
323
|
-
return
|
|
297
|
+
return {
|
|
298
|
+
branch: branchPersistValue(branch),
|
|
299
|
+
empty: false
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (projectId === existing.projectId) {
|
|
303
|
+
const pinned = contextBranch(existing);
|
|
304
|
+
if (pinned) return {
|
|
305
|
+
branch: pinned,
|
|
306
|
+
empty: false
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
return resolveBranchFromList(props, projectId);
|
|
310
|
+
};
|
|
311
|
+
const resolveBranchFromList = async (props, projectId) => {
|
|
312
|
+
const { data } = await props.apiClient.listProjectBranches({ projectId });
|
|
313
|
+
const branches = data.branches;
|
|
314
|
+
if (branches.length === 0) return { empty: true };
|
|
315
|
+
if (branches.length === 1) {
|
|
316
|
+
const [only] = branches;
|
|
317
|
+
if (!only) return { empty: true };
|
|
318
|
+
return {
|
|
319
|
+
branch: branchPersistValue(only),
|
|
320
|
+
empty: false
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
if (props.yes) {
|
|
324
|
+
const def = branches.find((b) => b.default);
|
|
325
|
+
if (!def) throw new Error(`Project '${projectId}' has no default branch. Pass --branch <name> to pin one.`);
|
|
326
|
+
return {
|
|
327
|
+
branch: branchPersistValue(def),
|
|
328
|
+
empty: false
|
|
329
|
+
};
|
|
324
330
|
}
|
|
325
|
-
if (
|
|
331
|
+
if (canPromptInteractively()) {
|
|
332
|
+
const picked = await pickBranchInteractively(branches, {
|
|
333
|
+
message: "Which branch would you like to link?",
|
|
334
|
+
nonInteractiveMessage: `No branch could be selected without an interactive terminal. Re-run \`${getCliName()} link\` interactively, or \`${getCliName()} checkout <branch>\` to pin one.`
|
|
335
|
+
});
|
|
336
|
+
if (picked.kind === "existing") {
|
|
337
|
+
const existing = branches.find((b) => b.id === picked.branchId);
|
|
338
|
+
return {
|
|
339
|
+
branch: existing ? branchPersistValue(existing) : picked.branchId,
|
|
340
|
+
empty: false
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
await createBranch(props.apiClient, projectId, picked.name, branches);
|
|
344
|
+
return {
|
|
345
|
+
branch: picked.name,
|
|
346
|
+
empty: false
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return { empty: false };
|
|
326
350
|
};
|
|
327
351
|
const runNonInteractive = async (props, inputs, existing) => {
|
|
328
352
|
if (inputs.projectName) {
|
|
@@ -351,36 +375,38 @@ const runNonInteractive = async (props, inputs, existing) => {
|
|
|
351
375
|
}
|
|
352
376
|
if (inputs.projectId) {
|
|
353
377
|
const orgId = await resolveOrgForProject(props, inputs, existing, inputs.projectId);
|
|
354
|
-
const
|
|
378
|
+
const resolved = await resolvePinnedBranch(props, inputs, existing, inputs.projectId);
|
|
355
379
|
applyContext(props.contextFile, {
|
|
356
380
|
orgId,
|
|
357
381
|
projectId: inputs.projectId,
|
|
358
|
-
branch
|
|
382
|
+
branch: resolved.branch
|
|
359
383
|
});
|
|
360
384
|
await finalizeLink(props, {
|
|
361
385
|
contextFile: props.contextFile,
|
|
362
386
|
orgId,
|
|
363
387
|
projectId: inputs.projectId,
|
|
364
|
-
branch,
|
|
365
|
-
created: false
|
|
388
|
+
branch: resolved.branch,
|
|
389
|
+
created: false,
|
|
390
|
+
empty: resolved.empty
|
|
366
391
|
});
|
|
367
392
|
return;
|
|
368
393
|
}
|
|
369
394
|
if (inputs.branch && existing.projectId) {
|
|
370
395
|
const projectId = existing.projectId;
|
|
371
396
|
const orgId = await resolveOrgForProject(props, inputs, existing, projectId);
|
|
372
|
-
const
|
|
397
|
+
const resolved = await resolvePinnedBranch(props, inputs, existing, projectId);
|
|
373
398
|
applyContext(props.contextFile, {
|
|
374
399
|
orgId,
|
|
375
400
|
projectId,
|
|
376
|
-
branch
|
|
401
|
+
branch: resolved.branch
|
|
377
402
|
});
|
|
378
403
|
await finalizeLink(props, {
|
|
379
404
|
contextFile: props.contextFile,
|
|
380
405
|
orgId,
|
|
381
406
|
projectId,
|
|
382
|
-
branch,
|
|
383
|
-
created: false
|
|
407
|
+
branch: resolved.branch,
|
|
408
|
+
created: false,
|
|
409
|
+
empty: resolved.empty
|
|
384
410
|
});
|
|
385
411
|
return;
|
|
386
412
|
}
|
|
@@ -439,20 +465,21 @@ const runInteractive = async (props, inputs) => {
|
|
|
439
465
|
const projects = await listAllProjects(props, orgId);
|
|
440
466
|
const action = await promptProjectChoice(projects, inputs.projectName);
|
|
441
467
|
if (action.type === "existing") {
|
|
442
|
-
const
|
|
468
|
+
const resolved = await resolveBranchFromList(props, action.projectId);
|
|
443
469
|
applyContext(props.contextFile, {
|
|
444
470
|
orgId,
|
|
445
471
|
projectId: action.projectId,
|
|
446
|
-
branch
|
|
472
|
+
branch: resolved.branch
|
|
447
473
|
});
|
|
448
474
|
await finalizeInteractiveLink(props, {
|
|
449
475
|
contextFile: props.contextFile,
|
|
450
476
|
orgId,
|
|
451
477
|
projectId: action.projectId,
|
|
452
|
-
branch,
|
|
478
|
+
branch: resolved.branch,
|
|
453
479
|
created: false,
|
|
454
480
|
projectName: action.name,
|
|
455
|
-
regionId: action.regionId
|
|
481
|
+
regionId: action.regionId,
|
|
482
|
+
empty: resolved.empty
|
|
456
483
|
});
|
|
457
484
|
return;
|
|
458
485
|
}
|
|
@@ -568,135 +595,6 @@ const promptRegion = async (props) => {
|
|
|
568
595
|
});
|
|
569
596
|
return regionId;
|
|
570
597
|
};
|
|
571
|
-
const runAgentSafely = async (props) => {
|
|
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
|
-
}
|
|
580
|
-
};
|
|
581
|
-
const runAgent = async (props, inputs) => {
|
|
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
|
-
});
|
|
696
|
-
};
|
|
697
|
-
const emitAgent = (response) => {
|
|
698
|
-
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
699
|
-
};
|
|
700
598
|
const ORG_KEY_LIMITED_FRAGMENT = "not allowed for organization API keys";
|
|
701
599
|
const isOrgKeyLimitedError = (err) => {
|
|
702
600
|
if (!isNeonApiError(err)) return false;
|
|
@@ -754,56 +652,6 @@ const detectOrgIdFromProjects = async (props) => {
|
|
|
754
652
|
return;
|
|
755
653
|
}
|
|
756
654
|
};
|
|
757
|
-
const buildNeedsOrgResponse = (resolution) => {
|
|
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
|
-
};
|
|
774
|
-
};
|
|
775
|
-
const toAgentError = (err) => {
|
|
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
|
-
};
|
|
806
|
-
};
|
|
807
655
|
const listAllProjects = async (props, orgId) => {
|
|
808
656
|
const result = [];
|
|
809
657
|
let cursor;
|
|
@@ -819,33 +667,6 @@ const listAllProjects = async (props, orgId) => {
|
|
|
819
667
|
}
|
|
820
668
|
return result;
|
|
821
669
|
};
|
|
822
|
-
/**
|
|
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
|
-
*/
|
|
830
|
-
const resolveInteractiveBranch = async (props, projectId) => {
|
|
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;
|
|
848
|
-
};
|
|
849
670
|
const fetchRegions = async (props) => {
|
|
850
671
|
try {
|
|
851
672
|
const { data } = await props.apiClient.getActiveRegions();
|
|
@@ -897,7 +718,8 @@ const printSummary = (_props, summary) => {
|
|
|
897
718
|
lines.push("Written offline (--no-checks): nothing was verified.");
|
|
898
719
|
} else if (summary.projectId && !summary.branch && !summary.orgOnly) {
|
|
899
720
|
lines.push("");
|
|
900
|
-
lines.push(`
|
|
721
|
+
if (summary.empty) lines.push(`This project has no branches, so none was pinned. Create a branch, then run \`${getCliName()} checkout <branch>\` to pin it and pull its env vars.`);
|
|
722
|
+
else lines.push(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
|
|
901
723
|
}
|
|
902
724
|
lines.push("");
|
|
903
725
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
@@ -965,10 +787,6 @@ const onPromptState = (state) => {
|
|
|
965
787
|
process.exit(1);
|
|
966
788
|
}
|
|
967
789
|
};
|
|
968
|
-
const shellArg = (value) => {
|
|
969
|
-
if (/^[A-Za-z0-9._:/-]+$/.test(value)) return value;
|
|
970
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
971
|
-
};
|
|
972
790
|
const mustString = (value, name) => {
|
|
973
791
|
if (value === void 0) throw new Error(`Internal error: expected ${name} to be set.`);
|
|
974
792
|
return value;
|
package/dist/commands/mcp.js
CHANGED
|
@@ -4,13 +4,13 @@ import { readContextFile } from "../context.js";
|
|
|
4
4
|
import { getCliName } from "../utils/cli_name.js";
|
|
5
5
|
import { writer } from "../writer.js";
|
|
6
6
|
import { noPassthrough, single } from "../utils/flags.js";
|
|
7
|
+
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
8
|
+
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
7
9
|
import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
|
|
8
10
|
import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
|
|
9
|
-
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
10
11
|
import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
|
|
11
12
|
import { confirmMcpInstall } from "../mcp/wizard.js";
|
|
12
13
|
import { resolveMcpPlan } from "../mcp/plan.js";
|
|
13
|
-
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
14
14
|
//#region src/commands/mcp.ts
|
|
15
15
|
var mcp_exports = /* @__PURE__ */ __exportAll({
|
|
16
16
|
builder: () => builder,
|
package/dist/commands/plugins.js
CHANGED
|
@@ -2,14 +2,14 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
|
2
2
|
import { log } from "../log.js";
|
|
3
3
|
import { writer } from "../writer.js";
|
|
4
4
|
import { noPassthrough } from "../utils/flags.js";
|
|
5
|
-
import { NEON_MCP_URL } from "../mcp/install.js";
|
|
6
5
|
import { getAgentDisplayName } from "../mcp/agents.js";
|
|
7
6
|
import "../init/agents.js";
|
|
7
|
+
import { pluginsInstallableAgents } from "../plugins/targets.js";
|
|
8
8
|
import { canPickAgentsInteractively } from "../utils/agent_picker.js";
|
|
9
|
+
import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
|
|
9
10
|
import { helpCsv, helpEpilogue } from "../utils/help_text.js";
|
|
10
|
-
import {
|
|
11
|
+
import { NEON_MCP_URL } from "../mcp/install.js";
|
|
11
12
|
import { resolvePluginsPlan } from "../plugins/plan.js";
|
|
12
|
-
import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
|
|
13
13
|
import { confirmPluginsInstall } from "../plugins/wizard.js";
|
|
14
14
|
//#region src/commands/plugins.ts
|
|
15
15
|
var plugins_exports = /* @__PURE__ */ __exportAll({
|
package/dist/commands/profile.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { t as writeSecretFile } from "../_chunks/secure_file-BucZj4yQ.js";
|
|
3
|
-
import { c as describeScope, d as scopeOf, l as interpretCredentials, n as KEYRING_SERVICE, o as apiKeyCredentials, r as keyringAccount, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-
|
|
3
|
+
import { c as describeScope, d as scopeOf, l as interpretCredentials, n as KEYRING_SERVICE, o as apiKeyCredentials, r as keyringAccount, s as credentialLabel, t as storeFor } from "../_chunks/credential_io-YeAxg9Xn.js";
|
|
4
4
|
import { isCi } from "../env.js";
|
|
5
5
|
import { log } from "../log.js";
|
|
6
6
|
import { getApiClient, isNeonApiError } from "../api.js";
|
|
7
|
-
import {
|
|
7
|
+
import { a as isOwnedCredentialPath, n as credentialsPath } from "../_chunks/paths-DMq0Lt7a.js";
|
|
8
8
|
import { setAuthContext } from "../auth_context.js";
|
|
9
|
-
import { _ as upsertProfile, a as canDropProfilesFile, c as listProfiles, d as newProfileLocation, f as profilesFilePath, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, o as credentialsDisplay, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, u as locationOf, v as CRED_STORAGE_FILE, y as CRED_STORAGE_KEYRING } from "../_chunks/profiles-
|
|
9
|
+
import { _ as upsertProfile, a as canDropProfilesFile, c as listProfiles, d as newProfileLocation, f as profilesFilePath, g as selectProfileName, h as resolveProfile, i as assertValidProfileName, l as locationForName, m as readProfiles, n as KEYRING_CREDENTIALS, o as credentialsDisplay, p as profilesUsingPath, r as assertProfilesUsable, s as isKeyringPointer, u as locationOf, v as CRED_STORAGE_FILE, y as CRED_STORAGE_KEYRING } from "../_chunks/profiles-CvnFEQyd.js";
|
|
10
10
|
import { auth, revokeToken } from "../auth.js";
|
|
11
|
-
import { t as credentialInputs } from "../_chunks/auth_selection-
|
|
11
|
+
import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
|
|
12
12
|
import { readOutgoingCredential, retirePreviousCredential, revokeTokenSet, withdrawKey } from "../retire_credential.js";
|
|
13
13
|
import { authFlow, usableCredential } from "./auth.js";
|
|
14
14
|
import { writer } from "../writer.js";
|
package/dist/config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCi } from "./env.js";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as isOwnedCredentialPath, i as isInsideConfigDir, n as credentialsPath, r as defaultDir, t as CREDENTIALS_FILE } from "./_chunks/paths-DMq0Lt7a.js";
|
|
3
3
|
import { existsSync, mkdirSync } from "node:fs";
|
|
4
4
|
//#region src/config.ts
|
|
5
5
|
const ensureConfigDir = ({ "config-dir": configDirArg, "force-auth": forceAuth }) => {
|
package/dist/credential_io.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as storeFor } from "./_chunks/credential_io-
|
|
1
|
+
import { t as storeFor } from "./_chunks/credential_io-YeAxg9Xn.js";
|
|
2
2
|
export { storeFor };
|