@zuvo/cli 0.1.8 → 0.1.11

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
@@ -8,6 +8,8 @@ zuvo login
8
8
  zuvo projects list
9
9
  zuvo link --project <ref>
10
10
  zuvo functions deploy
11
+ # also uploads supabase/functions/_shared (and other _* modules) with each deploy
12
+ zuvo functions deploy hello
11
13
  zuvo functions logs
12
14
  zuvo functions logs hello --since 6h
13
15
  zuvo functions logs -f
@@ -18,12 +20,16 @@ zuvo secrets unset OPENAI_API_KEY
18
20
  zuvo db push
19
21
 
20
22
  # App Hosting
23
+ zuvo github repos [--search my-app]
24
+ zuvo hosting create --repo owner/my-app --name admin --deploy --wait
25
+ zuvo hosting create # interactive repo picker (TTY)
21
26
  zuvo hosting apps
22
27
  zuvo hosting deploy --wait
23
28
  zuvo hosting deploy --commit abc1234 --app my-app
24
29
  zuvo hosting deployments
25
- zuvo hosting logs --since 6h
26
- zuvo hosting logs -f
30
+ zuvo hosting logs --app sklive-events --since 6h
31
+ zuvo hosting logs --app sklive-super-admin -f
32
+ zuvo hosting logs --app all
27
33
  zuvo hosting deploy-logs
28
34
  zuvo hosting env list
29
35
  zuvo hosting env set API_KEY=secret FOO=bar
@@ -39,6 +45,10 @@ zuvo hosting domains rm app.example.com
39
45
  Token: Studio → Account → Access Tokens, or `zuvo login --token zpat_…`.
40
46
  API override: `ZUVO_API_URL` / `--api-url` (default `https://api.zuvodev.com`).
41
47
 
48
+ **Documentation:** [docs.zuvodev.com](https://docs.zuvodev.com)
49
+
50
+ **App runtime clients** (not this CLI): [@zuvo/js](https://docs.zuvodev.com/guides/client-libraries/javascript), [Flutter / Swift / Kotlin](https://docs.zuvodev.com/guides/client-libraries) — or official Supabase SDKs against your Zuvo project URL.
51
+
42
52
  **Notes**
43
53
  - Hosting env keys must be `UPPER_SNAKE` (`^[A-Z][A-Z0-9_]{0,127}$`).
44
54
  - After `hosting env set|unset`, run `zuvo hosting deploy --wait` so the container picks up new vars.
package/dist/functions.js CHANGED
@@ -30,6 +30,31 @@ export async function listFunctionSlugs(cwd = process.cwd()) {
30
30
  return [];
31
31
  }
32
32
  }
33
+ /** Underscore-prefixed dirs (e.g. `_shared`) — not deployable functions, shared imports. */
34
+ export async function loadSharedModules(cwd = process.cwd()) {
35
+ const root = path.join(cwd, 'supabase', 'functions');
36
+ let entries;
37
+ try {
38
+ entries = await readdir(root, { withFileTypes: true });
39
+ }
40
+ catch {
41
+ return [];
42
+ }
43
+ const dirs = entries
44
+ .filter((ent) => ent.isDirectory() &&
45
+ ent.name.startsWith('_') &&
46
+ !ent.name.startsWith('.') &&
47
+ /^_[a-z0-9][a-z0-9_-]{0,62}$/i.test(ent.name))
48
+ .map((ent) => ent.name)
49
+ .sort();
50
+ const out = [];
51
+ for (const name of dirs) {
52
+ const files = await walkFiles(path.join(root, name));
53
+ if (files.length)
54
+ out.push({ name, files });
55
+ }
56
+ return out;
57
+ }
33
58
  export async function loadFunctionBundle(cwd, slug) {
34
59
  const dir = path.join(cwd, 'supabase', 'functions', slug);
35
60
  const info = await stat(dir).catch(() => null);
@@ -68,5 +93,10 @@ export function encodeFunctionForm(input) {
68
93
  for (const file of input.files) {
69
94
  form.append('file', new Blob([file.content], { type: 'text/plain' }), file.name);
70
95
  }
96
+ for (const mod of input.sharedModules || []) {
97
+ for (const file of mod.files) {
98
+ form.append('shared', new Blob([file.content], { type: 'text/plain' }), `${mod.name}/${file.name}`);
99
+ }
100
+ }
71
101
  return form;
72
102
  }
@@ -0,0 +1,75 @@
1
+ /** Normalize `owner/repo`, URL, or `.git` suffix into `owner/repo`. */
2
+ export function normalizeRepoSelector(input) {
3
+ let value = String(input || '').trim();
4
+ if (!value)
5
+ return '';
6
+ value = value.replace(/^git@github\.com:/i, '');
7
+ value = value.replace(/^https?:\/\/(?:www\.)?github\.com\//i, '');
8
+ value = value.replace(/\.git$/i, '');
9
+ value = value.replace(/\/+$/, '');
10
+ return value.toLowerCase();
11
+ }
12
+ export function parseRepoSelector(input) {
13
+ const full = normalizeRepoSelector(input);
14
+ const parts = full.split('/').filter(Boolean);
15
+ if (parts.length < 2) {
16
+ throw new Error(`Invalid repo: ${input} (use owner/repo)`);
17
+ }
18
+ const repo = parts.pop();
19
+ const owner = parts.join('/');
20
+ if (!owner || !repo)
21
+ throw new Error(`Invalid repo: ${input} (use owner/repo)`);
22
+ return { owner, repo, full: `${owner}/${repo}` };
23
+ }
24
+ export function matchGithubRepo(repos, selector) {
25
+ const want = normalizeRepoSelector(selector);
26
+ if (!want)
27
+ return undefined;
28
+ const exact = repos.find((repo) => normalizeRepoSelector(repo.name) === want);
29
+ if (exact)
30
+ return exact;
31
+ const tail = want.split('/').pop() || want;
32
+ const byTail = repos.filter((repo) => {
33
+ const name = normalizeRepoSelector(repo.name);
34
+ return name === tail || name.endsWith(`/${tail}`);
35
+ });
36
+ if (byTail.length === 1)
37
+ return byTail[0];
38
+ return undefined;
39
+ }
40
+ export function findProjectConnectionForRepo(connections, projectRef, repoFullName) {
41
+ const want = normalizeRepoSelector(repoFullName);
42
+ return connections.find((row) => {
43
+ const repoName = normalizeRepoSelector(String(row.repository?.name || ''));
44
+ const ref = String(row.project?.ref || '');
45
+ return repoName === want && (!ref || ref === projectRef);
46
+ });
47
+ }
48
+ export function defaultAppNameFromRepo(repoFullName) {
49
+ const parsed = parseRepoSelector(repoFullName);
50
+ return parsed.repo.replace(/[^a-zA-Z0-9-]+/g, '-').replace(/^-+|-+$/g, '') || 'app';
51
+ }
52
+ export function assertSafeAppName(name) {
53
+ const trimmed = name.trim();
54
+ if (!trimmed)
55
+ throw new Error('App name is required');
56
+ if (trimmed.length > 64)
57
+ throw new Error('App name is too long (max 64 characters)');
58
+ return trimmed;
59
+ }
60
+ export function buildHostingAppCreateBody(input) {
61
+ const body = {
62
+ name: assertSafeAppName(input.name),
63
+ github_connection_id: input.github_connection_id,
64
+ branch: input.branch.trim() || 'main',
65
+ };
66
+ if (input.slug?.trim())
67
+ body.slug = input.slug.trim();
68
+ if (input.root_dir != null)
69
+ body.root_dir = input.root_dir.trim();
70
+ return body;
71
+ }
72
+ export function formatGithubRepoLine(repo, index) {
73
+ const prefix = index == null ? '' : `${index + 1}. `;
74
+ return `${prefix}${repo.name} (${repo.default_branch || 'main'})`;
75
+ }
package/dist/hosting.js CHANGED
@@ -47,14 +47,39 @@ export function sortLogsChronological(rows) {
47
47
  return ta.localeCompare(tb);
48
48
  });
49
49
  }
50
+ export function hostingAppSlugFromLog(row) {
51
+ const meta = row.metadata && typeof row.metadata.app_slug === 'string' ? row.metadata.app_slug : '';
52
+ if (meta)
53
+ return meta;
54
+ const container = row.metadata && typeof row.metadata.app_container === 'string' ? row.metadata.app_container : '';
55
+ const match = String(container)
56
+ .replace(/^\//, '')
57
+ .match(/^app_([a-z0-9][a-z0-9-]*)(?:_[a-z0-9-]+)?$/);
58
+ return match?.[1] || '';
59
+ }
50
60
  export function formatHostingLogLine(row) {
51
61
  const ts = formatLogTimestamp(row.timestamp);
62
+ const slug = hostingAppSlugFromLog(row);
52
63
  const msg = row.event_message ?? '';
53
- return ts ? `${ts} ${msg}` : msg;
64
+ const bits = [ts, slug ? `[${slug}]` : '', msg].filter(Boolean);
65
+ return bits.join(' ');
54
66
  }
55
- export function hostingLogsSql(limit) {
67
+ export function assertSafeHostingSlug(key) {
68
+ const trimmed = key.trim();
69
+ if (trimmed === 'all')
70
+ return trimmed;
71
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(trimmed)) {
72
+ throw new Error(`Invalid app slug: ${key} (use letters, digits, hyphen)`);
73
+ }
74
+ return trimmed;
75
+ }
76
+ export function hostingLogsSql(limit, appSlug) {
56
77
  const safe = Math.min(Math.max(1, Math.floor(limit)), 5_000);
57
- return `select id, timestamp, event_message from hosting_logs order by timestamp desc limit ${safe}`;
78
+ if (!appSlug || appSlug === 'all') {
79
+ return `select id, timestamp, event_message, metadata from hosting_logs order by timestamp desc limit ${safe}`;
80
+ }
81
+ const slug = assertSafeHostingSlug(appSlug);
82
+ return `select id, timestamp, event_message, metadata from hosting_logs where metadata.app_slug = '${slug}' order by timestamp desc limit ${safe}`;
58
83
  }
59
84
  /** Slug or UUID safe to embed in analytics SQL. */
60
85
  export function assertSafeFunctionKey(key) {
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from 'node:util';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { stdin as input, stderr as output } from 'node:process';
3
5
  import { apiRequest, ApiError } from './api.js';
4
6
  import { argvAfterCommand } from './argv.js';
5
7
  import { apiUrlFromEnv, deleteAccessToken, isAccessToken, requireLinkedRef, saveLinkedRef, } from './config.js';
6
- import { encodeFunctionForm, listFunctionSlugs, loadFunctionBundle } from './functions.js';
8
+ import { encodeFunctionForm, listFunctionSlugs, loadFunctionBundle, loadSharedModules, } from './functions.js';
9
+ import { buildHostingAppCreateBody, defaultAppNameFromRepo, findProjectConnectionForRepo, formatGithubRepoLine, matchGithubRepo, parseRepoSelector, } from './hosting-apps.js';
7
10
  import { formatDomainDnsInstructions, formatDomainListLine, formatFunctionLogLine, formatHostingLogLine, formatLogTimestamp, functionLogsSql, hostingLogsSql, normalizeHostnameInput, parseSince, sortLogsChronological, } from './hosting.js';
8
11
  import { loginBrowser, loginWithToken } from './login.js';
9
12
  import { loadLocalMigrations, pendingMigrations } from './migrations.js';
@@ -24,10 +27,12 @@ Commands:
24
27
  secrets set --env-file <path>
25
28
  secrets unset NAME [NAME ...]
26
29
  db push
30
+ github repos [--search <q>]
31
+ hosting create [--repo owner/name] [--name <app>] [--slug <slug>] [--branch main] [--root-dir <path>] [--connection-id <uuid>] [--deploy] [--wait] [--yes]
27
32
  hosting apps
28
33
  hosting deployments [--app <appId|slug>]
29
34
  hosting deploy [--app <appId|slug>] [--commit <sha>] [--wait]
30
- hosting logs [--limit N] [--since 1h|30m|ISO] [-f|--follow]
35
+ hosting logs [--app <appId|slug|all>] [--limit N] [--since 1h|30m|ISO] [-f|--follow]
31
36
  hosting deploy-logs [deploymentId] [--app <appId|slug>]
32
37
  hosting env list [--app <appId|slug>]
33
38
  hosting env set NAME=VALUE [...] [--env-file <path>] [--app <appId|slug>]
@@ -133,7 +138,7 @@ async function cmdLink(apiUrl, argv) {
133
138
  if (!ref)
134
139
  throw new Error('Missing --project <ref>');
135
140
  const project = await apiRequest(apiUrl, 'GET', `/v1/projects/${ref}`);
136
- const linked = project.ref || project.id || ref;
141
+ const linked = String(project.ref || project.id || ref);
137
142
  await saveLinkedRef(linked);
138
143
  console.log(`Linked to project ${linked}`);
139
144
  }
@@ -150,11 +155,15 @@ async function cmdFunctionsList(apiUrl) {
150
155
  .join('\t'));
151
156
  }
152
157
  }
153
- async function deployOne(apiUrl, ref, slug) {
158
+ async function deployOne(apiUrl, ref, slug, sharedModules) {
154
159
  const bundle = await loadFunctionBundle(process.cwd(), slug);
155
- const form = encodeFunctionForm(bundle);
160
+ const shared = sharedModules ?? (await loadSharedModules());
161
+ const form = encodeFunctionForm({ ...bundle, sharedModules: shared });
156
162
  const meta = await apiRequest(apiUrl, 'POST', `/v1/projects/${ref}/functions/deploy`, { form, query: { slug: bundle.slug } });
157
- console.log(`Deployed ${meta.slug || bundle.slug}${meta.version != null ? ` (v${meta.version})` : ''}`);
163
+ const sharedNote = shared.length
164
+ ? ` (+${shared.map((m) => m.name).join(', ')})`
165
+ : '';
166
+ console.log(`Deployed ${meta.slug || bundle.slug}${meta.version != null ? ` (v${meta.version})` : ''}${sharedNote}`);
158
167
  }
159
168
  async function fetchFunctionLogs(apiUrl, ref, opts) {
160
169
  const body = {
@@ -241,11 +250,15 @@ async function cmdFunctionsDeploy(apiUrl, argv) {
241
250
  if (!slugs.length) {
242
251
  throw new Error('No functions to deploy (expected supabase/functions/<slug>).');
243
252
  }
253
+ const sharedModules = await loadSharedModules();
254
+ if (sharedModules.length) {
255
+ console.log(`Including shared modules: ${sharedModules.map((m) => m.name).join(', ')}`);
256
+ }
244
257
  const failures = [];
245
258
  for (let i = 0; i < slugs.length; i++) {
246
259
  const name = slugs[i];
247
260
  try {
248
- await deployOne(apiUrl, ref, name);
261
+ await deployOne(apiUrl, ref, name, sharedModules);
249
262
  }
250
263
  catch (error) {
251
264
  const message = error instanceof Error ? error.message : String(error);
@@ -336,6 +349,80 @@ async function cmdDbPush(apiUrl) {
336
349
  console.log(`Applied ${migration.filename}`);
337
350
  }
338
351
  }
352
+ async function loadLinkedProject(apiUrl) {
353
+ const ref = await requireLinkedRef();
354
+ const project = await apiRequest(apiUrl, 'GET', `/v1/projects/${ref}`);
355
+ const orgId = Number(project.organization_id || 0);
356
+ if (!orgId)
357
+ throw new Error('Could not resolve organization for linked project');
358
+ return { ref, project, orgId };
359
+ }
360
+ async function listGithubRepos(apiUrl, search) {
361
+ const data = await apiRequest(apiUrl, 'GET', '/integrations/github/repositories', {
362
+ query: search?.trim() ? { q: search.trim() } : undefined,
363
+ });
364
+ if (data.needs_reauthorization) {
365
+ throw new Error('GitHub is not authorized for your account. Connect GitHub in Studio → Integrations, then retry.');
366
+ }
367
+ return Array.isArray(data.repositories) ? data.repositories : [];
368
+ }
369
+ async function listGithubConnections(apiUrl, organizationId, projectRef) {
370
+ const data = await apiRequest(apiUrl, 'GET', '/integrations/github/connections', { query: { organization_id: String(organizationId), project_ref: projectRef } });
371
+ return Array.isArray(data.connections) ? data.connections : [];
372
+ }
373
+ async function pickGithubRepo(repos) {
374
+ if (!repos.length)
375
+ throw new Error('No GitHub repositories found for your account.');
376
+ if (repos.length === 1)
377
+ return repos[0];
378
+ for (let i = 0; i < repos.length; i += 1) {
379
+ console.error(formatGithubRepoLine(repos[i], i));
380
+ }
381
+ const rl = createInterface({ input, output });
382
+ try {
383
+ const answer = (await rl.question('Select repo number: ')).trim();
384
+ const index = Number(answer);
385
+ if (!Number.isFinite(index) || index < 1 || index > repos.length) {
386
+ throw new Error('Invalid selection');
387
+ }
388
+ return repos[index - 1];
389
+ }
390
+ finally {
391
+ rl.close();
392
+ }
393
+ }
394
+ async function resolveGithubRepo(apiUrl, selector, searchHint) {
395
+ if (selector?.trim()) {
396
+ const parsed = parseRepoSelector(selector);
397
+ const repos = await listGithubRepos(apiUrl, parsed.repo);
398
+ const match = matchGithubRepo(repos, parsed.full);
399
+ if (!match) {
400
+ throw new Error(`Repository not found or not accessible: ${parsed.full}`);
401
+ }
402
+ return match;
403
+ }
404
+ if (!input.isTTY) {
405
+ throw new Error('Pass --repo owner/name (non-interactive shell).');
406
+ }
407
+ const repos = await listGithubRepos(apiUrl, searchHint);
408
+ return pickGithubRepo(repos.slice(0, 30));
409
+ }
410
+ async function ensureGithubConnection(apiUrl, projectRef, organizationId, repo) {
411
+ const connections = await listGithubConnections(apiUrl, organizationId, projectRef);
412
+ const existing = findProjectConnectionForRepo(connections, projectRef, repo.name);
413
+ if (existing?.id)
414
+ return existing.id;
415
+ const created = await apiRequest(apiUrl, 'POST', '/integrations/github/connections', {
416
+ json: {
417
+ project_ref: projectRef,
418
+ repository_id: repo.id,
419
+ installation_id: repo.installation_id,
420
+ },
421
+ });
422
+ if (!created?.id)
423
+ throw new Error('Failed to attach GitHub repository to project');
424
+ return created.id;
425
+ }
339
426
  async function listHostingApps(apiUrl, ref) {
340
427
  const data = await apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/apps`);
341
428
  return Array.isArray(data?.apps) ? data.apps : [];
@@ -360,6 +447,125 @@ async function listHostingDeployments(apiUrl, ref, appId) {
360
447
  const data = await apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/apps/${appId}/deployments`);
361
448
  return Array.isArray(data?.deployments) ? data.deployments : [];
362
449
  }
450
+ async function cmdGithubRepos(apiUrl, argv) {
451
+ const { values } = parseArgs({
452
+ args: argv,
453
+ options: {
454
+ search: { type: 'string' },
455
+ q: { type: 'string' },
456
+ 'api-url': { type: 'string' },
457
+ },
458
+ allowPositionals: true,
459
+ strict: false,
460
+ });
461
+ const search = (typeof values.search === 'string' && values.search) ||
462
+ (typeof values.q === 'string' && values.q) ||
463
+ undefined;
464
+ const repos = await listGithubRepos(apiUrl, search);
465
+ if (!repos.length) {
466
+ console.log('No repositories found.');
467
+ return;
468
+ }
469
+ for (let i = 0; i < repos.length; i += 1) {
470
+ console.log([repos[i].id, repos[i].name, repos[i].default_branch || 'main'].join('\t'));
471
+ }
472
+ }
473
+ async function cmdHostingCreate(apiUrl, argv) {
474
+ const { values, positionals } = parseArgs({
475
+ args: argv,
476
+ options: {
477
+ repo: { type: 'string' },
478
+ name: { type: 'string' },
479
+ slug: { type: 'string' },
480
+ branch: { type: 'string' },
481
+ 'root-dir': { type: 'string' },
482
+ 'connection-id': { type: 'string' },
483
+ deploy: { type: 'boolean' },
484
+ wait: { type: 'boolean' },
485
+ yes: { type: 'boolean', short: 'y' },
486
+ 'api-url': { type: 'string' },
487
+ },
488
+ allowPositionals: true,
489
+ strict: false,
490
+ });
491
+ const { ref, orgId } = await loadLinkedProject(apiUrl);
492
+ const connectionId = typeof values['connection-id'] === 'string' ? values['connection-id'].trim() : '';
493
+ const repoSelector = typeof values.repo === 'string' ? values.repo.trim() : '';
494
+ const appName = (typeof values.name === 'string' && values.name.trim()) ||
495
+ positionals[0]?.trim() ||
496
+ '';
497
+ const slug = typeof values.slug === 'string' ? values.slug.trim() : undefined;
498
+ const rootDir = typeof values['root-dir'] === 'string' ? values['root-dir'] : undefined;
499
+ let githubConnectionId = connectionId;
500
+ let repo;
501
+ let branch = typeof values.branch === 'string' && values.branch.trim()
502
+ ? values.branch.trim()
503
+ : 'main';
504
+ if (!githubConnectionId) {
505
+ repo = await resolveGithubRepo(apiUrl, repoSelector || undefined);
506
+ branch =
507
+ typeof values.branch === 'string' && values.branch.trim()
508
+ ? values.branch.trim()
509
+ : repo.default_branch || 'main';
510
+ githubConnectionId = await ensureGithubConnection(apiUrl, ref, orgId, repo);
511
+ }
512
+ else if (repoSelector) {
513
+ throw new Error('Pass either --repo or --connection-id, not both.');
514
+ }
515
+ const resolvedName = appName || (repo ? defaultAppNameFromRepo(repo.name) : 'app');
516
+ const body = buildHostingAppCreateBody({
517
+ name: resolvedName,
518
+ slug,
519
+ github_connection_id: githubConnectionId,
520
+ branch,
521
+ root_dir: rootDir,
522
+ });
523
+ const slugCheck = await apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/check-slug`, {
524
+ query: { slug: body.slug || resolvedName, name: body.name },
525
+ });
526
+ if (slugCheck.available === false) {
527
+ throw new Error(slugCheck.message || `Slug "${slugCheck.slug || body.slug}" is not available`);
528
+ }
529
+ const previewRepo = repo?.name || '(existing connection)';
530
+ const previewUrl = slugCheck.public_url || '';
531
+ if (input.isTTY && !values.yes) {
532
+ console.error([
533
+ `Create hosting app "${body.name}"`,
534
+ `Repo: ${previewRepo} @ ${branch}`,
535
+ previewUrl ? `URL: ${previewUrl}` : '',
536
+ '',
537
+ 'Proceed? [y/N]',
538
+ ]
539
+ .filter(Boolean)
540
+ .join('\n'));
541
+ const rl = createInterface({ input, output });
542
+ let answer = '';
543
+ try {
544
+ answer = (await rl.question('> ')).trim().toLowerCase();
545
+ }
546
+ finally {
547
+ rl.close();
548
+ }
549
+ if (answer !== 'y' && answer !== 'yes') {
550
+ console.error('Cancelled.');
551
+ return;
552
+ }
553
+ }
554
+ const created = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps`, { json: body });
555
+ const app = created?.app;
556
+ if (!app?.id)
557
+ throw new Error('App creation failed (no app id).');
558
+ console.log(`Created app ${app.slug || app.name || app.id}`);
559
+ if (app.public_url)
560
+ console.log(app.public_url);
561
+ console.error(`Repo: ${previewRepo} @ ${branch}`);
562
+ if (values.deploy || values.wait) {
563
+ await deployHostingApp(apiUrl, ref, app, { wait: Boolean(values.wait) });
564
+ }
565
+ else {
566
+ console.error(`Deploy: zuvo hosting deploy --app ${app.slug || app.id} --wait`);
567
+ }
568
+ }
363
569
  async function cmdHostingApps(apiUrl) {
364
570
  const ref = await requireLinkedRef();
365
571
  const apps = await listHostingApps(apiUrl, ref);
@@ -403,7 +609,7 @@ async function cmdHostingDeployments(apiUrl, argv) {
403
609
  }
404
610
  async function fetchHostingRuntimeLogs(apiUrl, ref, opts) {
405
611
  const body = {
406
- sql: hostingLogsSql(opts.limit),
612
+ sql: hostingLogsSql(opts.limit, opts.appSlug),
407
613
  };
408
614
  if (opts.since)
409
615
  body.iso_timestamp_start = opts.since.toISOString();
@@ -414,6 +620,7 @@ async function cmdHostingLogs(apiUrl, argv) {
414
620
  const { values } = parseArgs({
415
621
  args: argv,
416
622
  options: {
623
+ app: { type: 'string' },
417
624
  limit: { type: 'string' },
418
625
  since: { type: 'string' },
419
626
  follow: { type: 'boolean', short: 'f' },
@@ -429,6 +636,19 @@ async function cmdHostingLogs(apiUrl, argv) {
429
636
  ? parseSince(values.since)
430
637
  : parseSince('1h');
431
638
  const follow = Boolean(values.follow);
639
+ const appArg = typeof values.app === 'string' ? values.app.trim() : '';
640
+ let appSlug;
641
+ if (appArg === 'all') {
642
+ appSlug = 'all';
643
+ }
644
+ else if (appArg) {
645
+ const app = await resolveHostingApp(apiUrl, ref, appArg);
646
+ appSlug = app.slug || app.id;
647
+ }
648
+ else {
649
+ const app = await resolveHostingApp(apiUrl, ref, undefined);
650
+ appSlug = app.slug || app.id;
651
+ }
432
652
  const seen = new Set();
433
653
  const printNew = (rows) => {
434
654
  for (const row of sortLogsChronological(rows)) {
@@ -439,9 +659,9 @@ async function cmdHostingLogs(apiUrl, argv) {
439
659
  console.log(formatHostingLogLine(row));
440
660
  }
441
661
  };
442
- const initial = await fetchHostingRuntimeLogs(apiUrl, ref, { limit, since });
662
+ const initial = await fetchHostingRuntimeLogs(apiUrl, ref, { limit, since, appSlug });
443
663
  if (!initial.length && !follow) {
444
- console.log('No hosting logs in range.');
664
+ console.log(`No hosting logs in range${appSlug && appSlug !== 'all' ? ` for ${appSlug}` : ''}.`);
445
665
  return;
446
666
  }
447
667
  printNew(initial);
@@ -454,13 +674,14 @@ async function cmdHostingLogs(apiUrl, argv) {
454
674
  if (parsed && !Number.isNaN(parsed.getTime()) && parsed > cursor)
455
675
  cursor = parsed;
456
676
  }
457
- console.error('Following hosting logs… (Ctrl+C to stop)');
677
+ console.error(`Following hosting logs${appSlug && appSlug !== 'all' ? ` for ${appSlug}` : ''}… (Ctrl+C to stop)`);
458
678
  for (;;) {
459
679
  await new Promise((r) => setTimeout(r, 2_500));
460
680
  const nextSince = new Date(Math.max(0, cursor.getTime() - 1_000));
461
681
  const rows = await fetchHostingRuntimeLogs(apiUrl, ref, {
462
682
  limit: Math.max(limit, 200),
463
683
  since: nextSince,
684
+ appSlug,
464
685
  });
465
686
  printNew(rows);
466
687
  for (const row of sortLogsChronological(rows)) {
@@ -508,23 +729,8 @@ function assertHostingEnvKey(key) {
508
729
  async function getDeploymentStatus(apiUrl, ref, appId, deploymentId) {
509
730
  return apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/apps/${appId}/deployments/${deploymentId}/logs`);
510
731
  }
511
- async function cmdHostingDeploy(apiUrl, argv) {
512
- const { values } = parseArgs({
513
- args: argv,
514
- options: {
515
- app: { type: 'string' },
516
- commit: { type: 'string' },
517
- wait: { type: 'boolean' },
518
- 'api-url': { type: 'string' },
519
- },
520
- allowPositionals: true,
521
- strict: false,
522
- });
523
- const ref = await requireLinkedRef();
524
- const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
525
- const commitSha = typeof values.commit === 'string' && values.commit.trim()
526
- ? values.commit.trim()
527
- : undefined;
732
+ async function deployHostingApp(apiUrl, ref, app, opts = {}) {
733
+ const commitSha = opts.commit?.trim() || undefined;
528
734
  const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps/${app.id}/deploy`, {
529
735
  json: commitSha ? { commit_sha: commitSha } : {},
530
736
  });
@@ -532,7 +738,7 @@ async function cmdHostingDeploy(apiUrl, argv) {
532
738
  if (!deployment?.id)
533
739
  throw new Error('Deploy enqueue failed (no deployment id).');
534
740
  console.log(`Queued deploy ${deployment.id} for ${app.slug || app.id} (${deployment.status || 'queued'})`);
535
- if (!values.wait) {
741
+ if (!opts.wait) {
536
742
  console.error(`Tip: zuvo hosting deploy-logs ${deployment.id} --app ${app.slug || app.id}`);
537
743
  return;
538
744
  }
@@ -565,6 +771,28 @@ async function cmdHostingDeploy(apiUrl, argv) {
565
771
  }
566
772
  }
567
773
  }
774
+ async function cmdHostingDeploy(apiUrl, argv) {
775
+ const { values } = parseArgs({
776
+ args: argv,
777
+ options: {
778
+ app: { type: 'string' },
779
+ commit: { type: 'string' },
780
+ wait: { type: 'boolean' },
781
+ 'api-url': { type: 'string' },
782
+ },
783
+ allowPositionals: true,
784
+ strict: false,
785
+ });
786
+ const ref = await requireLinkedRef();
787
+ const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
788
+ const commitSha = typeof values.commit === 'string' && values.commit.trim()
789
+ ? values.commit.trim()
790
+ : undefined;
791
+ await deployHostingApp(apiUrl, ref, app, {
792
+ commit: commitSha,
793
+ wait: Boolean(values.wait),
794
+ });
795
+ }
568
796
  async function cmdHostingEnvList(apiUrl, argv) {
569
797
  const { values } = parseArgs({
570
798
  args: argv,
@@ -810,6 +1038,10 @@ async function main() {
810
1038
  await cmdSecretsUnset(apiUrl, argvAfterCommand(argv, 'secrets', sub));
811
1039
  else if (command === 'db' && sub === 'push')
812
1040
  await cmdDbPush(apiUrl);
1041
+ else if (command === 'github' && (sub === 'repos' || sub === 'list'))
1042
+ await cmdGithubRepos(apiUrl, argvAfterCommand(argv, 'github', sub));
1043
+ else if (command === 'hosting' && sub === 'create')
1044
+ await cmdHostingCreate(apiUrl, argvAfterCommand(argv, 'hosting', 'create'));
813
1045
  else if (command === 'hosting' && (sub === 'apps' || sub === 'list'))
814
1046
  await cmdHostingApps(apiUrl);
815
1047
  else if (command === 'hosting' && sub === 'deployments')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuvo/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "description": "Zuvo CLI — login, link, functions/hosting deploy, secrets, env, domains, db push, and logs",
6
6
  "license": "MIT",