@zuvo/cli 0.1.6 → 0.1.8
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 +8 -0
- package/dist/hosting.js +62 -0
- package/dist/index.js +206 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -8,6 +8,9 @@ zuvo login
|
|
|
8
8
|
zuvo projects list
|
|
9
9
|
zuvo link --project <ref>
|
|
10
10
|
zuvo functions deploy
|
|
11
|
+
zuvo functions logs
|
|
12
|
+
zuvo functions logs hello --since 6h
|
|
13
|
+
zuvo functions logs -f
|
|
11
14
|
zuvo secrets set OPENAI_API_KEY=sk-…
|
|
12
15
|
zuvo secrets set --env-file ./supabase/.env.local
|
|
13
16
|
zuvo secrets list
|
|
@@ -27,6 +30,10 @@ zuvo hosting env set API_KEY=secret FOO=bar
|
|
|
27
30
|
zuvo hosting env set --env-file ./.env.hosting
|
|
28
31
|
zuvo hosting env unset API_KEY
|
|
29
32
|
zuvo hosting secrets list # alias for hosting env
|
|
33
|
+
zuvo hosting domains
|
|
34
|
+
zuvo hosting domains add app.example.com --app sklive-admin
|
|
35
|
+
zuvo hosting domains verify app.example.com
|
|
36
|
+
zuvo hosting domains rm app.example.com
|
|
30
37
|
```
|
|
31
38
|
|
|
32
39
|
Token: Studio → Account → Access Tokens, or `zuvo login --token zpat_…`.
|
|
@@ -36,3 +43,4 @@ API override: `ZUVO_API_URL` / `--api-url` (default `https://api.zuvodev.com`).
|
|
|
36
43
|
- Hosting env keys must be `UPPER_SNAKE` (`^[A-Z][A-Z0-9_]{0,127}$`).
|
|
37
44
|
- After `hosting env set|unset`, run `zuvo hosting deploy --wait` so the container picks up new vars.
|
|
38
45
|
- `zuvo secrets` = project Edge Function secrets; `zuvo hosting env` = App Hosting container env.
|
|
46
|
+
- Custom domains: add TXT `_zuvo-hosting-challenge.<host>` + CNAME → `app.zuvodev.com` or `sin.app.zuvodev.com`, then `domains verify`. Requires plan `allow_custom_domains`.
|
package/dist/hosting.js
CHANGED
|
@@ -56,3 +56,65 @@ export function hostingLogsSql(limit) {
|
|
|
56
56
|
const safe = Math.min(Math.max(1, Math.floor(limit)), 5_000);
|
|
57
57
|
return `select id, timestamp, event_message from hosting_logs order by timestamp desc limit ${safe}`;
|
|
58
58
|
}
|
|
59
|
+
/** Slug or UUID safe to embed in analytics SQL. */
|
|
60
|
+
export function assertSafeFunctionKey(key) {
|
|
61
|
+
const trimmed = key.trim();
|
|
62
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(trimmed)) {
|
|
63
|
+
throw new Error(`Invalid function name: ${key} (use slug or UUID: letters, digits, _, -)`);
|
|
64
|
+
}
|
|
65
|
+
return trimmed;
|
|
66
|
+
}
|
|
67
|
+
export function functionLogsSql(limit, functionKey) {
|
|
68
|
+
const safe = Math.min(Math.max(1, Math.floor(limit)), 5_000);
|
|
69
|
+
const where = functionKey
|
|
70
|
+
? ` where metadata.function_id = '${assertSafeFunctionKey(functionKey)}'`
|
|
71
|
+
: '';
|
|
72
|
+
return `select id, timestamp, event_message from function_logs${where} order by timestamp desc limit ${safe}`;
|
|
73
|
+
}
|
|
74
|
+
export function formatFunctionLogLine(row) {
|
|
75
|
+
const ts = formatLogTimestamp(row.timestamp);
|
|
76
|
+
const metaFn = row.metadata && typeof row.metadata.function_id === 'string'
|
|
77
|
+
? row.metadata.function_id
|
|
78
|
+
: '';
|
|
79
|
+
const fn = row.function_id || metaFn;
|
|
80
|
+
const bits = [
|
|
81
|
+
ts,
|
|
82
|
+
fn ? `[${fn}]` : '',
|
|
83
|
+
row.method || '',
|
|
84
|
+
row.status_code != null ? String(row.status_code) : '',
|
|
85
|
+
row.event_message ?? '',
|
|
86
|
+
].filter(Boolean);
|
|
87
|
+
return bits.join(' ');
|
|
88
|
+
}
|
|
89
|
+
export function normalizeHostnameInput(raw) {
|
|
90
|
+
return String(raw || '')
|
|
91
|
+
.trim()
|
|
92
|
+
.toLowerCase()
|
|
93
|
+
.replace(/^https?:\/\//, '')
|
|
94
|
+
.replace(/\/.*$/, '')
|
|
95
|
+
.replace(/\.$/, '');
|
|
96
|
+
}
|
|
97
|
+
export function formatDomainDnsInstructions(domain) {
|
|
98
|
+
const hostname = domain.hostname || '';
|
|
99
|
+
const txtName = domain.challenge_name || `_zuvo-hosting-challenge.${hostname}`;
|
|
100
|
+
const txtValue = domain.challenge_value || domain.verification_token || '';
|
|
101
|
+
const cnameTarget = domain.dns_cname_target || 'app.zuvodev.com';
|
|
102
|
+
return [
|
|
103
|
+
`Hostname: ${hostname}`,
|
|
104
|
+
`Status: ${domain.status || 'unknown'}`,
|
|
105
|
+
'',
|
|
106
|
+
'Add these DNS records at your registrar:',
|
|
107
|
+
'',
|
|
108
|
+
` TXT ${txtName}`,
|
|
109
|
+
` ${txtValue}`,
|
|
110
|
+
'',
|
|
111
|
+
` CNAME ${hostname}`,
|
|
112
|
+
` ${cnameTarget}`,
|
|
113
|
+
'',
|
|
114
|
+
'Then run: zuvo hosting domains verify ' + (hostname || '<hostname>'),
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
export function formatDomainListLine(domain) {
|
|
118
|
+
return [domain.id || '', domain.hostname || '', domain.status || '', domain.tls_status || '']
|
|
119
|
+
.join('\t');
|
|
120
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { apiRequest, ApiError } from './api.js';
|
|
|
4
4
|
import { argvAfterCommand } from './argv.js';
|
|
5
5
|
import { apiUrlFromEnv, deleteAccessToken, isAccessToken, requireLinkedRef, saveLinkedRef, } from './config.js';
|
|
6
6
|
import { encodeFunctionForm, listFunctionSlugs, loadFunctionBundle } from './functions.js';
|
|
7
|
-
import { formatHostingLogLine, formatLogTimestamp, hostingLogsSql, parseSince, sortLogsChronological, } from './hosting.js';
|
|
7
|
+
import { formatDomainDnsInstructions, formatDomainListLine, formatFunctionLogLine, formatHostingLogLine, formatLogTimestamp, functionLogsSql, hostingLogsSql, normalizeHostnameInput, parseSince, sortLogsChronological, } from './hosting.js';
|
|
8
8
|
import { loginBrowser, loginWithToken } from './login.js';
|
|
9
9
|
import { loadLocalMigrations, pendingMigrations } from './migrations.js';
|
|
10
10
|
import { loadSecretsEnvFile, parseSecretArgs } from './secrets.js';
|
|
@@ -18,6 +18,7 @@ Commands:
|
|
|
18
18
|
link --project <ref>
|
|
19
19
|
functions list
|
|
20
20
|
functions deploy [slug]
|
|
21
|
+
functions logs [slug] [--limit N] [--since 1h|30m|ISO] [-f|--follow]
|
|
21
22
|
secrets list
|
|
22
23
|
secrets set NAME=VALUE [NAME=VALUE ...]
|
|
23
24
|
secrets set --env-file <path>
|
|
@@ -32,6 +33,10 @@ Commands:
|
|
|
32
33
|
hosting env set NAME=VALUE [...] [--env-file <path>] [--app <appId|slug>]
|
|
33
34
|
hosting env unset NAME [...] [--app <appId|slug>]
|
|
34
35
|
hosting secrets … Alias for hosting env …
|
|
36
|
+
hosting domains [list] [--app <appId|slug>]
|
|
37
|
+
hosting domains add <hostname> [--app <appId|slug>]
|
|
38
|
+
hosting domains verify <hostname|id> [--app <appId|slug>]
|
|
39
|
+
hosting domains rm <hostname|id> [--app <appId|slug>]
|
|
35
40
|
|
|
36
41
|
Global:
|
|
37
42
|
--api-url <url> Default https://api.zuvodev.com (or ZUVO_API_URL)
|
|
@@ -151,6 +156,84 @@ async function deployOne(apiUrl, ref, slug) {
|
|
|
151
156
|
const meta = await apiRequest(apiUrl, 'POST', `/v1/projects/${ref}/functions/deploy`, { form, query: { slug: bundle.slug } });
|
|
152
157
|
console.log(`Deployed ${meta.slug || bundle.slug}${meta.version != null ? ` (v${meta.version})` : ''}`);
|
|
153
158
|
}
|
|
159
|
+
async function fetchFunctionLogs(apiUrl, ref, opts) {
|
|
160
|
+
const body = {
|
|
161
|
+
sql: functionLogsSql(opts.limit, opts.functionKey),
|
|
162
|
+
};
|
|
163
|
+
if (opts.since)
|
|
164
|
+
body.iso_timestamp_start = opts.since.toISOString();
|
|
165
|
+
const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/analytics/endpoints/logs.all`, { json: body });
|
|
166
|
+
return Array.isArray(data?.result) ? data.result : [];
|
|
167
|
+
}
|
|
168
|
+
async function cmdFunctionsLogs(apiUrl, argv) {
|
|
169
|
+
const { values, positionals } = parseArgs({
|
|
170
|
+
args: argv,
|
|
171
|
+
options: {
|
|
172
|
+
limit: { type: 'string' },
|
|
173
|
+
since: { type: 'string' },
|
|
174
|
+
follow: { type: 'boolean', short: 'f' },
|
|
175
|
+
'api-url': { type: 'string' },
|
|
176
|
+
},
|
|
177
|
+
allowPositionals: true,
|
|
178
|
+
strict: false,
|
|
179
|
+
});
|
|
180
|
+
const ref = await requireLinkedRef();
|
|
181
|
+
const functionKey = positionals.find((arg) => !arg.startsWith('-'));
|
|
182
|
+
const limitRaw = typeof values.limit === 'string' ? Number(values.limit) : 100;
|
|
183
|
+
const limit = Number.isFinite(limitRaw) ? limitRaw : 100;
|
|
184
|
+
const since = typeof values.since === 'string' && values.since.trim()
|
|
185
|
+
? parseSince(values.since)
|
|
186
|
+
: parseSince('1h');
|
|
187
|
+
const follow = Boolean(values.follow);
|
|
188
|
+
const seen = new Set();
|
|
189
|
+
const printNew = (rows) => {
|
|
190
|
+
for (const row of sortLogsChronological(rows)) {
|
|
191
|
+
const key = row.id || `${formatFunctionLogLine(row)}`;
|
|
192
|
+
if (seen.has(key))
|
|
193
|
+
continue;
|
|
194
|
+
seen.add(key);
|
|
195
|
+
console.log(formatFunctionLogLine(row));
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
const initial = await fetchFunctionLogs(apiUrl, ref, {
|
|
199
|
+
limit,
|
|
200
|
+
since,
|
|
201
|
+
functionKey,
|
|
202
|
+
});
|
|
203
|
+
if (!initial.length && !follow) {
|
|
204
|
+
console.log(functionKey
|
|
205
|
+
? `No edge function logs for ${functionKey} in range.`
|
|
206
|
+
: 'No edge function logs in range.');
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
printNew(initial);
|
|
210
|
+
if (!follow)
|
|
211
|
+
return;
|
|
212
|
+
let cursor = since;
|
|
213
|
+
for (const row of sortLogsChronological(initial)) {
|
|
214
|
+
const iso = formatLogTimestamp(row.timestamp);
|
|
215
|
+
const parsed = iso ? new Date(iso) : null;
|
|
216
|
+
if (parsed && !Number.isNaN(parsed.getTime()) && parsed > cursor)
|
|
217
|
+
cursor = parsed;
|
|
218
|
+
}
|
|
219
|
+
console.error('Following edge function logs… (Ctrl+C to stop)');
|
|
220
|
+
for (;;) {
|
|
221
|
+
await new Promise((r) => setTimeout(r, 2_500));
|
|
222
|
+
const nextSince = new Date(Math.max(0, cursor.getTime() - 1_000));
|
|
223
|
+
const rows = await fetchFunctionLogs(apiUrl, ref, {
|
|
224
|
+
limit: Math.max(limit, 200),
|
|
225
|
+
since: nextSince,
|
|
226
|
+
functionKey,
|
|
227
|
+
});
|
|
228
|
+
printNew(rows);
|
|
229
|
+
for (const row of sortLogsChronological(rows)) {
|
|
230
|
+
const iso = formatLogTimestamp(row.timestamp);
|
|
231
|
+
const parsed = iso ? new Date(iso) : null;
|
|
232
|
+
if (parsed && !Number.isNaN(parsed.getTime()) && parsed > cursor)
|
|
233
|
+
cursor = parsed;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
154
237
|
async function cmdFunctionsDeploy(apiUrl, argv) {
|
|
155
238
|
const ref = await requireLinkedRef();
|
|
156
239
|
const slug = argv.find((arg) => !arg.startsWith('-'));
|
|
@@ -577,6 +660,124 @@ async function cmdHostingEnv(apiUrl, argv) {
|
|
|
577
660
|
throw new Error('Usage: zuvo hosting env list|set|unset … (alias: zuvo hosting secrets …)');
|
|
578
661
|
}
|
|
579
662
|
}
|
|
663
|
+
async function fetchHostingDomains(apiUrl, ref, appId) {
|
|
664
|
+
return apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/apps/${appId}/domains`);
|
|
665
|
+
}
|
|
666
|
+
function resolveDomain(domains, key) {
|
|
667
|
+
const needle = normalizeHostnameInput(key);
|
|
668
|
+
const match = domains.find((d) => d.id === key || (d.hostname && normalizeHostnameInput(d.hostname) === needle));
|
|
669
|
+
if (!match)
|
|
670
|
+
throw new Error(`Domain not found: ${key}`);
|
|
671
|
+
return match;
|
|
672
|
+
}
|
|
673
|
+
async function cmdHostingDomainsList(apiUrl, argv) {
|
|
674
|
+
const { values } = parseArgs({
|
|
675
|
+
args: argv,
|
|
676
|
+
options: {
|
|
677
|
+
app: { type: 'string' },
|
|
678
|
+
'api-url': { type: 'string' },
|
|
679
|
+
},
|
|
680
|
+
allowPositionals: true,
|
|
681
|
+
strict: false,
|
|
682
|
+
});
|
|
683
|
+
const ref = await requireLinkedRef();
|
|
684
|
+
const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
|
|
685
|
+
const data = await fetchHostingDomains(apiUrl, ref, app.id);
|
|
686
|
+
if (data.platform_url)
|
|
687
|
+
console.error(`Platform URL: ${data.platform_url}`);
|
|
688
|
+
if (data.allow_custom_domains === false) {
|
|
689
|
+
console.error('Custom domains are not enabled for this organization plan.');
|
|
690
|
+
}
|
|
691
|
+
const domains = Array.isArray(data.domains) ? data.domains : [];
|
|
692
|
+
if (!domains.length) {
|
|
693
|
+
console.log('No custom domains.');
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
for (const domain of domains)
|
|
697
|
+
console.log(formatDomainListLine(domain));
|
|
698
|
+
}
|
|
699
|
+
async function cmdHostingDomainsAdd(apiUrl, argv) {
|
|
700
|
+
const { values, positionals } = parseArgs({
|
|
701
|
+
args: argv,
|
|
702
|
+
options: {
|
|
703
|
+
app: { type: 'string' },
|
|
704
|
+
'api-url': { type: 'string' },
|
|
705
|
+
},
|
|
706
|
+
allowPositionals: true,
|
|
707
|
+
strict: false,
|
|
708
|
+
});
|
|
709
|
+
const hostname = normalizeHostnameInput(positionals[0] || '');
|
|
710
|
+
if (!hostname) {
|
|
711
|
+
throw new Error('Usage: zuvo hosting domains add <hostname> [--app <id|slug>]');
|
|
712
|
+
}
|
|
713
|
+
const ref = await requireLinkedRef();
|
|
714
|
+
const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
|
|
715
|
+
const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps/${app.id}/domains`, { json: { hostname } });
|
|
716
|
+
console.log(formatDomainDnsInstructions(data.domain || { hostname }));
|
|
717
|
+
}
|
|
718
|
+
async function cmdHostingDomainsVerify(apiUrl, argv) {
|
|
719
|
+
const { values, positionals } = parseArgs({
|
|
720
|
+
args: argv,
|
|
721
|
+
options: {
|
|
722
|
+
app: { type: 'string' },
|
|
723
|
+
'api-url': { type: 'string' },
|
|
724
|
+
},
|
|
725
|
+
allowPositionals: true,
|
|
726
|
+
strict: false,
|
|
727
|
+
});
|
|
728
|
+
const key = positionals[0];
|
|
729
|
+
if (!key) {
|
|
730
|
+
throw new Error('Usage: zuvo hosting domains verify <hostname|id> [--app <id|slug>]');
|
|
731
|
+
}
|
|
732
|
+
const ref = await requireLinkedRef();
|
|
733
|
+
const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
|
|
734
|
+
const listed = await fetchHostingDomains(apiUrl, ref, app.id);
|
|
735
|
+
const domain = resolveDomain(listed.domains || [], key);
|
|
736
|
+
const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps/${app.id}/domains/${domain.id}/verify`, { json: {} });
|
|
737
|
+
const updated = data.domain || domain;
|
|
738
|
+
console.log([updated.id, updated.hostname, updated.status, updated.tls_status || '']
|
|
739
|
+
.filter(Boolean)
|
|
740
|
+
.join('\t'));
|
|
741
|
+
if (updated.status && !['active', 'provisioning', 'verified'].includes(updated.status)) {
|
|
742
|
+
console.log(formatDomainDnsInstructions(updated));
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
async function cmdHostingDomainsRm(apiUrl, argv) {
|
|
746
|
+
const { values, positionals } = parseArgs({
|
|
747
|
+
args: argv,
|
|
748
|
+
options: {
|
|
749
|
+
app: { type: 'string' },
|
|
750
|
+
'api-url': { type: 'string' },
|
|
751
|
+
},
|
|
752
|
+
allowPositionals: true,
|
|
753
|
+
strict: false,
|
|
754
|
+
});
|
|
755
|
+
const key = positionals[0];
|
|
756
|
+
if (!key) {
|
|
757
|
+
throw new Error('Usage: zuvo hosting domains rm <hostname|id> [--app <id|slug>]');
|
|
758
|
+
}
|
|
759
|
+
const ref = await requireLinkedRef();
|
|
760
|
+
const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
|
|
761
|
+
const listed = await fetchHostingDomains(apiUrl, ref, app.id);
|
|
762
|
+
const domain = resolveDomain(listed.domains || [], key);
|
|
763
|
+
await apiRequest(apiUrl, 'DELETE', `/platform/projects/${ref}/hosting/apps/${app.id}/domains/${domain.id}`);
|
|
764
|
+
console.log(`Removed ${domain.hostname || domain.id}`);
|
|
765
|
+
}
|
|
766
|
+
async function cmdHostingDomains(apiUrl, argv) {
|
|
767
|
+
const [action, ...rest] = argv;
|
|
768
|
+
if (!action || action === 'list' || action.startsWith('-')) {
|
|
769
|
+
await cmdHostingDomainsList(apiUrl, action?.startsWith('-') ? argv : rest);
|
|
770
|
+
}
|
|
771
|
+
else if (action === 'add')
|
|
772
|
+
await cmdHostingDomainsAdd(apiUrl, rest);
|
|
773
|
+
else if (action === 'verify')
|
|
774
|
+
await cmdHostingDomainsVerify(apiUrl, rest);
|
|
775
|
+
else if (action === 'rm' || action === 'remove' || action === 'delete')
|
|
776
|
+
await cmdHostingDomainsRm(apiUrl, rest);
|
|
777
|
+
else {
|
|
778
|
+
throw new Error('Usage: zuvo hosting domains [list|add|verify|rm] …');
|
|
779
|
+
}
|
|
780
|
+
}
|
|
580
781
|
async function main() {
|
|
581
782
|
const argv = process.argv.slice(2);
|
|
582
783
|
const global = parseGlobal(argv);
|
|
@@ -599,6 +800,8 @@ async function main() {
|
|
|
599
800
|
await cmdFunctionsList(apiUrl);
|
|
600
801
|
else if (command === 'functions' && sub === 'deploy')
|
|
601
802
|
await cmdFunctionsDeploy(apiUrl, argvAfterCommand(argv, 'functions', 'deploy'));
|
|
803
|
+
else if (command === 'functions' && sub === 'logs')
|
|
804
|
+
await cmdFunctionsLogs(apiUrl, argvAfterCommand(argv, 'functions', 'logs'));
|
|
602
805
|
else if (command === 'secrets' && (sub === 'list' || !sub))
|
|
603
806
|
await cmdSecretsList(apiUrl);
|
|
604
807
|
else if (command === 'secrets' && sub === 'set')
|
|
@@ -619,6 +822,8 @@ async function main() {
|
|
|
619
822
|
await cmdHostingDeployLogs(apiUrl, argvAfterCommand(argv, 'hosting', sub));
|
|
620
823
|
else if (command === 'hosting' && (sub === 'env' || sub === 'secrets'))
|
|
621
824
|
await cmdHostingEnv(apiUrl, argvAfterCommand(argv, 'hosting', sub));
|
|
825
|
+
else if (command === 'hosting' && (sub === 'domains' || sub === 'domain'))
|
|
826
|
+
await cmdHostingDomains(apiUrl, argvAfterCommand(argv, 'hosting', sub));
|
|
622
827
|
else {
|
|
623
828
|
console.error(usage());
|
|
624
829
|
process.exit(command ? 1 : 0);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zuvo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Zuvo CLI — login, link, functions/hosting deploy, secrets, env, db push, and logs",
|
|
5
|
+
"description": "Zuvo CLI — login, link, functions/hosting deploy, secrets, env, domains, db push, and logs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"zuvo": "bin/zuvo.js"
|