@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 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
- const headers = {
18
- authorization: `Bearer ${token}`,
19
- accept: 'application/json',
20
- };
21
- let body;
22
- if (opts.form) {
23
- body = opts.form;
24
- }
25
- else if (opts.json !== undefined) {
26
- headers['content-type'] = 'application/json';
27
- body = JSON.stringify(opts.json);
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
- catch {
37
- parsed = text;
35
+ else if (opts.json !== undefined) {
36
+ headers['content-type'] = 'application/json';
37
+ body = JSON.stringify(opts.json);
38
38
  }
39
- }
40
- if (!response.ok) {
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
- throw new ApiError(response.status, message);
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
- return parsed;
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
- for (const name of slugs) {
151
- await deployOne(apiUrl, ref, name);
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, ...rest] = global.positionals;
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, rest);
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, rest);
271
+ await cmdSecretsSet(apiUrl, argvAfterCommand(argv, 'secrets', 'set'));
251
272
  else if (command === 'secrets' && (sub === 'unset' || sub === 'delete'))
252
- await cmdSecretsUnset(apiUrl, rest);
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuvo/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Zuvo CLI — login, link, functions deploy, secrets, and db push",
6
6
  "license": "MIT",