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