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.
Files changed (57) hide show
  1. package/README.md +48 -113
  2. package/dist/_chunks/{auth_selection-ktL6uoEe.js → auth_selection-pitMFzDe.js} +1 -1
  3. package/dist/_chunks/{credential_io-B1JxWELN.js → credential_io-YeAxg9Xn.js} +2 -2
  4. package/dist/_chunks/{paths-g3DRqJiD.js → paths-DMq0Lt7a.js} +1 -1
  5. package/dist/_chunks/{profiles-D95AeZ5v.js → profiles-CvnFEQyd.js} +1 -1
  6. package/dist/analytics.js +2 -2
  7. package/dist/auth_context.js +1 -1
  8. package/dist/commands/auth.js +12 -9
  9. package/dist/commands/bootstrap.js +72 -43
  10. package/dist/commands/claim.js +1 -1
  11. package/dist/commands/env.js +3 -20
  12. package/dist/commands/init.js +80 -79
  13. package/dist/commands/link.js +110 -292
  14. package/dist/commands/mcp.js +2 -2
  15. package/dist/commands/plugins.js +3 -3
  16. package/dist/commands/profile.js +4 -4
  17. package/dist/config.js +1 -1
  18. package/dist/credential_io.js +1 -1
  19. package/dist/index.js +1 -1
  20. package/dist/init/child.js +36 -0
  21. package/dist/init/plan.js +47 -0
  22. package/dist/init/tooling.js +43 -0
  23. package/dist/init/wizard.js +42 -0
  24. package/dist/mcp/targets.js +1 -1
  25. package/dist/plugins/plan.js +1 -1
  26. package/dist/plugins/targets.js +5 -1
  27. package/dist/retire_credential.js +1 -1
  28. package/dist/utils/cli_name.js +1 -12
  29. package/dist/utils/middlewares.js +1 -1
  30. package/dist/utils/package_manager.js +10 -5
  31. package/package.json +13 -14
  32. package/dist/init/auth.js +0 -53
  33. package/dist/init/build_config.js +0 -4
  34. package/dist/init/detect_agent.js +0 -63
  35. package/dist/init/editors.js +0 -19
  36. package/dist/init/enrich_output.js +0 -61
  37. package/dist/init/extension.js +0 -154
  38. package/dist/init/inspect.js +0 -249
  39. package/dist/init/interactive.js +0 -529
  40. package/dist/init/neonctl.js +0 -139
  41. package/dist/init/orchestrate.js +0 -189
  42. package/dist/init/phases/auth.js +0 -195
  43. package/dist/init/phases/cleanup.js +0 -27
  44. package/dist/init/phases/db.js +0 -257
  45. package/dist/init/phases/getting_started.js +0 -218
  46. package/dist/init/phases/mcp.js +0 -219
  47. package/dist/init/phases/migrations.js +0 -250
  48. package/dist/init/phases/neon_auth.js +0 -116
  49. package/dist/init/phases/setup.js +0 -582
  50. package/dist/init/phases/skills.js +0 -86
  51. package/dist/init/phases/status.js +0 -82
  52. package/dist/init/profile_cli.js +0 -56
  53. package/dist/init/resolve_context.js +0 -105
  54. package/dist/init/route_command.js +0 -93
  55. package/dist/init/skills.js +0 -206
  56. package/dist/init/types.js +0 -1
  57. package/dist/init/vsix.js +0 -89
@@ -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, renderAgentPullNote } from "./env.js";
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: `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).`,
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
- 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
64
+ hidden: true,
65
+ type: "boolean"
53
66
  },
54
67
  yes: {
55
68
  alias: "y",
56
- describe: "Skip the \"already linked\" confirmation in interactive mode and re-link anyway.",
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", `Link an existing project (org is inferred); pin a branch later with '${getCliName()} checkout'`],
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 (isCi()) {
115
+ if (!canPromptInteractively()) {
103
116
  log.error([
104
- "Missing inputs and CI environment detected (no TTY for prompts).",
117
+ "Missing inputs and no interactive terminal for prompts.",
105
118
  "",
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)`
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
- * 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).
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, agentCode) {
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.`, "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");
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.`, status === 403 ? "NO_ACCESS" : "NOT_FOUND");
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>\`.`, "NOT_FOUND");
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.`, "ORG_MISMATCH");
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
- * 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.
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 branchPersistValue(branch);
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 (projectId === existing.projectId) return contextBranch(existing);
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 branch = await resolvePinnedBranch(props, inputs, existing, inputs.projectId);
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 branch = await resolvePinnedBranch(props, inputs, existing, projectId);
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 branch = await resolveInteractiveBranch(props, action.projectId);
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(`No branch pinned. Run \`${getCliName()} checkout <branch>\` to pin a branch and pull its env vars.`);
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;
@@ -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,
@@ -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 { pluginsInstallableAgents } from "../plugins/targets.js";
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({
@@ -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-B1JxWELN.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-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 { o as isOwnedCredentialPath, r as credentialsPath } from "../_chunks/paths-g3DRqJiD.js";
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-D95AeZ5v.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-CvnFEQyd.js";
10
10
  import { auth, revokeToken } from "../auth.js";
11
- import { t as credentialInputs } from "../_chunks/auth_selection-ktL6uoEe.js";
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 isInsideConfigDir, i as defaultDir, o as isOwnedCredentialPath, r as credentialsPath, t as CREDENTIALS_FILE } from "./_chunks/paths-g3DRqJiD.js";
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 }) => {
@@ -1,2 +1,2 @@
1
- import { t as storeFor } from "./_chunks/credential_io-B1JxWELN.js";
1
+ import { t as storeFor } from "./_chunks/credential_io-YeAxg9Xn.js";
2
2
  export { storeFor };