@zuvo/cli 0.1.1 → 0.1.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @zuvo/cli
2
2
 
3
- Login, link a project, deploy Edge Functions, and `db push` against [Zuvo](https://studio.zuvodev.com).
3
+ Login, link a project, deploy Edge Functions, manage secrets, and `db push` against [Zuvo](https://studio.zuvodev.com).
4
4
 
5
5
  ```bash
6
6
  npm i -g @zuvo/cli
@@ -8,6 +8,10 @@ zuvo login
8
8
  zuvo projects list
9
9
  zuvo link --project <ref>
10
10
  zuvo functions deploy
11
+ zuvo secrets set OPENAI_API_KEY=sk-…
12
+ zuvo secrets set --env-file ./supabase/.env.local
13
+ zuvo secrets list
14
+ zuvo secrets unset OPENAI_API_KEY
11
15
  zuvo db push
12
16
  ```
13
17
 
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ import { apiUrlFromEnv, deleteAccessToken, isAccessToken, requireLinkedRef, save
5
5
  import { encodeFunctionForm, listFunctionSlugs, loadFunctionBundle } from './functions.js';
6
6
  import { loginBrowser, loginWithToken } from './login.js';
7
7
  import { loadLocalMigrations, pendingMigrations } from './migrations.js';
8
+ import { loadSecretsEnvFile, parseSecretArgs } from './secrets.js';
8
9
  function usage() {
9
10
  return `Usage: zuvo <command>
10
11
 
@@ -15,6 +16,10 @@ Commands:
15
16
  link --project <ref>
16
17
  functions list
17
18
  functions deploy [slug]
19
+ secrets list
20
+ secrets set NAME=VALUE [NAME=VALUE ...]
21
+ secrets set --env-file <path>
22
+ secrets unset NAME [NAME ...]
18
23
  db push
19
24
 
20
25
  Global:
@@ -146,6 +151,53 @@ async function cmdFunctionsDeploy(apiUrl, argv) {
146
151
  await deployOne(apiUrl, ref, name);
147
152
  }
148
153
  }
154
+ async function cmdSecretsList(apiUrl) {
155
+ const ref = await requireLinkedRef();
156
+ const rows = await apiRequest(apiUrl, 'GET', `/v1/projects/${ref}/secrets`);
157
+ if (!Array.isArray(rows) || !rows.length) {
158
+ console.log('No secrets.');
159
+ return;
160
+ }
161
+ for (const row of rows) {
162
+ console.log([row.name, row.updated_at].filter(Boolean).join('\t'));
163
+ }
164
+ }
165
+ async function cmdSecretsSet(apiUrl, argv) {
166
+ const { values, positionals } = parseArgs({
167
+ args: argv,
168
+ options: {
169
+ 'env-file': { type: 'string' },
170
+ 'api-url': { type: 'string' },
171
+ },
172
+ allowPositionals: true,
173
+ strict: false,
174
+ });
175
+ const ref = await requireLinkedRef();
176
+ const fromFile = typeof values['env-file'] === 'string' && values['env-file'].trim()
177
+ ? await loadSecretsEnvFile(values['env-file'].trim())
178
+ : [];
179
+ const fromArgs = parseSecretArgs(positionals);
180
+ const pairs = [...fromFile, ...fromArgs];
181
+ if (!pairs.length) {
182
+ throw new Error('Usage: zuvo secrets set NAME=VALUE [NAME=VALUE ...] | --env-file <path>');
183
+ }
184
+ // API rejects empty values — fail locally with a clear message.
185
+ for (const pair of pairs) {
186
+ if (!pair.value) {
187
+ throw new Error(`Secret ${pair.name} has an empty value`);
188
+ }
189
+ }
190
+ await apiRequest(apiUrl, 'POST', `/v1/projects/${ref}/secrets`, { json: pairs });
191
+ console.log(`Set ${pairs.map((p) => p.name).join(', ')}`);
192
+ }
193
+ async function cmdSecretsUnset(apiUrl, argv) {
194
+ const names = argv.filter((arg) => !arg.startsWith('-')).map((n) => n.trim()).filter(Boolean);
195
+ if (!names.length)
196
+ throw new Error('Usage: zuvo secrets unset NAME [NAME ...]');
197
+ const ref = await requireLinkedRef();
198
+ await apiRequest(apiUrl, 'DELETE', `/v1/projects/${ref}/secrets`, { json: names });
199
+ console.log(`Unset ${names.join(', ')}`);
200
+ }
149
201
  async function cmdDbPush(apiUrl) {
150
202
  const ref = await requireLinkedRef();
151
203
  const local = await loadLocalMigrations();
@@ -192,6 +244,12 @@ async function main() {
192
244
  await cmdFunctionsList(apiUrl);
193
245
  else if (command === 'functions' && sub === 'deploy')
194
246
  await cmdFunctionsDeploy(apiUrl, rest);
247
+ else if (command === 'secrets' && (sub === 'list' || !sub))
248
+ await cmdSecretsList(apiUrl);
249
+ else if (command === 'secrets' && sub === 'set')
250
+ await cmdSecretsSet(apiUrl, rest);
251
+ else if (command === 'secrets' && (sub === 'unset' || sub === 'delete'))
252
+ await cmdSecretsUnset(apiUrl, rest);
195
253
  else if (command === 'db' && sub === 'push')
196
254
  await cmdDbPush(apiUrl);
197
255
  else {
@@ -201,7 +259,7 @@ async function main() {
201
259
  }
202
260
  catch (error) {
203
261
  if (error instanceof ApiError && error.status === 403) {
204
- fail('Forbidden. Deploy and db push require owner, admin, or developer.');
262
+ fail('Forbidden. Functions, secrets, and db push require owner, admin, or developer.');
205
263
  }
206
264
  fail(error);
207
265
  }
package/dist/login.js CHANGED
@@ -27,7 +27,8 @@ async function openBrowser(url) {
27
27
  }
28
28
  }
29
29
  if (platform === 'win32') {
30
- await execFileAsync('cmd', ['/c', 'start', '', url]);
30
+ // cmd.exe treats `&` as a command separator. Quote the URL as one argument.
31
+ await execFileAsync('cmd', ['/c', 'start', '', `"${url.replaceAll('"', '')}"`]);
31
32
  return;
32
33
  }
33
34
  await execFileAsync('xdg-open', [url]);
@@ -68,14 +69,18 @@ export async function loginWithToken(token) {
68
69
  }
69
70
  await saveAccessToken(trimmed);
70
71
  }
71
- export function buildCliLoginUrl(opts) {
72
- const studio = opts.studioUrl.replace(/\/$/, '');
73
- const params = new URLSearchParams({
72
+ export function packCliLoginPayload(opts) {
73
+ return Buffer.from(JSON.stringify({
74
74
  session_id: opts.sessionId,
75
75
  public_key: opts.publicKeyHex,
76
76
  token_name: opts.tokenName,
77
- });
78
- return `${studio}/cli/login?${params.toString()}`;
77
+ }), 'utf8').toString('base64url');
78
+ }
79
+ export function buildCliLoginUrl(opts) {
80
+ const studio = opts.studioUrl.replace(/\/$/, '');
81
+ // A single query param so Windows `start`, macOS Terminal, and VS Code
82
+ // hyperlinks cannot truncate `public_key` at `&`.
83
+ return `${studio}/cli/login?p=${packCliLoginPayload(opts)}`;
79
84
  }
80
85
  export async function loginBrowser(opts) {
81
86
  const { ecdh, publicKeyHex } = generateLoginKeyPair();
@@ -0,0 +1,49 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ /** Parse `NAME=VALUE` or `NAME VALUE` style CLI args into secret pairs. */
3
+ export function parseSecretArgs(args) {
4
+ const out = [];
5
+ for (let i = 0; i < args.length; i++) {
6
+ const arg = args[i];
7
+ if (arg.startsWith('-'))
8
+ continue;
9
+ const eq = arg.indexOf('=');
10
+ if (eq > 0) {
11
+ const name = arg.slice(0, eq).trim();
12
+ const value = arg.slice(eq + 1);
13
+ if (!name)
14
+ throw new Error(`Invalid secret: ${arg}`);
15
+ out.push({ name, value });
16
+ continue;
17
+ }
18
+ const next = args[i + 1];
19
+ if (!next || next.startsWith('-') || next.includes('=')) {
20
+ throw new Error(`Missing value for secret ${arg} (use NAME=VALUE)`);
21
+ }
22
+ out.push({ name: arg.trim(), value: next });
23
+ i++;
24
+ }
25
+ return out;
26
+ }
27
+ /** Load KEY=VALUE pairs from a dotenv-style file (comments and blanks ignored). */
28
+ export async function loadSecretsEnvFile(filePath) {
29
+ const text = await readFile(filePath, 'utf8');
30
+ const out = [];
31
+ for (const raw of text.split(/\r?\n/)) {
32
+ const line = raw.trim();
33
+ if (!line || line.startsWith('#'))
34
+ continue;
35
+ const eq = line.indexOf('=');
36
+ if (eq <= 0)
37
+ throw new Error(`Invalid line in ${filePath}: ${raw}`);
38
+ const name = line.slice(0, eq).trim();
39
+ let value = line.slice(eq + 1).trim();
40
+ if ((value.startsWith('"') && value.endsWith('"')) ||
41
+ (value.startsWith("'") && value.endsWith("'"))) {
42
+ value = value.slice(1, -1);
43
+ }
44
+ if (!name)
45
+ throw new Error(`Invalid line in ${filePath}: ${raw}`);
46
+ out.push({ name, value });
47
+ }
48
+ return out;
49
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@zuvo/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "type": "module",
5
- "description": "Zuvo CLI — login, link, functions deploy, and db push",
5
+ "description": "Zuvo CLI — login, link, functions deploy, secrets, and db push",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "zuvo": "bin/zuvo.js"