@zuvo/cli 0.1.9 → 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,6 +20,9 @@ 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
@@ -40,6 +45,10 @@ zuvo hosting domains rm app.example.com
40
45
  Token: Studio → Account → Access Tokens, or `zuvo login --token zpat_…`.
41
46
  API override: `ZUVO_API_URL` / `--api-url` (default `https://api.zuvodev.com`).
42
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
+
43
52
  **Notes**
44
53
  - Hosting env keys must be `UPPER_SNAKE` (`^[A-Z][A-Z0-9_]{0,127}$`).
45
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/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,6 +27,8 @@ 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]
@@ -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);
@@ -523,23 +729,8 @@ function assertHostingEnvKey(key) {
523
729
  async function getDeploymentStatus(apiUrl, ref, appId, deploymentId) {
524
730
  return apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/apps/${appId}/deployments/${deploymentId}/logs`);
525
731
  }
526
- async function cmdHostingDeploy(apiUrl, argv) {
527
- const { values } = parseArgs({
528
- args: argv,
529
- options: {
530
- app: { type: 'string' },
531
- commit: { type: 'string' },
532
- wait: { type: 'boolean' },
533
- 'api-url': { type: 'string' },
534
- },
535
- allowPositionals: true,
536
- strict: false,
537
- });
538
- const ref = await requireLinkedRef();
539
- const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
540
- const commitSha = typeof values.commit === 'string' && values.commit.trim()
541
- ? values.commit.trim()
542
- : undefined;
732
+ async function deployHostingApp(apiUrl, ref, app, opts = {}) {
733
+ const commitSha = opts.commit?.trim() || undefined;
543
734
  const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps/${app.id}/deploy`, {
544
735
  json: commitSha ? { commit_sha: commitSha } : {},
545
736
  });
@@ -547,7 +738,7 @@ async function cmdHostingDeploy(apiUrl, argv) {
547
738
  if (!deployment?.id)
548
739
  throw new Error('Deploy enqueue failed (no deployment id).');
549
740
  console.log(`Queued deploy ${deployment.id} for ${app.slug || app.id} (${deployment.status || 'queued'})`);
550
- if (!values.wait) {
741
+ if (!opts.wait) {
551
742
  console.error(`Tip: zuvo hosting deploy-logs ${deployment.id} --app ${app.slug || app.id}`);
552
743
  return;
553
744
  }
@@ -580,6 +771,28 @@ async function cmdHostingDeploy(apiUrl, argv) {
580
771
  }
581
772
  }
582
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
+ }
583
796
  async function cmdHostingEnvList(apiUrl, argv) {
584
797
  const { values } = parseArgs({
585
798
  args: argv,
@@ -825,6 +1038,10 @@ async function main() {
825
1038
  await cmdSecretsUnset(apiUrl, argvAfterCommand(argv, 'secrets', sub));
826
1039
  else if (command === 'db' && sub === 'push')
827
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'));
828
1045
  else if (command === 'hosting' && (sub === 'apps' || sub === 'list'))
829
1046
  await cmdHostingApps(apiUrl);
830
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.9",
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",