@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 +9 -0
- package/dist/functions.js +30 -0
- package/dist/hosting-apps.js +75 -0
- package/dist/index.js +241 -24
- package/package.json +1 -1
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
|
|
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
|
-
|
|
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
|
|
527
|
-
const
|
|
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 (!
|
|
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')
|