@zuvo/cli 0.1.3 → 0.1.4
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/dist/api.js +42 -24
- package/dist/argv.js +33 -0
- package/dist/index.js +27 -6
- package/dist/secrets.js +3 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -7,6 +7,12 @@ export class ApiError extends Error {
|
|
|
7
7
|
this.name = 'ApiError';
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
function shouldRetry(status) {
|
|
14
|
+
return status === 429 || status === 502 || status === 503 || status === 504;
|
|
15
|
+
}
|
|
10
16
|
export async function apiRequest(apiUrl, method, pathname, opts = {}) {
|
|
11
17
|
const token = opts.token ?? (await loadAccessToken());
|
|
12
18
|
const url = new URL(pathname.replace(/^\//, ''), `${apiUrl}/`);
|
|
@@ -14,34 +20,46 @@ export async function apiRequest(apiUrl, method, pathname, opts = {}) {
|
|
|
14
20
|
if (value)
|
|
15
21
|
url.searchParams.set(key, value);
|
|
16
22
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
const response = await fetch(url, { method, headers, body });
|
|
30
|
-
const text = await response.text();
|
|
31
|
-
let parsed = text;
|
|
32
|
-
if (text) {
|
|
33
|
-
try {
|
|
34
|
-
parsed = JSON.parse(text);
|
|
23
|
+
// Control-plane GoTrue/PostgREST rate-limits batch deploys; retry longer.
|
|
24
|
+
const maxAttempts = Math.max(1, (opts.retries ?? 8) + 1);
|
|
25
|
+
let lastError;
|
|
26
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
27
|
+
const headers = {
|
|
28
|
+
authorization: `Bearer ${token}`,
|
|
29
|
+
accept: 'application/json',
|
|
30
|
+
};
|
|
31
|
+
let body;
|
|
32
|
+
if (opts.form) {
|
|
33
|
+
body = opts.form;
|
|
35
34
|
}
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
else if (opts.json !== undefined) {
|
|
36
|
+
headers['content-type'] = 'application/json';
|
|
37
|
+
body = JSON.stringify(opts.json);
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const response = await fetch(url, { method, headers, body });
|
|
40
|
+
const text = await response.text();
|
|
41
|
+
let parsed = text;
|
|
42
|
+
if (text) {
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(text);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
parsed = text;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (response.ok)
|
|
51
|
+
return parsed;
|
|
41
52
|
const message = parsed && typeof parsed === 'object' && parsed !== null && 'message' in parsed
|
|
42
53
|
? String(parsed.message)
|
|
43
54
|
: text || `HTTP ${response.status}`;
|
|
44
|
-
|
|
55
|
+
lastError = new ApiError(response.status, message);
|
|
56
|
+
if (!shouldRetry(response.status) || attempt === maxAttempts)
|
|
57
|
+
break;
|
|
58
|
+
const retryAfter = Number(response.headers.get('retry-after') || '');
|
|
59
|
+
const backoffMs = Number.isFinite(retryAfter) && retryAfter > 0
|
|
60
|
+
? Math.max(retryAfter * 1000, 2_000)
|
|
61
|
+
: Math.min(20_000, 1_000 * 2 ** (attempt - 1));
|
|
62
|
+
await sleep(backoffMs);
|
|
45
63
|
}
|
|
46
|
-
|
|
64
|
+
throw lastError || new ApiError(500, 'Request failed');
|
|
47
65
|
}
|
package/dist/argv.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const GLOBAL_VALUE_FLAGS = new Set(['--api-url']);
|
|
2
|
+
const GLOBAL_BOOL_FLAGS = new Set(['-h', '--help']);
|
|
3
|
+
/**
|
|
4
|
+
* Slice argv after `zuvo <verbs…>`, keeping subcommand flags.
|
|
5
|
+
*
|
|
6
|
+
* Global `parseArgs({ strict: false })` drops unknown options like
|
|
7
|
+
* `--env-file` but leaves the path as a positional — so secrets set
|
|
8
|
+
* thought the file path was a secret name. Rebuild from the raw argv
|
|
9
|
+
* instead of relying on global positionals for flag-bearing commands.
|
|
10
|
+
*/
|
|
11
|
+
export function argvAfterCommand(argv, ...verbs) {
|
|
12
|
+
const stripped = [];
|
|
13
|
+
for (let i = 0; i < argv.length; i++) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (GLOBAL_BOOL_FLAGS.has(arg))
|
|
16
|
+
continue;
|
|
17
|
+
if (GLOBAL_VALUE_FLAGS.has(arg)) {
|
|
18
|
+
i += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg.startsWith('--api-url='))
|
|
22
|
+
continue;
|
|
23
|
+
stripped.push(arg);
|
|
24
|
+
}
|
|
25
|
+
let idx = 0;
|
|
26
|
+
for (const verb of verbs) {
|
|
27
|
+
if (stripped[idx] === verb)
|
|
28
|
+
idx += 1;
|
|
29
|
+
else
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return stripped.slice(idx);
|
|
33
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { apiRequest, ApiError } from './api.js';
|
|
4
|
+
import { argvAfterCommand } from './argv.js';
|
|
4
5
|
import { apiUrlFromEnv, deleteAccessToken, isAccessToken, requireLinkedRef, saveLinkedRef, } from './config.js';
|
|
5
6
|
import { encodeFunctionForm, listFunctionSlugs, loadFunctionBundle } from './functions.js';
|
|
6
7
|
import { loginBrowser, loginWithToken } from './login.js';
|
|
@@ -147,8 +148,28 @@ async function cmdFunctionsDeploy(apiUrl, argv) {
|
|
|
147
148
|
if (!slugs.length) {
|
|
148
149
|
throw new Error('No functions to deploy (expected supabase/functions/<slug>).');
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
const failures = [];
|
|
152
|
+
for (let i = 0; i < slugs.length; i++) {
|
|
153
|
+
const name = slugs[i];
|
|
154
|
+
try {
|
|
155
|
+
await deployOne(apiUrl, ref, name);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
159
|
+
failures.push({ slug: name, message });
|
|
160
|
+
console.error(`Failed ${name}: ${message}`);
|
|
161
|
+
if (slug)
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
// Space out multi-deploys so PostgREST/GoTrue do not rate-limit the next auth.
|
|
165
|
+
if (i < slugs.length - 1) {
|
|
166
|
+
await new Promise((r) => setTimeout(r, 1_500));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (failures.length) {
|
|
170
|
+
throw new Error(`Deploy finished with ${failures.length} failure(s): ${failures
|
|
171
|
+
.map((f) => f.slug)
|
|
172
|
+
.join(', ')}`);
|
|
152
173
|
}
|
|
153
174
|
}
|
|
154
175
|
async function cmdSecretsList(apiUrl) {
|
|
@@ -229,7 +250,7 @@ async function main() {
|
|
|
229
250
|
console.log(usage());
|
|
230
251
|
return;
|
|
231
252
|
}
|
|
232
|
-
const [command, sub
|
|
253
|
+
const [command, sub] = global.positionals;
|
|
233
254
|
const apiUrl = global.apiUrl;
|
|
234
255
|
try {
|
|
235
256
|
if (command === 'login')
|
|
@@ -243,13 +264,13 @@ async function main() {
|
|
|
243
264
|
else if (command === 'functions' && sub === 'list')
|
|
244
265
|
await cmdFunctionsList(apiUrl);
|
|
245
266
|
else if (command === 'functions' && sub === 'deploy')
|
|
246
|
-
await cmdFunctionsDeploy(apiUrl,
|
|
267
|
+
await cmdFunctionsDeploy(apiUrl, argvAfterCommand(argv, 'functions', 'deploy'));
|
|
247
268
|
else if (command === 'secrets' && (sub === 'list' || !sub))
|
|
248
269
|
await cmdSecretsList(apiUrl);
|
|
249
270
|
else if (command === 'secrets' && sub === 'set')
|
|
250
|
-
await cmdSecretsSet(apiUrl,
|
|
271
|
+
await cmdSecretsSet(apiUrl, argvAfterCommand(argv, 'secrets', 'set'));
|
|
251
272
|
else if (command === 'secrets' && (sub === 'unset' || sub === 'delete'))
|
|
252
|
-
await cmdSecretsUnset(apiUrl,
|
|
273
|
+
await cmdSecretsUnset(apiUrl, argvAfterCommand(argv, 'secrets', sub));
|
|
253
274
|
else if (command === 'db' && sub === 'push')
|
|
254
275
|
await cmdDbPush(apiUrl);
|
|
255
276
|
else {
|
package/dist/secrets.js
CHANGED
|
@@ -17,6 +17,9 @@ export function parseSecretArgs(args) {
|
|
|
17
17
|
}
|
|
18
18
|
const next = args[i + 1];
|
|
19
19
|
if (!next || next.startsWith('-') || next.includes('=')) {
|
|
20
|
+
if (/[./]/.test(arg) || arg.endsWith('.env') || arg.includes('.env.')) {
|
|
21
|
+
throw new Error(`Missing value for ${arg}. To load a file use: zuvo secrets set --env-file ${arg}`);
|
|
22
|
+
}
|
|
20
23
|
throw new Error(`Missing value for secret ${arg} (use NAME=VALUE)`);
|
|
21
24
|
}
|
|
22
25
|
out.push({ name: arg.trim(), value: next });
|