@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 +5 -1
- package/dist/index.js +59 -1
- package/dist/login.js +11 -6
- package/dist/secrets.js +49 -0
- package/package.json +2 -2
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.
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
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
|
-
|
|
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();
|
package/dist/secrets.js
ADDED
|
@@ -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.
|
|
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"
|