neonctl 2.28.0 → 2.29.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.
Files changed (135) hide show
  1. package/README.md +71 -71
  2. package/dist/analytics.js +35 -33
  3. package/dist/api.js +34 -34
  4. package/dist/auth.js +50 -44
  5. package/dist/cli.js +2 -2
  6. package/dist/commands/auth.js +58 -52
  7. package/dist/commands/bootstrap.js +115 -157
  8. package/dist/commands/branches.js +154 -147
  9. package/dist/commands/bucket.js +124 -118
  10. package/dist/commands/checkout.js +49 -49
  11. package/dist/commands/config.js +212 -88
  12. package/dist/commands/connection_string.js +62 -62
  13. package/dist/commands/data_api.js +96 -96
  14. package/dist/commands/databases.js +23 -23
  15. package/dist/commands/deploy.js +12 -12
  16. package/dist/commands/dev.js +114 -114
  17. package/dist/commands/env.js +43 -43
  18. package/dist/commands/functions.js +97 -98
  19. package/dist/commands/index.js +26 -26
  20. package/dist/commands/init.js +23 -22
  21. package/dist/commands/ip_allow.js +29 -29
  22. package/dist/commands/link.js +223 -166
  23. package/dist/commands/neon_auth.js +381 -363
  24. package/dist/commands/operations.js +11 -11
  25. package/dist/commands/orgs.js +8 -8
  26. package/dist/commands/projects.js +101 -99
  27. package/dist/commands/psql.js +31 -31
  28. package/dist/commands/roles.js +21 -21
  29. package/dist/commands/schema_diff.js +23 -23
  30. package/dist/commands/set_context.js +17 -17
  31. package/dist/commands/status.js +17 -17
  32. package/dist/commands/user.js +5 -5
  33. package/dist/commands/vpc_endpoints.js +50 -50
  34. package/dist/config.js +7 -7
  35. package/dist/config_format.js +5 -5
  36. package/dist/context.js +23 -16
  37. package/dist/current_branch_fast_path.js +6 -6
  38. package/dist/dev/env.js +34 -34
  39. package/dist/dev/functions.js +4 -4
  40. package/dist/dev/inputs.js +6 -6
  41. package/dist/dev/runtime.js +25 -25
  42. package/dist/env.js +14 -14
  43. package/dist/env_file.js +13 -13
  44. package/dist/errors.js +19 -19
  45. package/dist/functions_api.js +10 -10
  46. package/dist/help.js +15 -15
  47. package/dist/index.js +94 -92
  48. package/dist/log.js +2 -2
  49. package/dist/pkg.js +5 -5
  50. package/dist/psql/cli.js +4 -2
  51. package/dist/psql/command/cmd_cond.js +61 -61
  52. package/dist/psql/command/cmd_connect.js +159 -154
  53. package/dist/psql/command/cmd_copy.js +107 -97
  54. package/dist/psql/command/cmd_describe.js +368 -363
  55. package/dist/psql/command/cmd_format.js +276 -263
  56. package/dist/psql/command/cmd_io.js +269 -263
  57. package/dist/psql/command/cmd_lo.js +74 -66
  58. package/dist/psql/command/cmd_meta.js +148 -148
  59. package/dist/psql/command/cmd_misc.js +17 -17
  60. package/dist/psql/command/cmd_pipeline.js +142 -135
  61. package/dist/psql/command/cmd_restrict.js +25 -25
  62. package/dist/psql/command/cmd_show.js +183 -168
  63. package/dist/psql/command/dispatch.js +26 -26
  64. package/dist/psql/command/shared.js +14 -14
  65. package/dist/psql/complete/filenames.js +16 -16
  66. package/dist/psql/complete/index.js +4 -4
  67. package/dist/psql/complete/matcher.js +33 -32
  68. package/dist/psql/complete/psqlVars.js +173 -173
  69. package/dist/psql/complete/queries.js +5 -3
  70. package/dist/psql/complete/rules.js +900 -863
  71. package/dist/psql/core/common.js +136 -133
  72. package/dist/psql/core/help.js +343 -343
  73. package/dist/psql/core/mainloop.js +160 -153
  74. package/dist/psql/core/prompt.js +126 -123
  75. package/dist/psql/core/settings.js +111 -111
  76. package/dist/psql/core/sqlHelp.js +150 -150
  77. package/dist/psql/core/startup.js +211 -205
  78. package/dist/psql/core/syncVars.js +14 -14
  79. package/dist/psql/core/variables.js +24 -24
  80. package/dist/psql/describe/formatters.js +302 -289
  81. package/dist/psql/describe/processNamePattern.js +28 -28
  82. package/dist/psql/describe/queries.js +656 -651
  83. package/dist/psql/index.js +436 -411
  84. package/dist/psql/io/history.js +36 -36
  85. package/dist/psql/io/input.js +15 -15
  86. package/dist/psql/io/lineEditor/buffer.js +27 -25
  87. package/dist/psql/io/lineEditor/complete.js +15 -15
  88. package/dist/psql/io/lineEditor/filename.js +22 -22
  89. package/dist/psql/io/lineEditor/index.js +65 -62
  90. package/dist/psql/io/lineEditor/keymap.js +325 -318
  91. package/dist/psql/io/lineEditor/vt100.js +60 -60
  92. package/dist/psql/io/pgpass.js +18 -18
  93. package/dist/psql/io/pgservice.js +14 -14
  94. package/dist/psql/io/psqlrc.js +46 -46
  95. package/dist/psql/print/aligned.js +175 -166
  96. package/dist/psql/print/asciidoc.js +51 -51
  97. package/dist/psql/print/crosstab.js +34 -31
  98. package/dist/psql/print/csv.js +25 -22
  99. package/dist/psql/print/html.js +54 -54
  100. package/dist/psql/print/json.js +12 -12
  101. package/dist/psql/print/latex.js +118 -118
  102. package/dist/psql/print/pager.js +28 -26
  103. package/dist/psql/print/troff.js +48 -48
  104. package/dist/psql/print/unaligned.js +15 -14
  105. package/dist/psql/print/units.js +17 -17
  106. package/dist/psql/scanner/slash.js +48 -46
  107. package/dist/psql/scanner/sql.js +88 -84
  108. package/dist/psql/scanner/stringutils.js +21 -17
  109. package/dist/psql/types/index.js +7 -7
  110. package/dist/psql/types/scanner.js +8 -8
  111. package/dist/psql/wire/connection.js +341 -327
  112. package/dist/psql/wire/copy.js +7 -7
  113. package/dist/psql/wire/pipeline.js +26 -24
  114. package/dist/psql/wire/protocol.js +102 -102
  115. package/dist/psql/wire/sasl.js +62 -62
  116. package/dist/psql/wire/tls.js +79 -73
  117. package/dist/storage_api.js +15 -15
  118. package/dist/test_utils/fixtures.js +34 -31
  119. package/dist/test_utils/oauth_server.js +5 -5
  120. package/dist/utils/api_enums.js +13 -13
  121. package/dist/utils/branch_notice.js +5 -5
  122. package/dist/utils/branch_picker.js +26 -26
  123. package/dist/utils/compute_units.js +4 -4
  124. package/dist/utils/enrichers.js +20 -15
  125. package/dist/utils/esbuild.js +28 -28
  126. package/dist/utils/formats.js +1 -1
  127. package/dist/utils/middlewares.js +3 -3
  128. package/dist/utils/package_manager.js +68 -0
  129. package/dist/utils/point_in_time.js +12 -12
  130. package/dist/utils/psql.js +30 -30
  131. package/dist/utils/string.js +2 -2
  132. package/dist/utils/ui.js +9 -9
  133. package/dist/utils/zip.js +1 -1
  134. package/dist/writer.js +17 -17
  135. package/package.json +6 -7
@@ -9,25 +9,25 @@
9
9
  // a same-named type whose union is identical to the corresponding `@neon/sdk`
10
10
  // type, so values stay assignable in both directions.
11
11
  export const EndpointType = {
12
- ReadOnly: 'read_only',
13
- ReadWrite: 'read_write',
12
+ ReadOnly: "read_only",
13
+ ReadWrite: "read_write",
14
14
  };
15
15
  export const NeonAuthOauthProviderId = {
16
- Google: 'google',
17
- Github: 'github',
18
- Microsoft: 'microsoft',
19
- Vercel: 'vercel',
16
+ Google: "google",
17
+ Github: "github",
18
+ Microsoft: "microsoft",
19
+ Vercel: "vercel",
20
20
  };
21
21
  export const NeonAuthOauthProviderType = {
22
- Standard: 'standard',
23
- Shared: 'shared',
22
+ Standard: "standard",
23
+ Shared: "shared",
24
24
  };
25
25
  export const NeonAuthSupportedAuthProvider = {
26
- Mock: 'mock',
27
- Stack: 'stack',
28
- BetterAuth: 'better_auth',
26
+ Mock: "mock",
27
+ Stack: "stack",
28
+ BetterAuth: "better_auth",
29
29
  };
30
30
  export const NeonAuthEmailVerificationMethod = {
31
- Link: 'link',
32
- Otp: 'otp',
31
+ Link: "link",
32
+ Otp: "otp",
33
33
  };
@@ -1,5 +1,5 @@
1
- import chalk from 'chalk';
2
- import { log } from '../log.js';
1
+ import chalk from "chalk";
2
+ import { log } from "../log.js";
3
3
  /**
4
4
  * Print a one-line "this command is targeting <branch>" notice to **stderr** so
5
5
  * the user can sanity-check they're acting on the branch they think they are —
@@ -14,9 +14,9 @@ import { log } from '../log.js';
14
14
  * `→ Planning against branch main (br-…)`.
15
15
  */
16
16
  export const announceTargetBranch = (props, branch, verb) => {
17
- if (props.output === 'json' || props.output === 'yaml') {
17
+ if (props.output === "json" || props.output === "yaml") {
18
18
  return;
19
19
  }
20
- const suffix = branch.usedDefault ? chalk.dim(' · project default') : '';
21
- log.info('%s %s %s %s%s', chalk.dim(''), verb, chalk.cyan.bold(branch.branchName), chalk.dim(`(${branch.branchId})`), suffix);
20
+ const suffix = branch.usedDefault ? chalk.dim(" · project default") : "";
21
+ log.info("%s %s %s %s%s", chalk.dim(""), verb, chalk.cyan.bold(branch.branchName), chalk.dim(`(${branch.branchId})`), suffix);
22
22
  };
@@ -1,10 +1,10 @@
1
- import { EndpointType } from './api_enums.js';
2
- import prompts from 'prompts';
3
- import { retryOnLock } from '../api.js';
4
- import { log } from '../log.js';
5
- import { isCi } from '../env.js';
1
+ import prompts from "prompts";
2
+ import { retryOnLock } from "../api.js";
3
+ import { isCi } from "../env.js";
4
+ import { log } from "../log.js";
5
+ import { EndpointType } from "./api_enums.js";
6
6
  /** Sentinel `value` for the "create a new branch" choice (no branch id can collide). */
7
- const CREATE_BRANCH_CHOICE = Symbol('create-branch');
7
+ const CREATE_BRANCH_CHOICE = Symbol("create-branch");
8
8
  /**
9
9
  * Render a branch's display name with the same word labels as `neonctl branch list`
10
10
  * (`[default]`, `[protected]`) instead of symbols, so the picker reads clearly.
@@ -12,13 +12,13 @@ const CREATE_BRANCH_CHOICE = Symbol('create-branch');
12
12
  const branchLabel = (branch) => {
13
13
  const labels = [];
14
14
  if (branch.default) {
15
- labels.push('[default]');
15
+ labels.push("[default]");
16
16
  }
17
17
  if (branch.protected) {
18
- labels.push('[protected]');
18
+ labels.push("[protected]");
19
19
  }
20
20
  labels.push(branch.name);
21
- return labels.join(' ');
21
+ return labels.join(" ");
22
22
  };
23
23
  /**
24
24
  * Prompt the user to pick a branch from `branches`, with a "+ Create a new branch…" option
@@ -36,11 +36,11 @@ export const pickBranchInteractively = async (branches, opts) => {
36
36
  const defaultBranchIndex = branches.findIndex((b) => b.default);
37
37
  const initial = defaultBranchIndex >= 0 ? defaultBranchIndex + 1 : 0;
38
38
  const { choice } = await prompts({
39
- type: 'select',
40
- name: 'choice',
39
+ type: "select",
40
+ name: "choice",
41
41
  message: opts.message,
42
42
  choices: [
43
- { title: '+ Create a new branch…', value: CREATE_BRANCH_CHOICE },
43
+ { title: "+ Create a new branch…", value: CREATE_BRANCH_CHOICE },
44
44
  ...branches.map((b) => ({
45
45
  title: `${branchLabel(b)} (${b.id})`,
46
46
  value: b.id,
@@ -49,12 +49,12 @@ export const pickBranchInteractively = async (branches, opts) => {
49
49
  initial,
50
50
  });
51
51
  if (choice === undefined) {
52
- throw new Error('Aborted: no branch selected.');
52
+ throw new Error("Aborted: no branch selected.");
53
53
  }
54
54
  if (choice === CREATE_BRANCH_CHOICE) {
55
- return { kind: 'create', name: await promptNewBranchName(branches) };
55
+ return { kind: "create", name: await promptNewBranchName(branches) };
56
56
  }
57
- return { kind: 'existing', branchId: choice };
57
+ return { kind: "existing", branchId: choice };
58
58
  };
59
59
  /**
60
60
  * Prompt for a new branch name, rejecting empty input and names already taken on the
@@ -63,21 +63,21 @@ export const pickBranchInteractively = async (branches, opts) => {
63
63
  export const promptNewBranchName = async (branches) => {
64
64
  const existing = new Set(branches.map((b) => b.name));
65
65
  const { name } = await prompts({
66
- type: 'text',
67
- name: 'name',
68
- message: 'New branch name:',
66
+ type: "text",
67
+ name: "name",
68
+ message: "New branch name:",
69
69
  validate: (value) => {
70
70
  const trimmed = value.trim();
71
- if (trimmed === '')
72
- return 'Branch name cannot be empty.';
71
+ if (trimmed === "")
72
+ return "Branch name cannot be empty.";
73
73
  if (existing.has(trimmed))
74
74
  return `A branch named "${trimmed}" already exists.`;
75
75
  return true;
76
76
  },
77
77
  });
78
- const trimmed = typeof name === 'string' ? name.trim() : '';
79
- if (trimmed === '') {
80
- throw new Error('Aborted: no branch name provided.');
78
+ const trimmed = typeof name === "string" ? name.trim() : "";
79
+ if (trimmed === "") {
80
+ throw new Error("Aborted: no branch name provided.");
81
81
  }
82
82
  return trimmed;
83
83
  };
@@ -89,15 +89,15 @@ export const promptNewBranchName = async (branches) => {
89
89
  export const createBranch = async (apiClient, projectId, name, branches) => {
90
90
  const defaultBranch = branches.find((b) => b.default);
91
91
  if (!defaultBranch) {
92
- throw new Error('No default branch found');
92
+ throw new Error("No default branch found");
93
93
  }
94
94
  const { data } = await retryOnLock(() => apiClient.createProjectBranch(projectId, {
95
95
  branch: { name, parent_id: defaultBranch.id },
96
96
  endpoints: [{ type: EndpointType.ReadWrite }],
97
97
  }));
98
98
  if (defaultBranch.protected) {
99
- log.warning('The parent branch is protected; a unique role password has been generated for the new branch.');
99
+ log.warning("The parent branch is protected; a unique role password has been generated for the new branch.");
100
100
  }
101
- log.info('Created branch %s (%s).', data.branch.name, data.branch.id);
101
+ log.info("Created branch %s (%s).", data.branch.name, data.branch.id);
102
102
  return data.branch.id;
103
103
  };
@@ -6,20 +6,20 @@ export const getComputeUnits = (autoscaling) => {
6
6
  autoscaling_limit_max_cu: fixedSizeAutoscaling,
7
7
  };
8
8
  }
9
- if (!autoscaling.includes('-')) {
9
+ if (!autoscaling.includes("-")) {
10
10
  throw new Error('Autoscaling should be either fixed size (e.g. 2) or min and max sizes delimited with a dash (e.g. "0.5-1")');
11
11
  }
12
- const [min, max] = autoscaling.split('-');
12
+ const [min, max] = autoscaling.split("-");
13
13
  if (!min || !max) {
14
14
  throw new Error('Autoscaling should be either fixed size (e.g. 2) or min and max sizes delimited with a dash (e.g. "0.5-1")');
15
15
  }
16
16
  const minAutoscaling = Number(min);
17
17
  const maxAutoscaling = Number(max);
18
18
  if (isNaN(minAutoscaling)) {
19
- throw new Error('Autoscaling min should be a number');
19
+ throw new Error("Autoscaling min should be a number");
20
20
  }
21
21
  if (isNaN(maxAutoscaling)) {
22
- throw new Error('Autoscaling max should be a number');
22
+ throw new Error("Autoscaling max should be a number");
23
23
  }
24
24
  return {
25
25
  autoscaling_limit_min_cu: minAutoscaling,
@@ -1,6 +1,6 @@
1
- import { isCurrentBranchProbe } from '../context.js';
2
- import { looksLikeBranchId } from './formats.js';
3
- import { isNeonApiError, messageFromBody } from '../api.js';
1
+ import { isNeonApiError, messageFromBody } from "../api.js";
2
+ import { isConfigInit, isCurrentBranchProbe } from "../context.js";
3
+ import { looksLikeBranchId } from "./formats.js";
4
4
  export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
5
5
  branch = branch.toString();
6
6
  if (looksLikeBranchId(branch)) {
@@ -13,12 +13,12 @@ export const branchIdResolve = async ({ branch, apiClient, projectId, }) => {
13
13
  if (!branchData) {
14
14
  throw new Error(`Branch ${branch} not found.\nAvailable branches: ${data.branches
15
15
  .map((b) => b.name)
16
- .join(', ')}`);
16
+ .join(", ")}`);
17
17
  }
18
18
  return branchData.id;
19
19
  };
20
20
  const getBranchIdFromProps = async (props) => {
21
- const branch = 'branch' in props && typeof props.branch === 'string'
21
+ const branch = "branch" in props && typeof props.branch === "string"
22
22
  ? props.branch
23
23
  : props.id;
24
24
  if (branch) {
@@ -35,14 +35,14 @@ const getBranchIdFromProps = async (props) => {
35
35
  if (defaultBranch) {
36
36
  return defaultBranch.id;
37
37
  }
38
- throw new Error('No default branch found');
38
+ throw new Error("No default branch found");
39
39
  };
40
40
  export const branchIdFromProps = async (props) => {
41
41
  props.branchId = await getBranchIdFromProps(props);
42
42
  return props.branchId;
43
43
  };
44
44
  export const resolveBranchRef = async (props) => {
45
- const branch = 'branch' in props && typeof props.branch === 'string'
45
+ const branch = "branch" in props && typeof props.branch === "string"
46
46
  ? props.branch
47
47
  : props.id;
48
48
  const { data } = await props.apiClient.listProjectBranches({
@@ -68,11 +68,11 @@ export const resolveBranchRef = async (props) => {
68
68
  }
69
69
  throw new Error(`Branch ${ref} not found.\nAvailable branches: ${branches
70
70
  .map((b) => b.name)
71
- .join(', ')}`);
71
+ .join(", ")}`);
72
72
  }
73
73
  const defaultBranch = branches.find((b) => b.default);
74
74
  if (!defaultBranch) {
75
- throw new Error('No default branch found');
75
+ throw new Error("No default branch found");
76
76
  }
77
77
  return {
78
78
  branchId: defaultBranch.id,
@@ -85,7 +85,7 @@ export const resolveSingleDatabase = async (props) => {
85
85
  const databases = data.databases;
86
86
  if (props.database !== undefined) {
87
87
  if (!databases.find((d) => d.name === props.database)) {
88
- throw new Error(`Database not found: ${props.database}. Available databases on branch ${props.branchId}: ${databases.map((d) => d.name).join(', ')}`);
88
+ throw new Error(`Database not found: ${props.database}. Available databases on branch ${props.branchId}: ${databases.map((d) => d.name).join(", ")}`);
89
89
  }
90
90
  return props.database;
91
91
  }
@@ -95,7 +95,7 @@ export const resolveSingleDatabase = async (props) => {
95
95
  if (databases.length === 1) {
96
96
  return databases[0].name;
97
97
  }
98
- throw new Error(`Multiple databases found for the branch, please provide one with the --database option: ${databases.map((d) => d.name).join(', ')}`);
98
+ throw new Error(`Multiple databases found for the branch, please provide one with the --database option: ${databases.map((d) => d.name).join(", ")}`);
99
99
  };
100
100
  export const fillSingleProject = async (props) => {
101
101
  // The offline `--current-branch` probe needs no project at all and runs with no
@@ -104,6 +104,11 @@ export const fillSingleProject = async (props) => {
104
104
  if (isCurrentBranchProbe(props)) {
105
105
  return props;
106
106
  }
107
+ // `config init` is purely local (scaffold + npm install) and runs with no API
108
+ // client, so resolving a single project here would dereference a null client.
109
+ if (isConfigInit(props)) {
110
+ return props;
111
+ }
107
112
  if (props.projectId) {
108
113
  return { ...props, projectId: props.projectId };
109
114
  }
@@ -121,7 +126,7 @@ export const fillSingleProject = async (props) => {
121
126
  org_id: orgId,
122
127
  });
123
128
  if (data.projects.length === 0) {
124
- throw new Error('No projects found');
129
+ throw new Error("No projects found");
125
130
  }
126
131
  if (data.projects.length > 1) {
127
132
  throw new Error(`Multiple projects found, please provide one with the --project-id option`);
@@ -135,8 +140,8 @@ export const fillSingleProject = async (props) => {
135
140
  // If the API error is about missing org_id, provide a user-friendly message
136
141
  if (isNeonApiError(error) &&
137
142
  error.status === 400 &&
138
- messageFromBody(error.data)?.includes('org_id is required')) {
139
- throw new Error('Multiple projects found, please provide one with the --project-id option');
143
+ messageFromBody(error.data)?.includes("org_id is required")) {
144
+ throw new Error("Multiple projects found, please provide one with the --project-id option");
140
145
  }
141
146
  throw error;
142
147
  }
@@ -147,7 +152,7 @@ export const fillSingleOrg = async (props) => {
147
152
  }
148
153
  const { data } = await props.apiClient.getCurrentUserOrganizations();
149
154
  if (data.organizations.length === 0) {
150
- throw new Error('No organizations found');
155
+ throw new Error("No organizations found");
151
156
  }
152
157
  if (data.organizations.length > 1) {
153
158
  throw new Error(`Multiple organizations found, please provide one with the --org-id option`);
@@ -1,11 +1,11 @@
1
- import { spawn } from 'node:child_process';
2
- import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
3
- import { tmpdir } from 'node:os';
4
- import { basename, join } from 'node:path';
5
- import which from 'which';
6
- const NOT_FOUND = 'esbuild not found. neonctl ships esbuild for most platforms; if you see ' +
7
- 'this, install esbuild and ensure it is on your PATH (e.g. `npm i -g ' +
8
- 'esbuild`), or set NEON_ESBUILD_PATH to an esbuild binary.';
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import which from "which";
6
+ const NOT_FOUND = "esbuild not found. neonctl ships esbuild for most platforms; if you see " +
7
+ "this, install esbuild and ensure it is on your PATH (e.g. `npm i -g " +
8
+ "esbuild`), or set NEON_ESBUILD_PATH to an esbuild binary.";
9
9
  // Prepended to the ESM bundle. Bundled dependencies are frequently CommonJS, but an ESM
10
10
  // output (`--format=esm`) has no `require` / `__filename` / `__dirname` in scope — so any
11
11
  // bundled CJS code that calls `require(...)` would fail at load with
@@ -39,7 +39,7 @@ const bundleViaModule = async (source, loadEsbuild) => {
39
39
  // to the binary path), so keeping this specifier invisible to the scanners is
40
40
  // what keeps esbuild out of the snapshot.
41
41
  // Do NOT "simplify" this back to import('esbuild').
42
- const name = ['es', 'build'].join('');
42
+ const name = ["es", "build"].join("");
43
43
  let esbuild;
44
44
  try {
45
45
  esbuild = await loadEsbuild(name);
@@ -57,16 +57,16 @@ const bundleViaModule = async (source, loadEsbuild) => {
57
57
  // Emit `index.mjs` (not `out.js`): the Functions runtime imports the archive's entry
58
58
  // by the conventional `index.{js,mjs}` name, and `.mjs` makes Node treat the ESM
59
59
  // output as a module without needing a `package.json` type marker alongside it.
60
- outfile: 'index.mjs',
60
+ outfile: "index.mjs",
61
61
  write: false,
62
62
  minify: true,
63
- format: 'esm',
64
- platform: 'node',
63
+ format: "esm",
64
+ platform: "node",
65
65
  // Bundle dependencies into the entry so the deployed archive is self-contained (the
66
66
  // Functions runtime has no node_modules). Node built-ins stay external on
67
67
  // platform:'node'. The banner re-creates require/__filename/__dirname for bundled CJS.
68
68
  banner: { js: ESM_CJS_INTEROP_BANNER },
69
- logLevel: 'silent',
69
+ logLevel: "silent",
70
70
  })
71
71
  .catch((err) => {
72
72
  throw new Error(`Failed to bundle function from ${source}. ${message(err)}`.trim());
@@ -90,12 +90,12 @@ const resolveEsbuild = () => {
90
90
  return override;
91
91
  throw new Error(NOT_FOUND);
92
92
  }
93
- const onPath = which.sync('esbuild', { nothrow: true });
93
+ const onPath = which.sync("esbuild", { nothrow: true });
94
94
  if (onPath)
95
95
  return onPath;
96
96
  // CWD-relative (not install-relative): helps the dev checkout where esbuild is
97
97
  // a devDependency. In `npm i -g` and pkg installs the PATH branch above wins.
98
- const local = join(process.cwd(), 'node_modules', '.bin', 'esbuild');
98
+ const local = join(process.cwd(), "node_modules", ".bin", "esbuild");
99
99
  if (existsSync(local))
100
100
  return local;
101
101
  throw new Error(NOT_FOUND);
@@ -103,30 +103,30 @@ const resolveEsbuild = () => {
103
103
  const runEsbuild = (bin, args) => new Promise((resolve, reject) => {
104
104
  // stderr is captured (NOT inherited): with --log-level=error a success emits
105
105
  // nothing, and a failure's diagnostic is read out below. Never use 'inherit'.
106
- const child = spawn(bin, args, { stdio: ['ignore', 'ignore', 'pipe'] });
107
- let stderr = '';
108
- child.stderr.on('data', (chunk) => {
106
+ const child = spawn(bin, args, { stdio: ["ignore", "ignore", "pipe"] });
107
+ let stderr = "";
108
+ child.stderr.on("data", (chunk) => {
109
109
  stderr += chunk.toString();
110
110
  });
111
- child.on('error', reject);
112
- child.on('close', (code) => {
111
+ child.on("error", reject);
112
+ child.on("close", (code) => {
113
113
  resolve({ code, stderr });
114
114
  });
115
115
  });
116
116
  const bundleViaBinary = async (source) => {
117
117
  const bin = resolveEsbuild();
118
- const outDir = mkdtempSync(join(tmpdir(), 'neon-fn-bundle-'));
119
- const outfile = join(outDir, 'index.mjs');
118
+ const outDir = mkdtempSync(join(tmpdir(), "neon-fn-bundle-"));
119
+ const outfile = join(outDir, "index.mjs");
120
120
  try {
121
121
  const { code, stderr } = await runEsbuild(bin, [
122
122
  source,
123
- '--bundle',
123
+ "--bundle",
124
124
  `--outfile=${outfile}`,
125
- '--minify',
126
- '--format=esm',
127
- '--platform=node',
125
+ "--minify",
126
+ "--format=esm",
127
+ "--platform=node",
128
128
  `--banner:js=${ESM_CJS_INTEROP_BANNER}`,
129
- '--log-level=error',
129
+ "--log-level=error",
130
130
  ]);
131
131
  if (code !== 0) {
132
132
  throw new Error(`Failed to bundle function from ${source}. ${stderr.trim()}`.trim());
@@ -134,7 +134,7 @@ const bundleViaBinary = async (source) => {
134
134
  // No `--sourcemap`: the Functions runtime has no source-map support, so an uploaded
135
135
  // `index.mjs.map` is never consumed — emitting it only inflated the archive.
136
136
  return {
137
- 'index.mjs': new Uint8Array(readFileSync(outfile)),
137
+ "index.mjs": new Uint8Array(readFileSync(outfile)),
138
138
  };
139
139
  }
140
140
  finally {
@@ -1,5 +1,5 @@
1
1
  const HAIKU_REGEX = /^[a-z0-9]+-[a-z0-9]+-[a-z0-9]+$/;
2
- export const looksLikeBranchId = (branch) => branch.startsWith('br-') && HAIKU_REGEX.test(branch.substring(3));
2
+ export const looksLikeBranchId = (branch) => branch.startsWith("br-") && HAIKU_REGEX.test(branch.substring(3));
3
3
  const LSN_REGEX = /^[a-fA-F0-9]{1,8}\/[a-fA-F0-9]{1,8}$/;
4
4
  export const looksLikeLSN = (lsn) => LSN_REGEX.test(lsn);
5
5
  export const looksLikeTimestamp = (timestamp) => {
@@ -5,16 +5,16 @@
5
5
  */
6
6
  export const fillInArgs = (args, currentArgs = args, acc = []) => {
7
7
  Object.entries(currentArgs).forEach(([k, v]) => {
8
- if (k === '_' || k === '--') {
8
+ if (k === "_" || k === "--") {
9
9
  return;
10
10
  }
11
11
  // check if the value is an Object
12
- if (typeof v === 'object' && v !== null) {
12
+ if (typeof v === "object" && v !== null) {
13
13
  fillInArgs(args, v, [...acc, k]);
14
14
  }
15
15
  else if (acc.length > 0) {
16
16
  // if it's not an object, and we have a path, fill it in
17
- args[acc.join('.') + '.' + k] = v;
17
+ args[acc.join(".") + "." + k] = v;
18
18
  }
19
19
  });
20
20
  };
@@ -0,0 +1,68 @@
1
+ import { spawn } from "node:child_process";
2
+ import which from "which";
3
+ import { log } from "../log.js";
4
+ // npm first so it's the default/preselected choice; the rest follow in rough
5
+ // popularity order.
6
+ export const PACKAGE_MANAGERS = [
7
+ "npm",
8
+ "pnpm",
9
+ "yarn",
10
+ "bun",
11
+ ];
12
+ /**
13
+ * The package manager the CLI was invoked through, read from the
14
+ * `npm_config_user_agent` npm sets for `npm exec`/`npx`, `pnpm dlx`, `yarn
15
+ * dlx`, and `bunx` (so `pnpm dlx neonctl …` installs with pnpm). Returns
16
+ * undefined when there's nothing to infer from — e.g. a globally-installed
17
+ * `neon`/`neonctl` — so the caller can ask (or fall back) instead of silently
18
+ * assuming npm.
19
+ */
20
+ export const detectPackageManager = () => {
21
+ const ua = process.env.npm_config_user_agent ?? "";
22
+ if (ua.startsWith("pnpm"))
23
+ return "pnpm";
24
+ if (ua.startsWith("yarn"))
25
+ return "yarn";
26
+ if (ua.startsWith("bun"))
27
+ return "bun";
28
+ if (ua.startsWith("npm"))
29
+ return "npm";
30
+ return undefined;
31
+ };
32
+ /** The package managers actually on PATH, in {@link PACKAGE_MANAGERS} order. */
33
+ export const installedPackageManagers = () => PACKAGE_MANAGERS.filter((pm) => which.sync(pm, { nothrow: true }) !== null);
34
+ /**
35
+ * Pick a package manager without prompting: the one the CLI was invoked through,
36
+ * else the first one installed, else npm. Used by non-interactive flows (e.g.
37
+ * `config init`) where there's no scaffold prompt to hang a picker off.
38
+ */
39
+ export const resolvePackageManager = () => detectPackageManager() ?? installedPackageManagers()[0] ?? "npm";
40
+ /**
41
+ * The argv that adds `packages` as runtime dependencies with `pm`. npm spells it
42
+ * `install`; pnpm/yarn/bun use `add`.
43
+ */
44
+ export const addDependenciesArgs = (pm, packages) => (pm === "npm" ? ["install", ...packages] : ["add", ...packages]);
45
+ /**
46
+ * Run a command inheriting our stdio so the user sees install / link output
47
+ * live and can answer any prompts the child raises. Resolves to whether it
48
+ * exited cleanly; a non-zero exit is reported but never throws — the caller
49
+ * decides whether to treat it as fatal.
50
+ */
51
+ export const runCommand = (cmd, args, cwd) => new Promise((resolvePromise) => {
52
+ // npm/pnpm/yarn ship as .cmd shims on Windows, which need a shell to run.
53
+ const child = spawn(cmd, args, {
54
+ cwd,
55
+ stdio: "inherit",
56
+ shell: process.platform === "win32",
57
+ });
58
+ child.on("error", (err) => {
59
+ log.warning("Could not run `%s %s`: %s", cmd, args.join(" "), err instanceof Error ? err.message : String(err));
60
+ resolvePromise(false);
61
+ });
62
+ child.on("close", (code) => {
63
+ if (code !== 0) {
64
+ log.warning("`%s %s` exited with code %d.", cmd, args.join(" "), code);
65
+ }
66
+ resolvePromise(code === 0);
67
+ });
68
+ });
@@ -1,24 +1,24 @@
1
- import { looksLikeLSN, looksLikeTimestamp } from './formats.js';
2
- import { branchIdResolve } from './enrichers.js';
1
+ import { branchIdResolve } from "./enrichers.js";
2
+ import { looksLikeLSN, looksLikeTimestamp } from "./formats.js";
3
3
  export class PointInTimeParseError extends Error {
4
4
  constructor(message) {
5
5
  super(message);
6
- this.name = 'PointInTimeParseError';
6
+ this.name = "PointInTimeParseError";
7
7
  }
8
8
  }
9
9
  export const parsePITBranch = (input) => {
10
- const splitIndex = input.lastIndexOf('@');
10
+ const splitIndex = input.lastIndexOf("@");
11
11
  const sourceBranch = splitIndex === -1 ? input : input.slice(0, splitIndex);
12
12
  const exactPIT = splitIndex === -1 ? null : input.slice(splitIndex + 1);
13
13
  const result = {
14
14
  branch: sourceBranch,
15
15
  ...(exactPIT === null
16
- ? { tag: 'head' }
16
+ ? { tag: "head" }
17
17
  : looksLikeLSN(exactPIT)
18
- ? { tag: 'lsn', lsn: exactPIT }
19
- : { tag: 'timestamp', timestamp: exactPIT }),
18
+ ? { tag: "lsn", lsn: exactPIT }
19
+ : { tag: "timestamp", timestamp: exactPIT }),
20
20
  };
21
- if (result.tag === 'timestamp') {
21
+ if (result.tag === "timestamp") {
22
22
  const timestamp = result.timestamp;
23
23
  if (!looksLikeTimestamp(timestamp)) {
24
24
  throw new PointInTimeParseError(`Invalid source branch format - ${input}`);
@@ -31,15 +31,15 @@ export const parsePITBranch = (input) => {
31
31
  };
32
32
  export const parsePointInTime = async ({ pointInTime, targetBranchId, projectId, api, }) => {
33
33
  const parsedPIT = parsePITBranch(pointInTime);
34
- let branchId = '';
35
- if (parsedPIT.branch === '^self') {
34
+ let branchId = "";
35
+ if (parsedPIT.branch === "^self") {
36
36
  branchId = targetBranchId;
37
37
  }
38
- else if (parsedPIT.branch === '^parent') {
38
+ else if (parsedPIT.branch === "^parent") {
39
39
  const { data } = await api.getProjectBranch(projectId, targetBranchId);
40
40
  const { parent_id: parentId } = data.branch;
41
41
  if (parentId == null) {
42
- throw new PointInTimeParseError('Branch has no parent');
42
+ throw new PointInTimeParseError("Branch has no parent");
43
43
  }
44
44
  branchId = parentId;
45
45
  }