@zuvo/cli 0.1.6 → 0.1.7
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 +3 -0
- package/dist/hosting.js +30 -0
- package/dist/index.js +82 -1
- package/package.json +1 -1
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
|
package/dist/hosting.js
CHANGED
|
@@ -56,3 +56,33 @@ 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
|
+
}
|
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 { formatFunctionLogLine, formatHostingLogLine, formatLogTimestamp, functionLogsSql, hostingLogsSql, 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>
|
|
@@ -151,6 +152,84 @@ async function deployOne(apiUrl, ref, slug) {
|
|
|
151
152
|
const meta = await apiRequest(apiUrl, 'POST', `/v1/projects/${ref}/functions/deploy`, { form, query: { slug: bundle.slug } });
|
|
152
153
|
console.log(`Deployed ${meta.slug || bundle.slug}${meta.version != null ? ` (v${meta.version})` : ''}`);
|
|
153
154
|
}
|
|
155
|
+
async function fetchFunctionLogs(apiUrl, ref, opts) {
|
|
156
|
+
const body = {
|
|
157
|
+
sql: functionLogsSql(opts.limit, opts.functionKey),
|
|
158
|
+
};
|
|
159
|
+
if (opts.since)
|
|
160
|
+
body.iso_timestamp_start = opts.since.toISOString();
|
|
161
|
+
const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/analytics/endpoints/logs.all`, { json: body });
|
|
162
|
+
return Array.isArray(data?.result) ? data.result : [];
|
|
163
|
+
}
|
|
164
|
+
async function cmdFunctionsLogs(apiUrl, argv) {
|
|
165
|
+
const { values, positionals } = parseArgs({
|
|
166
|
+
args: argv,
|
|
167
|
+
options: {
|
|
168
|
+
limit: { type: 'string' },
|
|
169
|
+
since: { type: 'string' },
|
|
170
|
+
follow: { type: 'boolean', short: 'f' },
|
|
171
|
+
'api-url': { type: 'string' },
|
|
172
|
+
},
|
|
173
|
+
allowPositionals: true,
|
|
174
|
+
strict: false,
|
|
175
|
+
});
|
|
176
|
+
const ref = await requireLinkedRef();
|
|
177
|
+
const functionKey = positionals.find((arg) => !arg.startsWith('-'));
|
|
178
|
+
const limitRaw = typeof values.limit === 'string' ? Number(values.limit) : 100;
|
|
179
|
+
const limit = Number.isFinite(limitRaw) ? limitRaw : 100;
|
|
180
|
+
const since = typeof values.since === 'string' && values.since.trim()
|
|
181
|
+
? parseSince(values.since)
|
|
182
|
+
: parseSince('1h');
|
|
183
|
+
const follow = Boolean(values.follow);
|
|
184
|
+
const seen = new Set();
|
|
185
|
+
const printNew = (rows) => {
|
|
186
|
+
for (const row of sortLogsChronological(rows)) {
|
|
187
|
+
const key = row.id || `${formatFunctionLogLine(row)}`;
|
|
188
|
+
if (seen.has(key))
|
|
189
|
+
continue;
|
|
190
|
+
seen.add(key);
|
|
191
|
+
console.log(formatFunctionLogLine(row));
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
const initial = await fetchFunctionLogs(apiUrl, ref, {
|
|
195
|
+
limit,
|
|
196
|
+
since,
|
|
197
|
+
functionKey,
|
|
198
|
+
});
|
|
199
|
+
if (!initial.length && !follow) {
|
|
200
|
+
console.log(functionKey
|
|
201
|
+
? `No edge function logs for ${functionKey} in range.`
|
|
202
|
+
: 'No edge function logs in range.');
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
printNew(initial);
|
|
206
|
+
if (!follow)
|
|
207
|
+
return;
|
|
208
|
+
let cursor = since;
|
|
209
|
+
for (const row of sortLogsChronological(initial)) {
|
|
210
|
+
const iso = formatLogTimestamp(row.timestamp);
|
|
211
|
+
const parsed = iso ? new Date(iso) : null;
|
|
212
|
+
if (parsed && !Number.isNaN(parsed.getTime()) && parsed > cursor)
|
|
213
|
+
cursor = parsed;
|
|
214
|
+
}
|
|
215
|
+
console.error('Following edge function logs… (Ctrl+C to stop)');
|
|
216
|
+
for (;;) {
|
|
217
|
+
await new Promise((r) => setTimeout(r, 2_500));
|
|
218
|
+
const nextSince = new Date(Math.max(0, cursor.getTime() - 1_000));
|
|
219
|
+
const rows = await fetchFunctionLogs(apiUrl, ref, {
|
|
220
|
+
limit: Math.max(limit, 200),
|
|
221
|
+
since: nextSince,
|
|
222
|
+
functionKey,
|
|
223
|
+
});
|
|
224
|
+
printNew(rows);
|
|
225
|
+
for (const row of sortLogsChronological(rows)) {
|
|
226
|
+
const iso = formatLogTimestamp(row.timestamp);
|
|
227
|
+
const parsed = iso ? new Date(iso) : null;
|
|
228
|
+
if (parsed && !Number.isNaN(parsed.getTime()) && parsed > cursor)
|
|
229
|
+
cursor = parsed;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
154
233
|
async function cmdFunctionsDeploy(apiUrl, argv) {
|
|
155
234
|
const ref = await requireLinkedRef();
|
|
156
235
|
const slug = argv.find((arg) => !arg.startsWith('-'));
|
|
@@ -599,6 +678,8 @@ async function main() {
|
|
|
599
678
|
await cmdFunctionsList(apiUrl);
|
|
600
679
|
else if (command === 'functions' && sub === 'deploy')
|
|
601
680
|
await cmdFunctionsDeploy(apiUrl, argvAfterCommand(argv, 'functions', 'deploy'));
|
|
681
|
+
else if (command === 'functions' && sub === 'logs')
|
|
682
|
+
await cmdFunctionsLogs(apiUrl, argvAfterCommand(argv, 'functions', 'logs'));
|
|
602
683
|
else if (command === 'secrets' && (sub === 'list' || !sub))
|
|
603
684
|
await cmdSecretsList(apiUrl);
|
|
604
685
|
else if (command === 'secrets' && sub === 'set')
|