@zuvo/cli 0.1.7 → 0.1.9

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
@@ -22,14 +22,19 @@ zuvo hosting apps
22
22
  zuvo hosting deploy --wait
23
23
  zuvo hosting deploy --commit abc1234 --app my-app
24
24
  zuvo hosting deployments
25
- zuvo hosting logs --since 6h
26
- zuvo hosting logs -f
25
+ zuvo hosting logs --app sklive-events --since 6h
26
+ zuvo hosting logs --app sklive-super-admin -f
27
+ zuvo hosting logs --app all
27
28
  zuvo hosting deploy-logs
28
29
  zuvo hosting env list
29
30
  zuvo hosting env set API_KEY=secret FOO=bar
30
31
  zuvo hosting env set --env-file ./.env.hosting
31
32
  zuvo hosting env unset API_KEY
32
33
  zuvo hosting secrets list # alias for hosting env
34
+ zuvo hosting domains
35
+ zuvo hosting domains add app.example.com --app sklive-admin
36
+ zuvo hosting domains verify app.example.com
37
+ zuvo hosting domains rm app.example.com
33
38
  ```
34
39
 
35
40
  Token: Studio → Account → Access Tokens, or `zuvo login --token zpat_…`.
@@ -39,3 +44,4 @@ API override: `ZUVO_API_URL` / `--api-url` (default `https://api.zuvodev.com`).
39
44
  - Hosting env keys must be `UPPER_SNAKE` (`^[A-Z][A-Z0-9_]{0,127}$`).
40
45
  - After `hosting env set|unset`, run `zuvo hosting deploy --wait` so the container picks up new vars.
41
46
  - `zuvo secrets` = project Edge Function secrets; `zuvo hosting env` = App Hosting container env.
47
+ - 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
@@ -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(' ');
66
+ }
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;
54
75
  }
55
- export function hostingLogsSql(limit) {
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) {
@@ -86,3 +111,35 @@ export function formatFunctionLogLine(row) {
86
111
  ].filter(Boolean);
87
112
  return bits.join(' ');
88
113
  }
114
+ export function normalizeHostnameInput(raw) {
115
+ return String(raw || '')
116
+ .trim()
117
+ .toLowerCase()
118
+ .replace(/^https?:\/\//, '')
119
+ .replace(/\/.*$/, '')
120
+ .replace(/\.$/, '');
121
+ }
122
+ export function formatDomainDnsInstructions(domain) {
123
+ const hostname = domain.hostname || '';
124
+ const txtName = domain.challenge_name || `_zuvo-hosting-challenge.${hostname}`;
125
+ const txtValue = domain.challenge_value || domain.verification_token || '';
126
+ const cnameTarget = domain.dns_cname_target || 'app.zuvodev.com';
127
+ return [
128
+ `Hostname: ${hostname}`,
129
+ `Status: ${domain.status || 'unknown'}`,
130
+ '',
131
+ 'Add these DNS records at your registrar:',
132
+ '',
133
+ ` TXT ${txtName}`,
134
+ ` ${txtValue}`,
135
+ '',
136
+ ` CNAME ${hostname}`,
137
+ ` ${cnameTarget}`,
138
+ '',
139
+ 'Then run: zuvo hosting domains verify ' + (hostname || '<hostname>'),
140
+ ].join('\n');
141
+ }
142
+ export function formatDomainListLine(domain) {
143
+ return [domain.id || '', domain.hostname || '', domain.status || '', domain.tls_status || '']
144
+ .join('\t');
145
+ }
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 { formatFunctionLogLine, formatHostingLogLine, formatLogTimestamp, functionLogsSql, 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';
@@ -27,12 +27,16 @@ Commands:
27
27
  hosting apps
28
28
  hosting deployments [--app <appId|slug>]
29
29
  hosting deploy [--app <appId|slug>] [--commit <sha>] [--wait]
30
- hosting logs [--limit N] [--since 1h|30m|ISO] [-f|--follow]
30
+ hosting logs [--app <appId|slug|all>] [--limit N] [--since 1h|30m|ISO] [-f|--follow]
31
31
  hosting deploy-logs [deploymentId] [--app <appId|slug>]
32
32
  hosting env list [--app <appId|slug>]
33
33
  hosting env set NAME=VALUE [...] [--env-file <path>] [--app <appId|slug>]
34
34
  hosting env unset NAME [...] [--app <appId|slug>]
35
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>]
36
40
 
37
41
  Global:
38
42
  --api-url <url> Default https://api.zuvodev.com (or ZUVO_API_URL)
@@ -399,7 +403,7 @@ async function cmdHostingDeployments(apiUrl, argv) {
399
403
  }
400
404
  async function fetchHostingRuntimeLogs(apiUrl, ref, opts) {
401
405
  const body = {
402
- sql: hostingLogsSql(opts.limit),
406
+ sql: hostingLogsSql(opts.limit, opts.appSlug),
403
407
  };
404
408
  if (opts.since)
405
409
  body.iso_timestamp_start = opts.since.toISOString();
@@ -410,6 +414,7 @@ async function cmdHostingLogs(apiUrl, argv) {
410
414
  const { values } = parseArgs({
411
415
  args: argv,
412
416
  options: {
417
+ app: { type: 'string' },
413
418
  limit: { type: 'string' },
414
419
  since: { type: 'string' },
415
420
  follow: { type: 'boolean', short: 'f' },
@@ -425,6 +430,19 @@ async function cmdHostingLogs(apiUrl, argv) {
425
430
  ? parseSince(values.since)
426
431
  : parseSince('1h');
427
432
  const follow = Boolean(values.follow);
433
+ const appArg = typeof values.app === 'string' ? values.app.trim() : '';
434
+ let appSlug;
435
+ if (appArg === 'all') {
436
+ appSlug = 'all';
437
+ }
438
+ else if (appArg) {
439
+ const app = await resolveHostingApp(apiUrl, ref, appArg);
440
+ appSlug = app.slug || app.id;
441
+ }
442
+ else {
443
+ const app = await resolveHostingApp(apiUrl, ref, undefined);
444
+ appSlug = app.slug || app.id;
445
+ }
428
446
  const seen = new Set();
429
447
  const printNew = (rows) => {
430
448
  for (const row of sortLogsChronological(rows)) {
@@ -435,9 +453,9 @@ async function cmdHostingLogs(apiUrl, argv) {
435
453
  console.log(formatHostingLogLine(row));
436
454
  }
437
455
  };
438
- const initial = await fetchHostingRuntimeLogs(apiUrl, ref, { limit, since });
456
+ const initial = await fetchHostingRuntimeLogs(apiUrl, ref, { limit, since, appSlug });
439
457
  if (!initial.length && !follow) {
440
- console.log('No hosting logs in range.');
458
+ console.log(`No hosting logs in range${appSlug && appSlug !== 'all' ? ` for ${appSlug}` : ''}.`);
441
459
  return;
442
460
  }
443
461
  printNew(initial);
@@ -450,13 +468,14 @@ async function cmdHostingLogs(apiUrl, argv) {
450
468
  if (parsed && !Number.isNaN(parsed.getTime()) && parsed > cursor)
451
469
  cursor = parsed;
452
470
  }
453
- console.error('Following hosting logs… (Ctrl+C to stop)');
471
+ console.error(`Following hosting logs${appSlug && appSlug !== 'all' ? ` for ${appSlug}` : ''}… (Ctrl+C to stop)`);
454
472
  for (;;) {
455
473
  await new Promise((r) => setTimeout(r, 2_500));
456
474
  const nextSince = new Date(Math.max(0, cursor.getTime() - 1_000));
457
475
  const rows = await fetchHostingRuntimeLogs(apiUrl, ref, {
458
476
  limit: Math.max(limit, 200),
459
477
  since: nextSince,
478
+ appSlug,
460
479
  });
461
480
  printNew(rows);
462
481
  for (const row of sortLogsChronological(rows)) {
@@ -656,6 +675,124 @@ async function cmdHostingEnv(apiUrl, argv) {
656
675
  throw new Error('Usage: zuvo hosting env list|set|unset … (alias: zuvo hosting secrets …)');
657
676
  }
658
677
  }
678
+ async function fetchHostingDomains(apiUrl, ref, appId) {
679
+ return apiRequest(apiUrl, 'GET', `/platform/projects/${ref}/hosting/apps/${appId}/domains`);
680
+ }
681
+ function resolveDomain(domains, key) {
682
+ const needle = normalizeHostnameInput(key);
683
+ const match = domains.find((d) => d.id === key || (d.hostname && normalizeHostnameInput(d.hostname) === needle));
684
+ if (!match)
685
+ throw new Error(`Domain not found: ${key}`);
686
+ return match;
687
+ }
688
+ async function cmdHostingDomainsList(apiUrl, argv) {
689
+ const { values } = parseArgs({
690
+ args: argv,
691
+ options: {
692
+ app: { type: 'string' },
693
+ 'api-url': { type: 'string' },
694
+ },
695
+ allowPositionals: true,
696
+ strict: false,
697
+ });
698
+ const ref = await requireLinkedRef();
699
+ const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
700
+ const data = await fetchHostingDomains(apiUrl, ref, app.id);
701
+ if (data.platform_url)
702
+ console.error(`Platform URL: ${data.platform_url}`);
703
+ if (data.allow_custom_domains === false) {
704
+ console.error('Custom domains are not enabled for this organization plan.');
705
+ }
706
+ const domains = Array.isArray(data.domains) ? data.domains : [];
707
+ if (!domains.length) {
708
+ console.log('No custom domains.');
709
+ return;
710
+ }
711
+ for (const domain of domains)
712
+ console.log(formatDomainListLine(domain));
713
+ }
714
+ async function cmdHostingDomainsAdd(apiUrl, argv) {
715
+ const { values, positionals } = parseArgs({
716
+ args: argv,
717
+ options: {
718
+ app: { type: 'string' },
719
+ 'api-url': { type: 'string' },
720
+ },
721
+ allowPositionals: true,
722
+ strict: false,
723
+ });
724
+ const hostname = normalizeHostnameInput(positionals[0] || '');
725
+ if (!hostname) {
726
+ throw new Error('Usage: zuvo hosting domains add <hostname> [--app <id|slug>]');
727
+ }
728
+ const ref = await requireLinkedRef();
729
+ const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
730
+ const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps/${app.id}/domains`, { json: { hostname } });
731
+ console.log(formatDomainDnsInstructions(data.domain || { hostname }));
732
+ }
733
+ async function cmdHostingDomainsVerify(apiUrl, argv) {
734
+ const { values, positionals } = parseArgs({
735
+ args: argv,
736
+ options: {
737
+ app: { type: 'string' },
738
+ 'api-url': { type: 'string' },
739
+ },
740
+ allowPositionals: true,
741
+ strict: false,
742
+ });
743
+ const key = positionals[0];
744
+ if (!key) {
745
+ throw new Error('Usage: zuvo hosting domains verify <hostname|id> [--app <id|slug>]');
746
+ }
747
+ const ref = await requireLinkedRef();
748
+ const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
749
+ const listed = await fetchHostingDomains(apiUrl, ref, app.id);
750
+ const domain = resolveDomain(listed.domains || [], key);
751
+ const data = await apiRequest(apiUrl, 'POST', `/platform/projects/${ref}/hosting/apps/${app.id}/domains/${domain.id}/verify`, { json: {} });
752
+ const updated = data.domain || domain;
753
+ console.log([updated.id, updated.hostname, updated.status, updated.tls_status || '']
754
+ .filter(Boolean)
755
+ .join('\t'));
756
+ if (updated.status && !['active', 'provisioning', 'verified'].includes(updated.status)) {
757
+ console.log(formatDomainDnsInstructions(updated));
758
+ }
759
+ }
760
+ async function cmdHostingDomainsRm(apiUrl, argv) {
761
+ const { values, positionals } = parseArgs({
762
+ args: argv,
763
+ options: {
764
+ app: { type: 'string' },
765
+ 'api-url': { type: 'string' },
766
+ },
767
+ allowPositionals: true,
768
+ strict: false,
769
+ });
770
+ const key = positionals[0];
771
+ if (!key) {
772
+ throw new Error('Usage: zuvo hosting domains rm <hostname|id> [--app <id|slug>]');
773
+ }
774
+ const ref = await requireLinkedRef();
775
+ const app = await resolveHostingApp(apiUrl, ref, typeof values.app === 'string' ? values.app : undefined);
776
+ const listed = await fetchHostingDomains(apiUrl, ref, app.id);
777
+ const domain = resolveDomain(listed.domains || [], key);
778
+ await apiRequest(apiUrl, 'DELETE', `/platform/projects/${ref}/hosting/apps/${app.id}/domains/${domain.id}`);
779
+ console.log(`Removed ${domain.hostname || domain.id}`);
780
+ }
781
+ async function cmdHostingDomains(apiUrl, argv) {
782
+ const [action, ...rest] = argv;
783
+ if (!action || action === 'list' || action.startsWith('-')) {
784
+ await cmdHostingDomainsList(apiUrl, action?.startsWith('-') ? argv : rest);
785
+ }
786
+ else if (action === 'add')
787
+ await cmdHostingDomainsAdd(apiUrl, rest);
788
+ else if (action === 'verify')
789
+ await cmdHostingDomainsVerify(apiUrl, rest);
790
+ else if (action === 'rm' || action === 'remove' || action === 'delete')
791
+ await cmdHostingDomainsRm(apiUrl, rest);
792
+ else {
793
+ throw new Error('Usage: zuvo hosting domains [list|add|verify|rm] …');
794
+ }
795
+ }
659
796
  async function main() {
660
797
  const argv = process.argv.slice(2);
661
798
  const global = parseGlobal(argv);
@@ -700,6 +837,8 @@ async function main() {
700
837
  await cmdHostingDeployLogs(apiUrl, argvAfterCommand(argv, 'hosting', sub));
701
838
  else if (command === 'hosting' && (sub === 'env' || sub === 'secrets'))
702
839
  await cmdHostingEnv(apiUrl, argvAfterCommand(argv, 'hosting', sub));
840
+ else if (command === 'hosting' && (sub === 'domains' || sub === 'domain'))
841
+ await cmdHostingDomains(apiUrl, argvAfterCommand(argv, 'hosting', sub));
703
842
  else {
704
843
  console.error(usage());
705
844
  process.exit(command ? 1 : 0);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@zuvo/cli",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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"