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.
- package/README.md +51 -19
- package/commands/branches.js +14 -6
- package/commands/bucket.js +368 -0
- package/commands/checkout.js +24 -81
- package/commands/dev.js +48 -14
- package/commands/env.js +56 -2
- package/commands/index.js +2 -0
- package/commands/link.js +72 -10
- package/dev/env.js +24 -8
- package/package.json +1 -1
- package/pkg.js +23 -1
- package/storage_api.js +114 -0
- package/utils/branch_picker.js +88 -0
- package/utils/esbuild.js +8 -5
- package/utils/zip.js +1 -1
|
@@ -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
|
-
|
|
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
|
|
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, '
|
|
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
|
-
'
|
|
126
|
-
'
|
|
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 (
|
|
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 });
|