neon 4.17.0 → 4.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -742,7 +742,7 @@ When a package cannot be bundled — a native addon with no esbuild loader, or a
742
742
 
743
743
  `neon bootstrap` copies a Neon starter template into a new (or current) directory — conceptually like `degit`, but it only pulls from a small set of templates we maintain in the public [`neondatabase/examples`](https://github.com/neondatabase/examples) repo. The template copy needs no Neon login: it downloads files from GitHub.
744
744
 
745
- After scaffolding, an interactive terminal asks about dependency install, git, agent tooling (the Neon plugin, or skills and MCP separately — never both), and `neon link` before running those steps. Dependency install is last, except when the template has a `neon.ts` and you chose to link — then install runs first so link can pull env. `--default` / `-y` skips the template, install, git, and agent pickers, then installs agent tooling for project folders, else the host CLI agent. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `--default` / `-y` in a terminal to pick. `--agent` / `-a` names coding agents, skips agent selection, and is forwarded to `plugins`, or to `skills` and `mcp`, not both. `link --yes` still asks for a project unless one is already linked. `--no-agent-setup` and `--no-link` skip those. Non-interactive without `--default` prints next steps and does not install, set up agents, or link.
745
+ After scaffolding, an interactive terminal asks about dependency install and git, then finishes agent setup before asking whether to link a Neon project. Dependency install is last, except when the template has a `neon.ts` and you chose to link — then install runs first so the link flow can pull env. `--default` / `-y` skips the template, install, git, and agent pickers, then installs agent tooling for project folders, else the host CLI agent. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `--default` / `-y` in a terminal to pick. `--agent` / `-a` names coding agents, skips agent selection, and is forwarded to `plugins`, or to `skills` and `mcp`, not both. Project selection may still be required. `--no-agent-setup` and `--no-link` skip those steps. Non-interactive without `--default` prints next steps and does not install, set up agents, or link.
746
746
 
747
747
  Pass a target directory (or `.` for the current one). In an interactive terminal you pick the template from a list; in CI / non-interactive contexts pass `--template <id>`.
748
748
 
@@ -773,18 +773,19 @@ An empty directory (nothing except `.git`) asks how to set up the directory. Pic
773
773
 
774
774
  `-y` in an empty directory scaffolds the default template (`bootstrap --default`) and does not add a later `config init` step. `--skip-template` skips scaffolding even with `-y`. `--template <id>` scaffolds that template; combine it with `-y` to skip the remaining bootstrap pickers.
775
775
 
776
- An existing app (or skip-template) installs agent tooling, then `neon link --no-config` unless `.neon` already has a projectId, then asks whether to create `neon.ts`. Saying no skips the services picker and does not write the file. `--no-config` does the same without asking. `-y` writes `config init --services none` unless `--no-config`. `--services` implies creating `neon.ts`.
776
+ An existing app (or skip-template) installs agent tooling, then asks whether to link unless `.neon` already has a projectId, then asks whether to create `neon.ts`. Linking uses the same organization, project, and branch flow as `neon link`. Saying no to `neon.ts` skips the services picker and does not write the file. `--no-config` does the same without asking. `-y` writes `config init --services none` unless `--no-config`. `--services` implies creating `neon.ts`.
777
777
 
778
- In an interactive terminal it offers one of: the Neon plugin (`neon plugins`), skills and MCP separately (`neon skills`, then `neon mcp`), or skip agent setup. It never runs plugin and skills+MCP together.
778
+ In an interactive terminal it offers one of: the Neon plugin (`neon plugins`), skills and MCP separately (`neon skills`, then `neon mcp`), or skip agent setup. Agent selection and setup finish before the link question. It never runs plugin and skills+MCP together.
779
779
 
780
780
  ```bash
781
781
  $ neon init
782
782
  $ neon init --skip-template
783
+ $ neon init --skip-template --no-link
783
784
  $ neon init -y
784
785
  $ neon init --agent cursor --agent claude-code
785
786
  ```
786
787
 
787
- Without a TTY, pass `-y`. `--agent` skips agent selection but does not replace `-y` for link or templates.
788
+ Without a TTY, pass `-y`. `--agent` skips agent selection but does not replace `-y` for link or templates. `--no-link` skips project linking without asking, including when a template is scaffolded.
788
789
 
789
790
  `-y` skips the template picker and the agent-setup offer. Empty dir: `bootstrap --default`. `--skip-template` or an existing app: plugin when Cursor, Claude Code, or Codex is in project folders, else the host CLI agent; otherwise skills and MCP. If none are found, it exits: pass `--agent <name>`, run from a supported agent, or omit `-y` in a terminal to pick. VS Code, GitHub Copilot CLI, and Grok only take the plugin user-level (`neon plugins --global`), so `-y` uses skills and MCP for those.
790
791
 
@@ -794,7 +795,7 @@ Without a TTY, pass `-y`. `--agent` skips agent selection but does not replace `
794
795
 
795
796
  `--config`, `--no-config`, and `--services` apply on the existing-app and `--skip-template` path. A template's own `neon.ts` is left as the template shipped it. Passing those flags while scaffolding prints a warning and still copies the template as shipped. After a new `neon.ts` on a pinned branch, init runs `env pull`.
796
797
 
797
- `-y` forwards `-y` to `plugins` or `skills`/`mcp`, `--default` to nested bootstrap, `--yes --no-config` to `link`, and `--services none` to `config init`. `--agent` is forwarded with them. `mcp -y` is the global install. `link --yes` only skips the "already linked" confirmation; it still asks for a project unless one is already linked.
798
+ `-y` forwards `-y` to `plugins` or `skills`/`mcp`, uses default choices in the built-in link flow, passes `--default` to nested bootstrap, and forwards `--services none` to `config init`. `--agent` is forwarded to agent setup. `mcp -y` is the global install. Default linking skips the "already linked" confirmation; it can still require project selection.
798
799
 
799
800
  A failed step stops the rest. `--profile` and `--config-dir` are forwarded to each child. `--output json` and `--output yaml` are refused; the commands init runs print their own output.
800
801
 
@@ -1228,7 +1229,7 @@ When both are only environment variables the key wins, which keeps a CI pipeline
1228
1229
 
1229
1230
  `neon auth` and the `profile` subcommands are outside all of this, because they read the same flags to mean something else: `neon auth --profile work` names where to write a credential, and `neon profile create work --api-key …` names one to store.
1230
1231
 
1231
- `neon init` forwards `--profile` and `--config-dir` to the commands it runs. An explicit `--api-key` is passed to those children through `NEON_API_KEY`, not argv.
1232
+ `neon init` forwards `--profile` and `--config-dir` to agent and config children and uses them directly for its link flow. An explicit `--api-key` is passed to authenticated children through `NEON_API_KEY`, not argv.
1232
1233
 
1233
1234
  ## API passthrough (`api`)
1234
1235
 
@@ -0,0 +1,61 @@
1
+ import { t as storeFor } from "./_chunks/credential_io-D8AR-jvB.js";
2
+ import { log } from "./log.js";
3
+ import { authFailureMessage, getAuthContext, locationFromContext } from "./auth_context.js";
4
+ import { AuthRefreshError } from "./auth.js";
5
+ import { isAccessTokenUsable, refreshStoredCredentials } from "./commands/auth.js";
6
+ //#region src/auth_recovery.ts
7
+ const supersededOnDisk = (rejectedToken) => {
8
+ if (rejectedToken === void 0) return false;
9
+ const context = getAuthContext();
10
+ if (context === null) return false;
11
+ const at = locationFromContext(context);
12
+ if (at === null) return false;
13
+ try {
14
+ const loaded = storeFor(context.configDir).read(at);
15
+ return loaded !== null && loaded.credentials.access_token !== rejectedToken && isAccessTokenUsable(loaded.credentials, Date.now());
16
+ } catch {
17
+ return false;
18
+ }
19
+ };
20
+ const recoverFrom401 = async (canRetry) => {
21
+ const context = getAuthContext();
22
+ if (context === null || context.source !== "stored-credentials") {
23
+ log.error(authFailureMessage(context));
24
+ return false;
25
+ }
26
+ if (context.refreshed === true || !canRetry) {
27
+ log.error(authFailureMessage(context));
28
+ return false;
29
+ }
30
+ if (supersededOnDisk(context.accessToken)) {
31
+ log.debug("The rejected token has already been replaced on disk; retrying with the current one");
32
+ return true;
33
+ }
34
+ const at = locationFromContext(context);
35
+ if (at === null || context.oauthHost === void 0 || context.clientId === void 0) {
36
+ log.error(authFailureMessage(context));
37
+ return false;
38
+ }
39
+ try {
40
+ if (await refreshStoredCredentials(at, {
41
+ apiHost: "",
42
+ oauthHost: context.oauthHost,
43
+ clientId: context.clientId,
44
+ configDir: context.configDir
45
+ })) {
46
+ log.debug("Refreshed the stored session after a 401; retrying");
47
+ return true;
48
+ }
49
+ } catch (err) {
50
+ if (err instanceof AuthRefreshError) {
51
+ log.error(err.message);
52
+ if (err.terminal) log.error(`Run \`neon auth --profile ${context.profile ?? "DEFAULT"}\` to sign in again.`);
53
+ return false;
54
+ }
55
+ log.debug("Refresh after 401 failed: %s", err instanceof Error ? err.message : "unknown error");
56
+ }
57
+ log.error(authFailureMessage(context));
58
+ return false;
59
+ };
60
+ //#endregion
61
+ export { recoverFrom401 };
@@ -1,19 +1,20 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
- import { getCliName } from "../utils/cli_name.js";
5
4
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
5
+ import { getCliName } from "../utils/cli_name.js";
6
+ import { helpCsv, helpEpilogue } from "../utils/help_text.js";
6
7
  import { writer } from "../writer.js";
7
8
  import { FALLBACK_TEMPLATES, ensureTargetUsable, fetchTemplates, findTemplate, scaffoldTemplate, templateIds } from "../init/bootstrap.js";
8
9
  import { spawnCliChild } from "../init/child.js";
9
10
  import { agentSetupDoneLabel, formatInitDone, printInitBanner, printInitDone, shouldPrintInitBanner } from "../init/chrome.js";
11
+ import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
12
+ import { runAuthenticatedLink } from "../init/link.js";
10
13
  import { coerceAgentFlag } from "../utils/agent_flag.js";
11
- import { assertNamedAgentTooling, chooseYesAgentTooling, initPluginAgents, initSkillsMcpAgents, planLinkStep, postScaffoldActions, projectContextFile, resolveNamedAgents } from "../init/plan.js";
14
+ import { assertNamedAgentTooling, chooseYesAgentTooling, initPluginAgents, initSkillsMcpAgents, postScaffoldActions, projectContextFile, resolveNamedAgents } from "../init/plan.js";
12
15
  import { formatTemplateTitle } from "../init/template_title.js";
13
- import { pickAgentSetupInteractively } from "../init/wizard.js";
14
- import { runAgentTooling, runInitSteps } from "../init/tooling.js";
15
- import { helpCsv, helpEpilogue } from "../utils/help_text.js";
16
- import { formatInstallCommand, inferPackageManager, installArgs, installedPackageManagers, resolvePackageManager, runCommand } from "../utils/package_manager.js";
16
+ import { pickAgentSetupInteractively, pickInitLinkInteractively } from "../init/wizard.js";
17
+ import { runAgentTooling } from "../init/tooling.js";
17
18
  import { existsSync } from "node:fs";
18
19
  import { join, relative, resolve } from "node:path";
19
20
  import prompts from "prompts";
@@ -47,7 +48,7 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
47
48
  },
48
49
  default: {
49
50
  alias: "y",
50
- describe: "Quick start: scaffold the default template (or --template), then install, git, agent tooling (project folders, else the host CLI agent; if none, pass --agent or omit --default in a terminal), and link --yes. Skips those pickers; link --yes still asks for a project unless one is already linked",
51
+ describe: "Quick start: scaffold the default template (or --template), then install, git, agent tooling (project folders, else the host CLI agent; if none, pass --agent or omit --default in a terminal), and link with defaults. Project selection may still be required",
51
52
  type: "boolean",
52
53
  default: false
53
54
  },
@@ -62,7 +63,7 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
62
63
  default: true
63
64
  },
64
65
  link: {
65
- describe: `Run \`${getCliName()} link\` after scaffolding. Templates with neon.ts link after install so env pull works; otherwise link runs before install. In interactive mode this is offered as a prompt; use --no-link to skip without being asked.`,
66
+ describe: "Link a Neon project after scaffolding. Templates with neon.ts link after install so env pull works; otherwise linking happens before install. In interactive mode this is offered as a prompt; use --no-link to skip without being asked.",
66
67
  type: "boolean",
67
68
  default: true
68
69
  },
@@ -78,7 +79,7 @@ const builder = (argv) => argv.usage("$0 bootstrap [directory] [options]").posit
78
79
  describe: "Coding agent to install into (repeatable). Forwarded to plugins, or to skills and mcp. Skips agent selection. Values listed below",
79
80
  coerce: coerceAgentFlag
80
81
  }
81
- }).example("$0 bootstrap my-app", "Create ./my-app from an interactively chosen template").example("$0 bootstrap . --template hono", "Scaffold the Hono template into the current directory").example("$0 bootstrap my-app --default", "Skip the pickers; link --yes still asks for a project unless one is already linked").example("$0 bootstrap --list-templates --output json", "Print the template catalog as JSON").example("$0 bootstrap my-app --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with --default.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).strict();
82
+ }).example("$0 bootstrap my-app", "Create ./my-app from an interactively chosen template").example("$0 bootstrap . --template hono", "Scaffold the Hono template into the current directory").example("$0 bootstrap my-app --default", "Skip the pickers; project selection may still be required").example("$0 bootstrap --list-templates --output json", "Print the template catalog as JSON").example("$0 bootstrap my-app --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with --default.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).strict();
82
83
  const handler = async (props) => {
83
84
  if (props.listTemplates) {
84
85
  const templates = await fetchTemplates();
@@ -215,7 +216,6 @@ const runPostScaffoldSteps = async (props, targetDir, interactive, template, nam
215
216
  const agentSetup = props.agentSetup === false ? "skip" : named.length > 0 ? chooseYesAgentTooling(named).setup : await (props.pickAgentSetup ?? pickAgentSetupInteractively)();
216
217
  const canLink = props.link && !(neonConfig && !wantInstall);
217
218
  if (props.link && !canLink) logSkippedLink(pm);
218
- const wantLink = canLink && await confirm(`Link this project to a Neon project now? (runs ${getCliName()} link)`);
219
219
  const outcome = await executePostScaffold(props, targetDir, {
220
220
  yes: false,
221
221
  lockAgentSetup: named.length === 0,
@@ -223,7 +223,7 @@ const runPostScaffoldSteps = async (props, targetDir, interactive, template, nam
223
223
  git: wantGit,
224
224
  agentSetup,
225
225
  install: wantInstall,
226
- link: wantLink,
226
+ link: canLink,
227
227
  hasNeonConfig: neonConfig,
228
228
  named
229
229
  });
@@ -239,9 +239,8 @@ const runPostScaffoldSteps = async (props, targetDir, interactive, template, nam
239
239
  return nestedResult(template, targetDir, neonConfig, outcome);
240
240
  };
241
241
  const installPrompt = (inferred) => inferred ? `Install dependencies with ${inferred}?` : "Install dependencies?";
242
- /** `link --yes` still asks for a project unless one is already linked. */
243
242
  const runDefaultSteps = async (props, targetDir, pm, neonConfig, template, named) => {
244
- log.info("Quick start (--default): skipping the template, install, git, and agent pickers. link --yes still asks for a project unless one is already linked.");
243
+ log.info("Quick start (--default): skipping the template, install, git, and agent pickers. Project selection may still be required.");
245
244
  const wantGit = props.git && !isGitRepo(targetDir);
246
245
  const agentSetup = props.agentSetup === false ? "skip" : named.length > 0 ? chooseYesAgentTooling(named).setup : "skills-mcp";
247
246
  const outcome = await executePostScaffold(props, targetDir, {
@@ -322,12 +321,26 @@ const executePostScaffold = async (props, targetDir, choices) => {
322
321
  logSkippedLink(choices.pm);
323
322
  continue;
324
323
  }
325
- await runInitSteps([props.linkNoConfig === true ? planLinkStep({
324
+ if (!(choices.yes || await (props.pickLink ?? pickInitLinkInteractively)())) continue;
325
+ await (props.linkProject ?? runAuthenticatedLink)({
326
+ apiClient: props.apiClient,
327
+ apiKey: props.apiKey,
328
+ apiHost: props.apiHost,
329
+ output: props.output,
330
+ contextFile: projectContextFile(targetDir, props.contextFile),
326
331
  yes: choices.yes,
327
- extra: props.linkExtra
328
- }) : choices.yes ? ["link", "--yes"] : ["link"]], {
332
+ clear: false,
333
+ checks: true,
334
+ envPull: true,
335
+ config: props.linkNoConfig !== true,
329
336
  cwd: targetDir,
330
- ...kids
337
+ ...props.linkInputs,
338
+ ...props.configDir ? { configDir: props.configDir } : {},
339
+ ...props.profile ? { profile: props.profile } : {},
340
+ ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
341
+ ...props.clientId ? { clientId: props.clientId } : {},
342
+ ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
343
+ ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {}
331
344
  });
332
345
  linked = true;
333
346
  }
@@ -3,13 +3,13 @@ import { log } from "../log.js";
3
3
  import { retryOnLock } from "../api.js";
4
4
  import { contextBranch, readContextFile } from "../context.js";
5
5
  import { writer } from "../writer.js";
6
- import { branchCreateRequest } from "../parameters.gen.js";
7
6
  import { EndpointType } from "../utils/api_enums.js";
8
- import { getComputeUnits } from "../utils/compute_units.js";
9
7
  import { looksLikeBranchId, looksLikeLSN, looksLikeTimestamp } from "../utils/formats.js";
10
8
  import { branchIdFromProps, branchIdResolve, fillSingleProject } from "../utils/enrichers.js";
11
- import { parsePointInTime } from "../utils/point_in_time.js";
9
+ import { branchCreateRequest } from "../parameters.gen.js";
10
+ import { getComputeUnits } from "../utils/compute_units.js";
12
11
  import { psql } from "../utils/psql.js";
12
+ import { parsePointInTime } from "../utils/point_in_time.js";
13
13
  import { parseSchemaDiffParams, schemaDiff } from "./schema_diff.js";
14
14
  //#region src/commands/branches.ts
15
15
  var branches_exports = /* @__PURE__ */ __exportAll({
@@ -4,9 +4,9 @@ import { log } from "../log.js";
4
4
  import { isNeonApiError } from "../api.js";
5
5
  import { applyContext, contextBranch, readContextFile } from "../context.js";
6
6
  import { getCliName } from "../utils/cli_name.js";
7
+ import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
7
8
  import { looksLikeBranchId } from "../utils/formats.js";
8
9
  import { fillSingleProject } from "../utils/enrichers.js";
9
- import { createBranch, pickBranchInteractively } from "../utils/branch_picker.js";
10
10
  import { autoPullEnvAfterPin } from "./env.js";
11
11
  import { applyPolicyOnCreate, createBranchFromPolicyOnCheckout, envFlag } from "./config.js";
12
12
  import { handler as handler$1 } from "./link.js";
@@ -3,12 +3,12 @@ import { isCi } from "../env.js";
3
3
  import { log } from "../log.js";
4
4
  import { getApiClient } from "../api.js";
5
5
  import { applyContext, contextBranch, readContextFile } from "../context.js";
6
- import { claimableDataApiCreateBody, declaredNeonServices } from "../config_services.js";
7
- import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
8
- import { resolveEnvFilePath } from "../env_file.js";
9
6
  import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
10
7
  import { ClaimableClient, ClaimableServiceError } from "../claimable/api.js";
11
8
  import { assertionHasExpired, listClaimableCredentials, readClaimableCredentials, readLinkedClaimableCredentials, removeClaimableCredentials, writeClaimableCredentials } from "../claimable/state.js";
9
+ import { claimableDataApiCreateBody, declaredNeonServices } from "../config_services.js";
10
+ import { NEON_SERVICES, deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
11
+ import { resolveEnvFilePath } from "../env_file.js";
12
12
  import { writer } from "../writer.js";
13
13
  import { noPassthrough } from "../utils/flags.js";
14
14
  import { pull } from "./env.js";
@@ -10,11 +10,11 @@ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOpt
10
10
  import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE, FUNCTION_FILENAME, FUNCTION_SLUG, FUNCTION_TEMPLATE, REQUIRED_PACKAGES, renderNeonConfig, renderNeonConfigFromView } from "../config_template.js";
11
11
  import { loadEnvFileIntoProcess } from "../env_file.js";
12
12
  import { writer } from "../writer.js";
13
- import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
14
- import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
15
13
  import { assertAiGatewayProvisionable, warnAiGateway } from "../utils/ai_gateway_notice.js";
16
14
  import { announceTargetBranch } from "../utils/branch_notice.js";
17
15
  import { renderAppliedChanges, renderBranchSettingConflicts } from "../utils/config_diff.js";
16
+ import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
17
+ import { formatInstallCommand, installArgs, resolvePackageManager, runCommand } from "../utils/package_manager.js";
18
18
  import { bundleEntry } from "../utils/esbuild.js";
19
19
  import { pickServicesInteractively } from "../utils/service_picker.js";
20
20
  import { zipBundle } from "../utils/zip.js";
@@ -2,8 +2,8 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { writer } from "../writer.js";
3
3
  import { EndpointType } from "../utils/api_enums.js";
4
4
  import { branchIdFromProps, fillSingleProject } from "../utils/enrichers.js";
5
- import { parsePITBranch } from "../utils/point_in_time.js";
6
5
  import { psql } from "../utils/psql.js";
6
+ import { parsePITBranch } from "../utils/point_in_time.js";
7
7
  //#region src/commands/connection_string.ts
8
8
  var connection_string_exports = /* @__PURE__ */ __exportAll({
9
9
  SSL_MODES: () => SSL_MODES,
@@ -1,9 +1,9 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
+ import { isClaimableEnvTarget } from "../claimable/state.js";
3
4
  import { getCliName } from "../utils/cli_name.js";
4
5
  import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
5
6
  import { y as functionBaseUrlKey } from "../_chunks/env_services-CbARRnq9.js";
6
- import { isClaimableEnvTarget } from "../claimable/state.js";
7
7
  import { branchIdResolve } from "../utils/enrichers.js";
8
8
  import { bundleEntry } from "../utils/esbuild.js";
9
9
  import { a as resolveDevEnv } from "../_chunks/env-B75xufe_.js";
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { ensureGitignored } from "../context.js";
4
+ import { isClaimableEnvTarget } from "../claimable/state.js";
4
5
  import { getCliName } from "../utils/cli_name.js";
5
6
  import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
6
7
  import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
7
8
  import { a as envKeysForSelection, b as isFunctionBaseUrlKey, c as ownedEnvServiceKeys, f as NEON_ENV_VAR_KEYS, i as ENV_PULL_SERVICES, l as parseEnvPullKeys, o as envPullFlagValue, r as ENV_PULL_KEY_HELP } from "../_chunks/env_services-CbARRnq9.js";
8
- import { isClaimableEnvTarget } from "../claimable/state.js";
9
- import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
10
9
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
11
10
  import { announceTargetBranch } from "../utils/branch_notice.js";
11
+ import { fillSingleProject, resolveBranchRef } from "../utils/enrichers.js";
12
12
  import { i as dropClaimableUnsupported, o as resolveNeonEnvVars } from "../_chunks/env-B75xufe_.js";
13
13
  import { existsSync } from "node:fs";
14
14
  import chalk from "chalk";
@@ -2,13 +2,13 @@ import { t as auth_exports } from "./auth.js";
2
2
  import { t as api_exports } from "./api.js";
3
3
  import { t as api_keys_exports } from "./api_keys.js";
4
4
  import { t as ask_exports } from "./ask.js";
5
- import { t as bootstrap_exports } from "./bootstrap.js";
6
- import { t as branches_exports } from "./branches.js";
7
- import { t as bucket_exports } from "./bucket.js";
8
5
  import { t as env_exports } from "./env.js";
9
6
  import { t as config_exports } from "./config.js";
10
7
  import { t as projects_exports } from "./projects.js";
11
8
  import { t as link_exports } from "./link.js";
9
+ import { t as bootstrap_exports } from "./bootstrap.js";
10
+ import { t as branches_exports } from "./branches.js";
11
+ import { t as bucket_exports } from "./bucket.js";
12
12
  import { t as checkout_exports } from "./checkout.js";
13
13
  import { t as claim_exports } from "./claim.js";
14
14
  import { t as connection_string_exports } from "./connection_string.js";
@@ -1,21 +1,22 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { contextBranch, readContextFile } from "../context.js";
4
+ import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
4
5
  import { getCliName } from "../utils/cli_name.js";
5
6
  import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption } from "../neon_services.js";
6
7
  import { CONFIG_INIT_NONE_MEANS, CONFIG_INIT_SERVICES, CONFIG_INIT_UNAVAILABLE } from "../config_template.js";
7
- import { t as credentialInputs } from "../_chunks/auth_selection-pitMFzDe.js";
8
+ import { helpCsv, helpEpilogue } from "../utils/help_text.js";
8
9
  import { fetchTemplates } from "../init/bootstrap.js";
9
10
  import { initChildEnv, spawnCliChild } from "../init/child.js";
10
11
  import { agentSetupLabel, configSummaryLabel, formatInitDone, printInitBanner, printInitDone, shouldPrintInitBanner } from "../init/chrome.js";
12
+ import { hasNeonConfigFile } from "./config.js";
13
+ import { runAuthenticatedLink } from "../init/link.js";
11
14
  import { coerceAgentFlag } from "../utils/agent_flag.js";
12
- import { INIT_NEEDS_YES_OR_TERMINAL, assertNamedAgentTooling, directoryIsEmpty, initPluginAgents, initSkillsMcpAgents, linkInputArgv, planExistingInit, resolveNamedAgents } from "../init/plan.js";
15
+ import { INIT_NEEDS_YES_OR_TERMINAL, assertNamedAgentTooling, directoryIsEmpty, initPluginAgents, initSkillsMcpAgents, planExistingInit, resolveNamedAgents } from "../init/plan.js";
13
16
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
14
- import { pickInitConfigInteractively, pickInitTemplateInteractively } from "../init/wizard.js";
17
+ import { pickInitConfigInteractively, pickInitLinkInteractively, pickInitTemplateInteractively } from "../init/wizard.js";
15
18
  import { runAgentTooling, runInitSteps } from "../init/tooling.js";
16
- import { helpCsv, helpEpilogue } from "../utils/help_text.js";
17
19
  import { handler as handler$1 } from "./bootstrap.js";
18
- import { hasNeonConfigFile } from "./config.js";
19
20
  import { INIT_CONFIG_SERVICES_CONFLICT, INIT_TEMPLATE_KEEPS_CONFIG, configPlanFromResolution, resolveInitConfigChoice, resolveInitTemplateChoice, shouldRefreshEnvAfterNewConfig } from "../init/choices.js";
20
21
  import { existsSync, readdirSync } from "node:fs";
21
22
  import { resolve } from "node:path";
@@ -34,7 +35,7 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
34
35
  alias: "y",
35
36
  type: "boolean",
36
37
  default: false,
37
- describe: "Empty dir: scaffold the default template. --skip-template: plugin, or skills and MCP, for project folders, else the host CLI agent. Exits if none. Then link --yes and config init --services none. link --yes still asks for a project unless one is already linked"
38
+ describe: "Empty dir: scaffold the default template. --skip-template: plugin, or skills and MCP, for project folders, else the host CLI agent. Exits if none. Then link with defaults and create the bare neon.ts policy. Project selection may still be required"
38
39
  }).option("skip-template", {
39
40
  type: "boolean",
40
41
  default: false,
@@ -42,6 +43,10 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
42
43
  }).option("template", {
43
44
  type: "string",
44
45
  describe: "Template to scaffold into an empty directory. Conflicts with --skip-template"
46
+ }).option("link", {
47
+ type: "boolean",
48
+ default: true,
49
+ describe: "Link a Neon project during setup. Use --no-link to skip without being asked"
45
50
  }).option("config", {
46
51
  type: "boolean",
47
52
  describe: "Existing app or --skip-template: create neon.ts after linking. Use --no-config to skip. Omitted in a terminal: you will be asked. Scaffolding a template keeps that template's neon.ts"
@@ -80,7 +85,7 @@ const builder = (yargs) => yargs.usage("$0 init [options]").option("context-file
80
85
  alias: "o",
81
86
  hidden: true,
82
87
  describe: "Not supported; the commands init runs print their own output"
83
- }).example("$0 init", "Set up this directory (template picker if empty)").example("$0 init --skip-template", "Empty dir: agents, link, and neon.ts — no template files").example("$0 init -y", "Default template, or existing-app defaults").example("$0 init --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("Empty directory: pick a starter template, or skip scaffolding and only set up agents, a Neon project, and neon.ts. That skip is not on `neon bootstrap`.", "Interactive agent setup: plugin (recommended), skills and MCP separately, or skip agent setup. Never both plugin and skills+MCP.", "neon.ts is optional when you skip the template or set up an existing app. Saying no skips the services picker and does not write the file. Scaffolding a template keeps that template's neon.ts.", "-y installs the plugin when Cursor, Claude Code, or Codex is in project folders, else the host CLI agent. Otherwise skills and MCP. If none are found, it exits: pass --agent <name>, run from a supported agent, or omit -y in a terminal to pick. Then link unless already linked. link --yes may still ask for a project.", "--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with -y.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).check((argv) => {
88
+ }).example("$0 init", "Set up this directory (template picker if empty)").example("$0 init --skip-template", "Empty dir: agents, link, and neon.ts — no template files").example("$0 init -y", "Default template, or existing-app defaults").example("$0 init --agent cursor --agent claude-code", "Skip agent selection; install the plugin for those agents").epilogue(helpEpilogue("Empty directory: pick a starter template, or skip scaffolding and only set up agents, a Neon project, and neon.ts. That skip is not on `neon bootstrap`.", "Interactive agent setup: plugin (recommended), skills and MCP separately, or skip agent setup. Never both plugin and skills+MCP.", "neon.ts is optional when you skip the template or set up an existing app. Saying no skips the services picker and does not write the file. Scaffolding a template keeps that template's neon.ts.", "-y installs the plugin when Cursor, Claude Code, or Codex is in project folders, else the host CLI agent. Otherwise skills and MCP. If none are found, it exits: pass --agent <name>, run from a supported agent, or omit -y in a terminal to pick. Then link unless already linked. Project selection may still be required.", "--agent / -a is forwarded to plugins, or to skills and mcp, not both. It skips agent selection, including with -y.", helpCsv("Plugin agents", initPluginAgents()), helpCsv("Skills and MCP agents", initSkillsMcpAgents()))).check((argv) => {
84
89
  if (argv.data !== void 0) throw new Error(removedProtocol());
85
90
  if (argv.help !== true && (argv.output === "json" || argv.output === "yaml")) throw new Error(`\`${getCliName()} init\` does not support --output. The commands it runs print their own output.`);
86
91
  return true;
@@ -101,7 +106,7 @@ const parsedInitServices = (raw) => {
101
106
  onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical))
102
107
  });
103
108
  };
104
- const nestedBootstrapProps = (props, cwd, contextFile, template, linkExtra) => ({
109
+ const nestedBootstrapProps = (props, cwd, contextFile, template, linkInputs) => ({
105
110
  apiClient: props.apiClient,
106
111
  apiKey: props.apiKey,
107
112
  apiHost: props.apiHost,
@@ -113,19 +118,25 @@ const nestedBootstrapProps = (props, cwd, contextFile, template, linkExtra) => (
113
118
  default: template.useDefault,
114
119
  install: true,
115
120
  git: true,
116
- link: true,
121
+ link: props.link !== false,
117
122
  printBanner: false,
118
123
  skipDoneSummary: true,
119
124
  linkNoConfig: true,
120
125
  narrate: "human",
121
126
  ...template.selected ? { selectedTemplate: template.selected } : {},
122
127
  ...!template.selected && template.id !== void 0 ? { template: template.id } : {},
123
- ...linkExtra.length > 0 ? { linkExtra } : {},
128
+ linkInputs,
124
129
  ...props.agent !== void 0 ? { agent: props.agent } : {},
125
130
  ...props.configDir ? { configDir: props.configDir } : {},
126
131
  ...props.profile ? { profile: props.profile } : {},
132
+ ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
133
+ ...props.clientId ? { clientId: props.clientId } : {},
134
+ ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
135
+ ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {},
127
136
  ...props.analytics === false ? { analytics: false } : {},
128
137
  ...props.run ? { run: props.run } : {},
138
+ ...props.linkProject ? { linkProject: props.linkProject } : {},
139
+ ...props.pickLink ? { pickLink: props.pickLink } : {},
129
140
  ...props.pickAgentSetup ? { pickAgentSetup: props.pickAgentSetup } : {},
130
141
  ...props.detectProjectAgents ? { detectProjectAgents: props.detectProjectAgents } : {},
131
142
  ...props.detectAgent ? { detectAgent: props.detectAgent } : {},
@@ -193,13 +204,14 @@ const handler = async (props) => {
193
204
  };
194
205
  const services = parsedInitServices(props.services);
195
206
  if (props.config === false && services !== void 0) throw new Error(INIT_CONFIG_SERVICES_CONFLICT);
196
- const linkExtra = linkInputArgv({
207
+ const linkInputs = {
197
208
  ...props.orgId ? { orgId: props.orgId } : {},
198
209
  ...props.projectId ? { projectId: props.projectId } : {},
199
210
  ...props.projectName ? { projectName: props.projectName } : {},
200
211
  ...props.regionId ? { regionId: props.regionId } : {},
201
212
  ...props.branch ? { branch: props.branch } : {}
202
- });
213
+ };
214
+ const hasExplicitLinkInputs = props.orgId !== void 0 || props.projectId !== void 0 || props.projectName !== void 0 || props.regionId !== void 0 || props.branch !== void 0;
203
215
  const templateChoice = resolveInitTemplateChoice({
204
216
  empty: directoryIsEmpty(names),
205
217
  yes,
@@ -214,7 +226,7 @@ const handler = async (props) => {
214
226
  const result = await runBootstrap(nestedBootstrapProps(props, cwd, contextFile, {
215
227
  useDefault: yes,
216
228
  ...templateChoice.kind === "template" ? { id: templateChoice.id } : {}
217
- }, linkExtra));
229
+ }, linkInputs));
218
230
  printNestedBootstrapDone(result, cwd, templateChoice.kind === "template" ? templateChoice.id : "default");
219
231
  return;
220
232
  }
@@ -228,13 +240,13 @@ const handler = async (props) => {
228
240
  const result = await runBootstrap(nestedBootstrapProps(props, cwd, contextFile, {
229
241
  selected: picked.template,
230
242
  useDefault: false
231
- }, linkExtra));
243
+ }, linkInputs));
232
244
  printNestedBootstrapDone(result, cwd, picked.template.id);
233
245
  return;
234
246
  }
235
247
  }
236
248
  const alreadyLinked = isLinked(contextFile);
237
- const shouldLink = !alreadyLinked || linkExtra.length > 0;
249
+ const shouldLink = props.link !== false && (!alreadyLinked || hasExplicitLinkInputs);
238
250
  const existingConfig = hasNeonConfigFile(cwd);
239
251
  const canAskConfig = props.pickConfig !== void 0 || canPickAgentsInteractively();
240
252
  const agentSetup = await runAgentTooling({
@@ -251,18 +263,27 @@ const handler = async (props) => {
251
263
  ...props.detectAgent ? { detectAgent: props.detectAgent } : {},
252
264
  ...props.hasProjectPlugins ? { hasProjectPlugins: props.hasProjectPlugins } : {}
253
265
  });
254
- await runInitSteps(planExistingInit({
255
- linked: !shouldLink,
266
+ const canAskLink = props.pickLink !== void 0 || canPickAgentsInteractively();
267
+ const acceptedLink = shouldLink && (yes || !canAskLink || await (props.pickLink ?? pickInitLinkInteractively)());
268
+ if (acceptedLink) await (props.linkProject ?? runAuthenticatedLink)({
269
+ apiClient: props.apiClient,
270
+ apiKey: props.apiKey,
271
+ apiHost: props.apiHost,
272
+ output: props.output,
273
+ contextFile,
256
274
  yes,
257
- agentSetup: "skip",
258
- config: { kind: "skip" },
259
- ...linkExtra.length > 0 ? { linkExtra } : {}
260
- }), {
275
+ clear: false,
276
+ checks: true,
277
+ envPull: true,
278
+ config: false,
261
279
  cwd,
262
- run,
263
- forward,
264
- authEnv,
265
- narrate: "human"
280
+ ...linkInputs,
281
+ ...props.configDir ? { configDir: props.configDir } : {},
282
+ ...props.profile ? { profile: props.profile } : {},
283
+ ...props.oauthHost ? { oauthHost: props.oauthHost } : {},
284
+ ...props.clientId ? { clientId: props.clientId } : {},
285
+ ...props.forceAuth !== void 0 ? { forceAuth: props.forceAuth } : {},
286
+ ...props.allowUnsafeTls !== void 0 ? { allowUnsafeTls: props.allowUnsafeTls } : {}
266
287
  });
267
288
  const configResolution = resolveInitConfigChoice({
268
289
  flag: props.config,
@@ -317,7 +338,7 @@ Run \`${getCliName()} env pull\` once resolved.`, message);
317
338
  label: "Project",
318
339
  value: projectRow({
319
340
  alreadyLinked,
320
- linkedNow: shouldLink
341
+ linkedNow: acceptedLink
321
342
  })
322
343
  },
323
344
  {
@@ -2,8 +2,8 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { getCliName } from "../utils/cli_name.js";
4
4
  import { writer } from "../writer.js";
5
- import { projectUpdateRequest } from "../parameters.gen.js";
6
5
  import { fillSingleProject } from "../utils/enrichers.js";
6
+ import { projectUpdateRequest } from "../parameters.gen.js";
7
7
  //#region src/commands/ip_allow.ts
8
8
  var ip_allow_exports = /* @__PURE__ */ __exportAll({
9
9
  builder: () => builder,
@@ -16,6 +16,7 @@ var link_exports = /* @__PURE__ */ __exportAll({
16
16
  command: () => command,
17
17
  describe: () => describe,
18
18
  handler: () => handler,
19
+ runLink: () => runLink,
19
20
  shouldOfferConfigInit: () => shouldOfferConfigInit
20
21
  });
21
22
  const PROJECTS_LIST_LIMIT = 100;
@@ -94,7 +95,7 @@ const builder = (argv) => argv.usage("$0 link [options]").options({
94
95
  ["$0 link --no-checks --org-id org-… --project-id polished-snowflake-12345678", "Write the context offline (no API calls, no verification)"],
95
96
  ["$0 link --clear", "Forget the current org/project/branch context"]
96
97
  ]).epilogue(helpEpilogue("Non-interactive (CI, scripts, agents):", ...nonInteractiveLinkCommands().map((command) => ` ${command}`), orgScopedKeyHint)).strict();
97
- const handler = async (props) => {
98
+ const runLink = async (props) => {
98
99
  if (props.clear) {
99
100
  clearContext(props.contextFile);
100
101
  return;
@@ -110,19 +111,16 @@ const handler = async (props) => {
110
111
  await runNonInteractive(props, inputs, existing);
111
112
  return;
112
113
  }
113
- if (!canPromptInteractively()) {
114
- log.error([
115
- "Missing inputs and no interactive terminal for prompts.",
116
- "",
117
- "Use:",
118
- nonInteractiveLinkHelp(),
119
- orgScopedKeyHint
120
- ].join("\n"));
121
- process.exit(1);
122
- return;
123
- }
114
+ if (!canPromptInteractively()) throw new LinkInputError([
115
+ "Missing inputs and no interactive terminal for prompts.",
116
+ "",
117
+ "Use:",
118
+ nonInteractiveLinkHelp(),
119
+ orgScopedKeyHint
120
+ ].join("\n"));
124
121
  await runInteractive(props, inputs);
125
122
  };
123
+ const handler = runLink;
126
124
  const parseInputs = (props) => {
127
125
  let fromParams = {};
128
126
  if (props.params !== void 0 && props.params !== "") {
@@ -430,7 +428,9 @@ const runNonInteractive = async (props, inputs, existing) => {
430
428
  }
431
429
  };
432
430
  const runInteractive = async (props, inputs) => {
433
- if (!props.yes) await confirmRelinkIfNeeded(props);
431
+ if (!props.yes) {
432
+ if (!await confirmRelinkIfNeeded(props)) return;
433
+ }
434
434
  const orgResolution = await resolveOrg(props, inputs.orgId);
435
435
  let orgId;
436
436
  if (orgResolution.kind === "resolved") {
@@ -505,7 +505,7 @@ const runInteractive = async (props, inputs) => {
505
505
  };
506
506
  const confirmRelinkIfNeeded = async (props) => {
507
507
  const existing = readContextFile(props.contextFile);
508
- if (!existing.orgId || !existing.projectId) return;
508
+ if (!existing.orgId || !existing.projectId) return true;
509
509
  const { proceed } = await prompts({
510
510
  onState: onPromptState,
511
511
  type: "confirm",
@@ -515,8 +515,9 @@ const confirmRelinkIfNeeded = async (props) => {
515
515
  });
516
516
  if (!proceed) {
517
517
  process.stdout.write("Aborted. Existing link preserved.\n");
518
- process.exit(0);
518
+ return false;
519
519
  }
520
+ return true;
520
521
  };
521
522
  const promptOrgFromList = async (orgs) => {
522
523
  if (!orgs.length) throw new Error(`You don't belong to any organizations. Create one in the Neon Console first: https://console.neon.tech/`);
@@ -734,6 +735,7 @@ const finalizeLink = async (props, summary) => {
734
735
  const { config: _offerConfig, ...rest } = props;
735
736
  await autoPullEnvAfterPin({
736
737
  ...rest,
738
+ ...props.cwd ? { cwd: props.cwd } : {},
737
739
  projectId: summary.projectId,
738
740
  branch: summary.branch,
739
741
  envPull: props.envPull
@@ -756,7 +758,7 @@ const finalizeInteractiveLink = async (props, summary) => {
756
758
  */
757
759
  const shouldOfferConfigInit = (input) => !input.hasConfig && input.offer;
758
760
  const maybeOfferConfigInit = async (props, summary) => {
759
- const cwd = process.cwd();
761
+ const cwd = props.cwd ?? process.cwd();
760
762
  if (!shouldOfferConfigInit({
761
763
  hasConfig: hasNeonConfigFile(cwd),
762
764
  offer: props.config !== false
@@ -777,6 +779,7 @@ const maybeOfferConfigInit = async (props, summary) => {
777
779
  const { config: _offerConfig, ...rest } = props;
778
780
  await autoPullEnvAfterPin({
779
781
  ...rest,
782
+ cwd,
780
783
  projectId: summary.projectId,
781
784
  branch: summary.branch,
782
785
  envPull: props.envPull
@@ -795,4 +798,4 @@ const mustString = (value, name) => {
795
798
  return value;
796
799
  };
797
800
  //#endregion
798
- export { builder, command, describe, handler, shouldOfferConfigInit, link_exports as t };
801
+ export { builder, command, describe, handler, runLink, shouldOfferConfigInit, link_exports as t };
@@ -2,12 +2,12 @@ import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
3
  import { readContextFile } from "../context.js";
4
4
  import { getCliName } from "../utils/cli_name.js";
5
+ import { helpCsv, helpEpilogue } from "../utils/help_text.js";
5
6
  import { writer } from "../writer.js";
6
7
  import { noPassthrough, single } from "../utils/flags.js";
7
8
  import { NEON_MCP_CATEGORIES, NEON_MCP_URL, existingNeonApiKey, installNeonMcpServer, neonMcpUrl, parseMcpCategories, trackedProjectMcpConfig } from "../mcp/install.js";
8
9
  import { mcpInstallableAgents, resolveInstallTargets } from "../mcp/targets.js";
9
10
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
10
- import { helpCsv, helpEpilogue } from "../utils/help_text.js";
11
11
  import { mintMcpApiKey, mintedKeyRevokeCommand, withdrawMintedKey } from "../mcp/mint.js";
12
12
  import { confirmMcpInstall } from "../mcp/wizard.js";
13
13
  import { resolveMcpPlan } from "../mcp/plan.js";
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
+ import { helpCsv, helpEpilogue } from "../utils/help_text.js";
3
4
  import { writer } from "../writer.js";
4
5
  import { noPassthrough } from "../utils/flags.js";
5
6
  import { getAgentDisplayName } from "../mcp/agents.js";
@@ -8,7 +9,6 @@ import "../init/agents.js";
8
9
  import { pluginsInstallableAgents } from "../plugins/targets.js";
9
10
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
10
11
  import { NEON_PLUGIN_NAME, PLUGIN_SKILLS, PLUGIN_SOURCE, neonPluginsRetryCommand, pluginsAddArgs, runPluginsCli } from "../plugins/run.js";
11
- import { helpCsv, helpEpilogue } from "../utils/help_text.js";
12
12
  import { resolvePluginsPlan } from "../plugins/plan.js";
13
13
  import { confirmPluginsInstall } from "../plugins/wizard.js";
14
14
  //#region src/commands/plugins.ts
@@ -1,12 +1,12 @@
1
1
  import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
2
2
  import { log } from "../log.js";
3
+ import { helpCsv, helpEpilogue } from "../utils/help_text.js";
3
4
  import { writer } from "../writer.js";
4
5
  import { noPassthrough } from "../utils/flags.js";
5
6
  import { getAgentDisplayName } from "../mcp/agents.js";
6
7
  import "../init/agents.js";
7
8
  import { mappedSkillsAgentNames, skillsInstallableAgents } from "../skills/targets.js";
8
9
  import { canPickAgentsInteractively } from "../utils/agent_picker.js";
9
- import { helpCsv, helpEpilogue } from "../utils/help_text.js";
10
10
  import { skillsHelpValues, skillsYesHelp } from "../skills/catalog.js";
11
11
  import { confirmSkillsInstall, confirmSkillsUpdate } from "../skills/wizard.js";
12
12
  import { assertSkillsCanRun, resolveSkillsPlan } from "../skills/plan.js";
package/dist/help.js CHANGED
@@ -1,8 +1,75 @@
1
+ import { globalOptionsTrailer, helpWidth, wrapHelpText } from "./utils/help_text.js";
1
2
  import { consumeBlockIfMatches, consumeNextMatching, drawPointer, splitColumns } from "./utils/ui.js";
2
3
  import chalk from "chalk";
3
4
  import cliui from "cliui";
4
5
  //#region src/help.ts
5
6
  const SPACE_WIDTH = 20;
7
+ const DESCRIPTION_GUTTER = 4;
8
+ const wrapDescription = (text) => wrapHelpText(text, Math.max(1, helpWidth() - SPACE_WIDTH - DESCRIPTION_GUTTER));
9
+ const isGlobalOptionsHeader = (header) => /global options:/i.test(header);
10
+ const isTopLevelUsage = (usage) => /^\S+ <command>/.test(usage);
11
+ const renderOptionBlock = (optionsBlock) => {
12
+ const result = [];
13
+ const [header, ...body] = optionsBlock;
14
+ if (header === void 0) return result;
15
+ result.push(header);
16
+ body.forEach((line) => {
17
+ const [option, description] = splitColumns(line);
18
+ const ui = cliui({
19
+ width: helpWidth(),
20
+ wrap: false
21
+ });
22
+ if (option.startsWith("-")) {
23
+ ui.div({
24
+ text: chalk.green(option),
25
+ padding: [
26
+ 0,
27
+ 0,
28
+ 0,
29
+ 0
30
+ ]
31
+ });
32
+ ui.div({
33
+ text: chalk.gray(drawPointer(SPACE_WIDTH)),
34
+ width: SPACE_WIDTH,
35
+ padding: [
36
+ 0,
37
+ 2,
38
+ 0,
39
+ 0
40
+ ]
41
+ }, {
42
+ text: chalk.rgb(210, 210, 210)(wrapDescription(description ?? "")),
43
+ padding: [
44
+ 0,
45
+ 0,
46
+ 0,
47
+ 0
48
+ ]
49
+ });
50
+ } else ui.div({
51
+ padding: [
52
+ 0,
53
+ 0,
54
+ 0,
55
+ 0
56
+ ],
57
+ text: "",
58
+ width: SPACE_WIDTH
59
+ }, {
60
+ text: chalk.rgb(210, 210, 210)(wrapDescription(option)),
61
+ padding: [
62
+ 0,
63
+ 0,
64
+ 0,
65
+ 0
66
+ ]
67
+ });
68
+ result.push(ui.toString());
69
+ });
70
+ result.push("");
71
+ return result;
72
+ };
6
73
  const formatHelp = (help) => {
7
74
  const lines = help.split("\n");
8
75
  const result = [];
@@ -11,11 +78,16 @@ const formatHelp = (help) => {
11
78
  result.push(chalk.bold(topLevelCommand.replace("[options]", chalk.reset.green("[options]"))));
12
79
  result.push("");
13
80
  }
14
- const commandsBlock = consumeBlockIfMatches(lines, /^Commands:/);
15
- if (commandsBlock.length > 0) {
81
+ const consumeCommands = () => {
82
+ const commandsBlock = consumeBlockIfMatches(lines, /^Commands:/);
83
+ if (commandsBlock.length === 0) return;
16
84
  const header = commandsBlock.shift();
85
+ if (header === void 0) return;
17
86
  result.push(header);
18
- const ui = cliui({ width: 0 });
87
+ const ui = cliui({
88
+ width: helpWidth(),
89
+ wrap: false
90
+ });
19
91
  commandsBlock.forEach((line) => {
20
92
  if (/^\s{3,}/.exec(line)) {
21
93
  ui.div({
@@ -28,7 +100,7 @@ const formatHelp = (help) => {
28
100
  0
29
101
  ]
30
102
  }, {
31
- text: line.trim(),
103
+ text: wrapDescription(line.trim()),
32
104
  padding: [
33
105
  0,
34
106
  0,
@@ -54,7 +126,7 @@ const formatHelp = (help) => {
54
126
  0
55
127
  ]
56
128
  }, {
57
- text: description,
129
+ text: wrapDescription(description),
58
130
  padding: [
59
131
  0,
60
132
  0,
@@ -65,12 +137,17 @@ const formatHelp = (help) => {
65
137
  });
66
138
  result.push(ui.toString());
67
139
  result.push("");
68
- }
69
- const positionalsBlock = consumeBlockIfMatches(lines, /Positionals:/);
70
- if (positionalsBlock.length > 0) {
140
+ };
141
+ const consumePositionals = () => {
142
+ const positionalsBlock = consumeBlockIfMatches(lines, /Positionals:/);
143
+ if (positionalsBlock.length === 0) return;
71
144
  const header = positionalsBlock.shift();
145
+ if (header === void 0) return;
72
146
  result.push(header);
73
- const ui = cliui({ width: 0 });
147
+ const ui = cliui({
148
+ width: helpWidth(),
149
+ wrap: false
150
+ });
74
151
  positionalsBlock.forEach((line) => {
75
152
  const [positional, description] = splitColumns(line);
76
153
  ui.div({
@@ -83,7 +160,7 @@ const formatHelp = (help) => {
83
160
  0
84
161
  ]
85
162
  }, {
86
- text: description,
163
+ text: wrapDescription(description),
87
164
  padding: [
88
165
  0,
89
166
  0,
@@ -94,73 +171,42 @@ const formatHelp = (help) => {
94
171
  });
95
172
  result.push(ui.toString());
96
173
  result.push("");
97
- }
174
+ };
175
+ consumeCommands();
176
+ consumePositionals();
98
177
  const descriptionBlock = consumeBlockIfMatches(lines, /^(?!.*options:)/i);
99
178
  if (descriptionBlock.length > 0) {
100
179
  result.push(...descriptionBlock);
101
180
  result.push("");
102
181
  }
182
+ consumeCommands();
183
+ consumePositionals();
184
+ const optionBlocks = [];
103
185
  while (true) {
104
186
  const optionsBlock = consumeBlockIfMatches(lines, /.*options:/i);
105
187
  if (optionsBlock.length === 0) break;
106
- result.push(optionsBlock.shift());
107
- optionsBlock.forEach((line) => {
108
- const [option, description] = splitColumns(line);
109
- const ui = cliui({ width: 0 });
110
- if (option.startsWith("-")) {
111
- ui.div({
112
- text: chalk.green(option),
113
- padding: [
114
- 0,
115
- 0,
116
- 0,
117
- 0
118
- ]
119
- });
120
- ui.div({
121
- text: chalk.gray(drawPointer(SPACE_WIDTH)),
122
- width: SPACE_WIDTH,
123
- padding: [
124
- 0,
125
- 2,
126
- 0,
127
- 0
128
- ]
129
- }, {
130
- text: chalk.rgb(210, 210, 210)(description ?? ""),
131
- padding: [
132
- 0,
133
- 0,
134
- 0,
135
- 0
136
- ]
137
- });
138
- } else ui.div({
139
- padding: [
140
- 0,
141
- 0,
142
- 0,
143
- 0
144
- ],
145
- text: "",
146
- width: SPACE_WIDTH
147
- }, {
148
- text: chalk.rgb(210, 210, 210)(option),
149
- padding: [
150
- 0,
151
- 0,
152
- 0,
153
- 0
154
- ]
155
- });
156
- result.push(ui.toString());
157
- });
188
+ optionBlocks.push(optionsBlock);
189
+ }
190
+ for (const block of optionBlocks) {
191
+ const header = block[0];
192
+ if (header !== void 0 && !isGlobalOptionsHeader(header)) result.push(...renderOptionBlock(block));
193
+ }
194
+ const globalBlocks = optionBlocks.filter((block) => {
195
+ const header = block[0];
196
+ return header !== void 0 && isGlobalOptionsHeader(header);
197
+ });
198
+ if (topLevelCommand !== null && isTopLevelUsage(topLevelCommand)) for (const block of globalBlocks) result.push(...renderOptionBlock(block));
199
+ else if (globalBlocks.length > 0 && topLevelCommand !== null) {
200
+ result.push(globalOptionsTrailer(topLevelCommand));
158
201
  result.push("");
159
202
  }
160
203
  const exampleBlock = consumeBlockIfMatches(lines, /Examples:/);
161
204
  if (exampleBlock.length > 0) {
162
205
  result.push(exampleBlock.shift());
163
- const ui = cliui({ width: 0 });
206
+ const ui = cliui({
207
+ width: helpWidth(),
208
+ wrap: false
209
+ });
164
210
  for (const line of exampleBlock) {
165
211
  const [command, description] = splitColumns(line);
166
212
  ui.div({
@@ -173,7 +219,7 @@ const formatHelp = (help) => {
173
219
  ]
174
220
  });
175
221
  ui.div({
176
- text: chalk.reset(description),
222
+ text: chalk.reset(wrapDescription(description)),
177
223
  padding: [
178
224
  0,
179
225
  0,
@@ -188,7 +234,16 @@ const formatHelp = (help) => {
188
234
  };
189
235
  const showHelp = async (argv) => {
190
236
  const help = await argv.getHelp();
191
- process.stderr.write(formatHelp(help).join("\n") + "\n");
237
+ const text = `${formatHelp(help).join("\n")}\n`;
238
+ await new Promise((resolve, reject) => {
239
+ process.stdout.write(text, (err) => {
240
+ if (err) {
241
+ reject(err);
242
+ return;
243
+ }
244
+ resolve();
245
+ });
246
+ });
192
247
  process.exit(0);
193
248
  };
194
249
  //#endregion
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- import { t as storeFor } from "./_chunks/credential_io-D8AR-jvB.js";
2
1
  import { log } from "./log.js";
3
2
  import pkg_default from "./pkg.js";
4
3
  import { isNeonApiError, messageFromBody } from "./api.js";
5
- import { authFailureMessage, getAuthContext, locationFromContext } from "./auth_context.js";
4
+ import { getAuthContext } from "./auth_context.js";
6
5
  import { r as defaultDir } from "./_chunks/paths-DMq0Lt7a.js";
7
6
  import { ensureConfigDir } from "./config.js";
8
7
  import { currentContextFile, enrichFromContext } from "./context.js";
9
8
  import { analyticsMiddleware, closeAnalytics, getAnalyticsEventProperties, initAnalyticsClientMiddleware, sendError, trackEvent } from "./analytics.js";
10
9
  import { NETWORK_ERROR_MESSAGE, isNetworkError, matchErrorCode } from "./errors.js";
11
10
  import { AuthRefreshError, defaultClientID } from "./auth.js";
11
+ import { ensureAuth } from "./commands/auth.js";
12
+ import { recoverFrom401 } from "./auth_recovery.js";
12
13
  import { getCliName } from "./utils/cli_name.js";
13
14
  import { showHelp } from "./help.js";
14
- import { ensureAuth, isAccessTokenUsable, refreshStoredCredentials } from "./commands/auth.js";
15
15
  import { rewriteUnknownAgentArg } from "./init/plan.js";
16
16
  import commands_default from "./commands/index.js";
17
17
  import { fillInArgs, resolveApiKeyFromEnv } from "./utils/middlewares.js";
@@ -100,6 +100,7 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
100
100
  },
101
101
  "context-file": {
102
102
  describe: "Context file",
103
+ group: "Global options:",
103
104
  type: "string",
104
105
  default: currentContextFile
105
106
  },
@@ -124,63 +125,6 @@ builder = builder.scriptName(pkg_default.name).locale("en").usage("$0 <command>
124
125
  }).alias("help", "h").middleware(async (args) => {
125
126
  if (args.help || args._.length === 1 && !NO_SUBCOMMANDS_VERBS.includes(args._[0])) await showHelp(builder);
126
127
  }).middleware(ensureAuth).middleware(enrichFromContext).middleware(analyticsMiddleware).command(commands_default).strictCommands().version(pkg_default.version).group("version", "Global options:").alias("version", "v").completion().scriptName(getCliName()).epilog("For more information, visit https://neon.com/docs/reference/neon-cli").wrap(null).fail(false);
127
- function supersededOnDisk(rejectedToken) {
128
- if (rejectedToken === void 0) return false;
129
- const context = getAuthContext();
130
- if (context === null) return false;
131
- const at = locationFromContext(context);
132
- if (at === null) return false;
133
- try {
134
- const loaded = storeFor(context.configDir).read(at);
135
- return loaded !== null && loaded.credentials.access_token !== rejectedToken && isAccessTokenUsable(loaded.credentials, Date.now());
136
- } catch {
137
- return false;
138
- }
139
- }
140
- async function recoverFrom401(canRetry) {
141
- const context = getAuthContext();
142
- if (context === null || context.source !== "stored-credentials") {
143
- log.error(authFailureMessage(context));
144
- return false;
145
- }
146
- if (context.refreshed === true) {
147
- log.error(authFailureMessage(context));
148
- return false;
149
- }
150
- if (!canRetry) {
151
- log.error(authFailureMessage(context));
152
- return false;
153
- }
154
- if (supersededOnDisk(context.accessToken)) {
155
- log.debug("The rejected token has already been replaced on disk; retrying with the current one");
156
- return true;
157
- }
158
- const at = locationFromContext(context);
159
- if (at === null || context.oauthHost === void 0 || context.clientId === void 0) {
160
- log.error(authFailureMessage(context));
161
- return false;
162
- }
163
- try {
164
- if (await refreshStoredCredentials(at, {
165
- apiHost: "",
166
- oauthHost: context.oauthHost,
167
- clientId: context.clientId,
168
- configDir: context.configDir
169
- })) {
170
- log.debug("Refreshed the stored session after a 401; retrying");
171
- return true;
172
- }
173
- } catch (err) {
174
- if (err instanceof AuthRefreshError) {
175
- log.error(err.message);
176
- if (err.terminal) log.error(`Run \`neon auth --profile ${context.profile ?? "DEFAULT"}\` to sign in again.`);
177
- return false;
178
- }
179
- log.debug("Refresh after 401 failed: %s", err instanceof Error ? err.message : "unknown error");
180
- }
181
- log.error(authFailureMessage(context));
182
- return false;
183
- }
184
128
  async function handleError(msg, err, canRetry) {
185
129
  if (process.argv.some((arg) => arg === "--help" || arg === "-h")) {
186
130
  await showHelp(builder);
@@ -0,0 +1,38 @@
1
+ import { isNeonApiError } from "../api.js";
2
+ import { r as defaultDir } from "../_chunks/paths-DMq0Lt7a.js";
3
+ import "../config.js";
4
+ import "../auth.js";
5
+ import { ensureAuth } from "../commands/auth.js";
6
+ import { recoverFrom401 } from "../auth_recovery.js";
7
+ import { runLink } from "../commands/link.js";
8
+ //#region src/init/link.ts
9
+ const isUnauthorized = (error) => isNeonApiError(error) && error.status === 401;
10
+ const authenticationFailed = () => /* @__PURE__ */ new Error("Authentication failed.");
11
+ const runAuthenticatedLink = async (props) => {
12
+ const authenticated = {
13
+ ...props,
14
+ _: ["link"],
15
+ configDir: props.configDir ?? defaultDir,
16
+ oauthHost: props.oauthHost ?? process.env.NEON_OAUTH_HOST ?? "https://oauth2.neon.tech",
17
+ clientId: props.clientId ?? "neonctl",
18
+ help: false
19
+ };
20
+ await ensureAuth(authenticated);
21
+ try {
22
+ await runLink(authenticated);
23
+ return;
24
+ } catch (error) {
25
+ if (!isUnauthorized(error)) throw error;
26
+ if (!await recoverFrom401(true)) throw authenticationFailed();
27
+ }
28
+ await ensureAuth(authenticated);
29
+ try {
30
+ await runLink(authenticated);
31
+ } catch (error) {
32
+ if (!isUnauthorized(error)) throw error;
33
+ await recoverFrom401(false);
34
+ throw authenticationFailed();
35
+ }
36
+ };
37
+ //#endregion
38
+ export { runAuthenticatedLink };
@@ -91,5 +91,17 @@ const pickInitConfigInteractively = async () => {
91
91
  if (value === void 0) throw new Error("Aborted.");
92
92
  return value === true;
93
93
  };
94
+ const pickInitLinkInteractively = async () => {
95
+ requireInteractive();
96
+ const { value } = await prompts({
97
+ onState: restoreCursorOnAbort,
98
+ type: "confirm",
99
+ name: "value",
100
+ message: "Link this project to a Neon project now?",
101
+ initial: true
102
+ });
103
+ if (value === void 0) throw new Error("Aborted.");
104
+ return value === true;
105
+ };
94
106
  //#endregion
95
- export { INIT_TEMPLATE_PICKER_INITIAL, initTemplatePickerChoices, pickAgentSetupInteractively, pickInitConfigInteractively, pickInitTemplateInteractively };
107
+ export { INIT_TEMPLATE_PICKER_INITIAL, initTemplatePickerChoices, pickAgentSetupInteractively, pickInitConfigInteractively, pickInitLinkInteractively, pickInitTemplateInteractively };
@@ -85,6 +85,7 @@ const test = test$1.extend({
85
85
  expect(code).toBe(options?.code ?? 0);
86
86
  if (options.snapshot !== false) expect(output).toMatchSnapshot();
87
87
  if (options.stderr !== void 0) expect(strip(error).replace(/\s+/g, " ").trim()).toEqual(typeof options.stderr === "string" ? options.stderr.toString().replace(/\s+/g, " ") : options.stderr);
88
+ if (options.stdout !== void 0) expect(strip(output).replace(/\s+/g, " ").trim()).toEqual(typeof options.stdout === "string" ? options.stdout.toString().replace(/\s+/g, " ") : options.stdout);
88
89
  resolve({
89
90
  stdout: output,
90
91
  stderr: error,
@@ -1,10 +1,13 @@
1
+ import { parseColumns } from "../human_table.js";
1
2
  //#region src/utils/help_text.ts
2
3
  const HELP_WRAP = 78;
3
4
  const WRAP_INDENT = " ";
4
5
  const helpWidth = () => {
5
- const columns = process.stderr.columns ?? process.stdout.columns;
6
- if (typeof columns !== "number" || !Number.isFinite(columns) || columns < 1) return 78;
7
- return Math.max(24, columns - 1);
6
+ const fromStdout = process.stdout.columns;
7
+ if (typeof fromStdout === "number" && Number.isFinite(fromStdout) && fromStdout >= 1) return Math.max(24, fromStdout - 1);
8
+ const fromEnv = parseColumns(process.env.COLUMNS);
9
+ if (fromEnv !== void 0) return Math.max(24, fromEnv - 1);
10
+ return 78;
8
11
  };
9
12
  const helpCsv = (label, items, width = helpWidth()) => {
10
13
  if (items.length === 0) return "";
@@ -24,6 +27,31 @@ const helpCsv = (label, items, width = helpWidth()) => {
24
27
  lines.push(line);
25
28
  return lines.join("\n");
26
29
  };
30
+ const wrapHelpText = (text, width) => {
31
+ if (width < 1) return text;
32
+ const words = text.trim().split(/\s+/).filter((word) => word !== "");
33
+ if (words.length === 0) return text;
34
+ const lines = [];
35
+ let line = "";
36
+ for (const word of words) {
37
+ if (line === "") {
38
+ line = word;
39
+ continue;
40
+ }
41
+ if (line.length + 1 + word.length <= width) {
42
+ line = `${line} ${word}`;
43
+ continue;
44
+ }
45
+ lines.push(line);
46
+ line = word;
47
+ }
48
+ if (line !== "") lines.push(line);
49
+ return lines.join("\n");
50
+ };
27
51
  const helpEpilogue = (...blocks) => ["", ...blocks.filter((block) => block !== "")].join("\n");
52
+ const globalOptionsTrailer = (usage) => {
53
+ const cli = usage.trim().split(/\s+/)[0];
54
+ return `Global options: see ${cli === "" ? "neon" : cli} --help`;
55
+ };
28
56
  //#endregion
29
- export { HELP_WRAP, helpCsv, helpEpilogue, helpWidth };
57
+ export { HELP_WRAP, globalOptionsTrailer, helpCsv, helpEpilogue, helpWidth, wrapHelpText };
package/dist/writer.js CHANGED
@@ -7,7 +7,8 @@ const writeYaml = (chunks) => {
7
7
  return YAML.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
8
8
  };
9
9
  const writeJson = (chunks) => {
10
- return JSON.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
10
+ const json = JSON.stringify(chunks.length === 1 ? chunks[0].data : Object.fromEntries(chunks.map(({ config, data }, idx) => [config.title ? toSnakeCase(config.title) : idx, data])), null, 2);
11
+ return json === void 0 ? json : `${json}\n`;
11
12
  };
12
13
  const writeTable = (chunks, out, width) => {
13
14
  chunks.forEach(({ data, config }, i) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neon",
3
- "version": "4.17.0",
3
+ "version": "4.17.1",
4
4
  "description": "CLI tool for Neon, the cloud backend primitives built around Lakebase Postgres",
5
5
  "keywords": [
6
6
  "neon",
@@ -63,9 +63,9 @@
63
63
  "yaml": "^2.9.0",
64
64
  "yargs": "17.7.2",
65
65
  "yoctocolors": "^2.1.2",
66
- "@neon/config-runtime": "1.3.0",
66
+ "@neon/config": "1.4.0",
67
67
  "@neon/sdk": "4.1.0",
68
- "@neon/config": "1.4.0"
68
+ "@neon/config-runtime": "1.3.0"
69
69
  },
70
70
  "optionalDependencies": {
71
71
  "@napi-rs/keyring": "1.3.0",
@@ -98,8 +98,8 @@
98
98
  "typescript": "^5.9.0",
99
99
  "vitest": "^3.0.9",
100
100
  "@neon-internals/cli-core": "0.0.0",
101
- "@neon-internals/env-core": "0.0.12",
102
101
  "@neon/e2e-harness": "0.0.0",
102
+ "@neon-internals/env-core": "0.0.12",
103
103
  "@neon/functions": "0.9.0"
104
104
  },
105
105
  "publishConfig": {