neonctl 2.24.0 → 2.24.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.
@@ -0,0 +1,88 @@
1
+ import { EndpointType } from '@neondatabase/api-client';
2
+ import prompts from 'prompts';
3
+ import { retryOnLock } from '../api.js';
4
+ import { log } from '../log.js';
5
+ import { isCi } from '../env.js';
6
+ /** Sentinel `value` for the "create a new branch" choice (no branch id can collide). */
7
+ const CREATE_BRANCH_CHOICE = Symbol('create-branch');
8
+ /**
9
+ * Prompt the user to pick a branch from `branches`, with a "+ Create a new branch…" option
10
+ * pinned to the top (mirroring the project/org pickers). The default selection is the
11
+ * project's default branch (the create option sits at index 0, so the default index is
12
+ * offset by one).
13
+ *
14
+ * Throws `opts.nonInteractiveMessage` when there is no TTY (or in CI): the caller knows the
15
+ * right guidance for its command, so the message is supplied rather than hard-coded here.
16
+ */
17
+ export const pickBranchInteractively = async (branches, opts) => {
18
+ if (isCi() || !process.stdout.isTTY) {
19
+ throw new Error(opts.nonInteractiveMessage);
20
+ }
21
+ const defaultBranchIndex = branches.findIndex((b) => b.default);
22
+ const initial = defaultBranchIndex >= 0 ? defaultBranchIndex + 1 : 0;
23
+ const { choice } = await prompts({
24
+ type: 'select',
25
+ name: 'choice',
26
+ message: opts.message,
27
+ choices: [
28
+ { title: '+ Create a new branch…', value: CREATE_BRANCH_CHOICE },
29
+ ...branches.map((b) => ({
30
+ title: `${b.default ? '✱ ' : ''}${b.name} (${b.id})`,
31
+ value: b.id,
32
+ })),
33
+ ],
34
+ initial,
35
+ });
36
+ if (choice === undefined) {
37
+ throw new Error('Aborted: no branch selected.');
38
+ }
39
+ if (choice === CREATE_BRANCH_CHOICE) {
40
+ return { kind: 'create', name: await promptNewBranchName(branches) };
41
+ }
42
+ return { kind: 'existing', branchId: choice };
43
+ };
44
+ /**
45
+ * Prompt for a new branch name, rejecting empty input and names already taken on the
46
+ * project (so we never silently select a different, pre-existing branch).
47
+ */
48
+ export const promptNewBranchName = async (branches) => {
49
+ const existing = new Set(branches.map((b) => b.name));
50
+ const { name } = await prompts({
51
+ type: 'text',
52
+ name: 'name',
53
+ message: 'New branch name:',
54
+ validate: (value) => {
55
+ const trimmed = value.trim();
56
+ if (trimmed === '')
57
+ return 'Branch name cannot be empty.';
58
+ if (existing.has(trimmed))
59
+ return `A branch named "${trimmed}" already exists.`;
60
+ return true;
61
+ },
62
+ });
63
+ const trimmed = typeof name === 'string' ? name.trim() : '';
64
+ if (trimmed === '') {
65
+ throw new Error('Aborted: no branch name provided.');
66
+ }
67
+ return trimmed;
68
+ };
69
+ /**
70
+ * Create a branch with the same defaults as `neonctl branch create --name <name>`:
71
+ * branched from the project's default branch with a read-write compute endpoint. Returns
72
+ * the new branch id.
73
+ */
74
+ export const createBranch = async (apiClient, projectId, name, branches) => {
75
+ const defaultBranch = branches.find((b) => b.default);
76
+ if (!defaultBranch) {
77
+ throw new Error('No default branch found');
78
+ }
79
+ const { data } = await retryOnLock(() => apiClient.createProjectBranch(projectId, {
80
+ branch: { name, parent_id: defaultBranch.id },
81
+ endpoints: [{ type: EndpointType.ReadWrite }],
82
+ }));
83
+ if (defaultBranch.protected) {
84
+ log.warning('The parent branch is protected; a unique role password has been generated for the new branch.');
85
+ }
86
+ log.info('Created branch %s (%s).', data.branch.name, data.branch.id);
87
+ return data.branch.id;
88
+ };
package/utils/esbuild.js CHANGED
@@ -48,7 +48,10 @@ const bundleViaModule = async (source, loadEsbuild) => {
48
48
  .build({
49
49
  entryPoints: [source],
50
50
  bundle: true,
51
- outfile: 'out.js',
51
+ // Emit `index.mjs` (not `out.js`): the Functions runtime imports the archive's entry
52
+ // by the conventional `index.{js,mjs}` name, and `.mjs` makes Node treat the ESM
53
+ // output as a module without needing a `package.json` type marker alongside it.
54
+ outfile: 'index.mjs',
52
55
  write: false,
53
56
  sourcemap: true,
54
57
  minify: true,
@@ -61,7 +64,7 @@ const bundleViaModule = async (source, loadEsbuild) => {
61
64
  throw new Error(`Failed to bundle function from ${source}. ${message(err)}`.trim());
62
65
  });
63
66
  const files = result.outputFiles ?? [];
64
- // write:false with one entry always yields out.js + out.js.map; an empty set
67
+ // write:false with one entry always yields index.mjs + index.mjs.map; an empty set
65
68
  // means the API contract changed under us — fail loud rather than ship an
66
69
  // empty archive.
67
70
  if (files.length === 0) {
@@ -105,7 +108,7 @@ const runEsbuild = (bin, args) => new Promise((resolve, reject) => {
105
108
  const bundleViaBinary = async (source) => {
106
109
  const bin = resolveEsbuild();
107
110
  const outDir = mkdtempSync(join(tmpdir(), 'neon-fn-bundle-'));
108
- const outfile = join(outDir, 'out.js');
111
+ const outfile = join(outDir, 'index.mjs');
109
112
  try {
110
113
  const { code, stderr } = await runEsbuild(bin, [
111
114
  source,
@@ -122,8 +125,8 @@ const bundleViaBinary = async (source) => {
122
125
  throw new Error(`Failed to bundle function from ${source}. ${stderr.trim()}`.trim());
123
126
  }
124
127
  return {
125
- 'out.js': new Uint8Array(readFileSync(outfile)),
126
- 'out.js.map': new Uint8Array(readFileSync(`${outfile}.map`)),
128
+ 'index.mjs': new Uint8Array(readFileSync(outfile)),
129
+ 'index.mjs.map': new Uint8Array(readFileSync(`${outfile}.map`)),
127
130
  };
128
131
  }
129
132
  finally {
package/utils/zip.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import { zipSync } from 'fflate';
2
- // Zip the esbuild output (out.js + out.js.map) into the archive the Functions
2
+ // Zip the esbuild output (index.mjs + index.mjs.map) into the archive the Functions
3
3
  // deploy endpoint expects. Compression level 6 matches the previous bundler.
4
4
  export const zipBundle = (entries) => zipSync(entries, { level: 6 });