@zerct/zerct 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/bin/zerct.js +106 -5
- package/package.json +1 -1
package/bin/zerct.js
CHANGED
|
@@ -4,9 +4,10 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
|
|
7
|
-
const VERSION = '0.1.
|
|
7
|
+
const VERSION = '0.1.11'
|
|
8
8
|
const DEFAULT_API_URL = 'https://api.zerct.com'
|
|
9
9
|
const ARCHIVE_LIMIT_BYTES = 48 * 1024 * 1024
|
|
10
|
+
const DEFAULT_DEPLOY_WAIT_TIMEOUT_SECONDS = 900
|
|
10
11
|
const SESSION_DIR = '.zerct'
|
|
11
12
|
const SESSION_FILE = 'session-token'
|
|
12
13
|
const SESSION_SERVICE = 'com.zerct.cli'
|
|
@@ -64,7 +65,7 @@ Usage:
|
|
|
64
65
|
zerct install [path]
|
|
65
66
|
zerct doctor [path] [--json]
|
|
66
67
|
zerct login [--token <token>] [--api <url>]
|
|
67
|
-
zerct deploy [path] [--database] [--api <url>] [--json]
|
|
68
|
+
zerct deploy [path] [--database] [--wait] [--wait-timeout <seconds>] [--api <url>] [--json]
|
|
68
69
|
zerct capabilities [--api <url>] [--json]
|
|
69
70
|
zerct me [--api <url>] [--json]
|
|
70
71
|
zerct usage [--api <url>] [--json]
|
|
@@ -184,8 +185,10 @@ function parseArgs(argv) {
|
|
|
184
185
|
limit: '',
|
|
185
186
|
cursor: '',
|
|
186
187
|
token: '',
|
|
188
|
+
waitTimeoutSeconds: DEFAULT_DEPLOY_WAIT_TIMEOUT_SECONDS,
|
|
187
189
|
json: false,
|
|
188
190
|
database: false,
|
|
191
|
+
wait: false,
|
|
189
192
|
help: false,
|
|
190
193
|
version: false
|
|
191
194
|
}
|
|
@@ -203,6 +206,11 @@ function parseArgs(argv) {
|
|
|
203
206
|
cli.database = true
|
|
204
207
|
} else if (arg === '--no-database') {
|
|
205
208
|
cli.database = false
|
|
209
|
+
} else if (arg === '--wait') {
|
|
210
|
+
cli.wait = true
|
|
211
|
+
} else if (arg === '--wait-timeout') {
|
|
212
|
+
cli.waitTimeoutSeconds = parsePositiveInteger(requireValue(argv, index, '--wait-timeout'), '--wait-timeout')
|
|
213
|
+
index += 1
|
|
206
214
|
} else if (arg === '--api') {
|
|
207
215
|
cli.apiUrl = requireValue(argv, index, '--api')
|
|
208
216
|
index += 1
|
|
@@ -238,6 +246,14 @@ function parseArgs(argv) {
|
|
|
238
246
|
return cli
|
|
239
247
|
}
|
|
240
248
|
|
|
249
|
+
function parsePositiveInteger(value, name) {
|
|
250
|
+
const parsed = Number.parseInt(value, 10)
|
|
251
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
252
|
+
throw agentError('invalid_argument', `${name} must be a positive integer.`, `Pass ${name} as seconds, for example ${name} 900.`, false)
|
|
253
|
+
}
|
|
254
|
+
return parsed
|
|
255
|
+
}
|
|
256
|
+
|
|
241
257
|
function requireValue(argv, index, name) {
|
|
242
258
|
const value = argv[index + 1]
|
|
243
259
|
if (!value || value.startsWith('--')) {
|
|
@@ -448,12 +464,17 @@ async function deploy(projectDir, cli) {
|
|
|
448
464
|
throw agentError('invalid_database_target', 'Static frontends cannot attach managed Postgres directly.', 'Deploy a Rust backend with managed Postgres and call it from the frontend.', cli.json)
|
|
449
465
|
}
|
|
450
466
|
const token = await readOrLoginToken(project.dir, cli)
|
|
467
|
+
await preflightDeployLimits([project], cli, token, cli.database)
|
|
451
468
|
const result = await deployProject(project.dir, cli, token, cli.database)
|
|
469
|
+
if (cli.wait) {
|
|
470
|
+
result.final_build = await waitForBuild(cli, token, result.build_job.id)
|
|
471
|
+
}
|
|
452
472
|
printDeployResult(result, cli)
|
|
453
473
|
return
|
|
454
474
|
}
|
|
455
475
|
|
|
456
476
|
const token = await readOrLoginToken(projectDir, cli)
|
|
477
|
+
await preflightDeployLimits(projects, cli, token, cli.database)
|
|
457
478
|
const results = []
|
|
458
479
|
if (!cli.json) {
|
|
459
480
|
console.log(`deploying ${projects.length} projects`)
|
|
@@ -472,9 +493,59 @@ async function deploy(projectDir, cli) {
|
|
|
472
493
|
}
|
|
473
494
|
}
|
|
474
495
|
|
|
496
|
+
if (cli.wait) {
|
|
497
|
+
for (const result of results) {
|
|
498
|
+
result.finalBuild = await waitForBuild(cli, token, result.response.build_job.id)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
475
502
|
printWorkspaceDeployResults(projectDir, results, cli)
|
|
476
503
|
}
|
|
477
504
|
|
|
505
|
+
async function preflightDeployLimits(projects, cli, token, databaseRequested) {
|
|
506
|
+
const [usageResponse, appsResponse] = await Promise.all([
|
|
507
|
+
apiRequest(cli, 'GET', '/v1/usage', token, null),
|
|
508
|
+
apiRequest(cli, 'GET', '/v1/apps', token, null)
|
|
509
|
+
])
|
|
510
|
+
const usage = usageResponse?.usage || {}
|
|
511
|
+
const limits = usageResponse?.limits || {}
|
|
512
|
+
const apps = Array.isArray(appsResponse?.apps) ? appsResponse.apps : []
|
|
513
|
+
const existingApps = new Map(apps.map((app) => [app.name, app]))
|
|
514
|
+
let newProjects = 0
|
|
515
|
+
let newDatabases = 0
|
|
516
|
+
|
|
517
|
+
for (const project of projects) {
|
|
518
|
+
if (!project.name || project.kind === 'unknown') {
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
const existing = existingApps.get(project.name)
|
|
522
|
+
if (!existing) {
|
|
523
|
+
newProjects += 1
|
|
524
|
+
}
|
|
525
|
+
if (databaseRequested && project.kind === 'rust_backend' && !existing?.databaseStorageMib) {
|
|
526
|
+
newDatabases += 1
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (newProjects > 0 && Number(usage.appCount) + newProjects > Number(limits.projects)) {
|
|
531
|
+
throw agentError(
|
|
532
|
+
'payment_required',
|
|
533
|
+
`Project limit reached: ${usage.appCount}/${limits.projects} projects are already used.`,
|
|
534
|
+
'Redeploy an existing app by reusing its `name` in zerct.toml, or run `npx @zerct/zerct billing` to open Stripe Checkout before creating another project.',
|
|
535
|
+
cli.json
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (newDatabases > 0 && Number(usage.databaseCount) + newDatabases > Number(limits.managedDatabases)) {
|
|
540
|
+
throw agentError(
|
|
541
|
+
'payment_required',
|
|
542
|
+
`Managed Postgres limit reached: ${usage.databaseCount}/${limits.managedDatabases} databases are already used.`,
|
|
543
|
+
'Redeploy an app that already has managed Postgres, deploy without `--database`, or run `npx @zerct/zerct billing` to open Stripe Checkout.',
|
|
544
|
+
cli.json
|
|
545
|
+
)
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
478
549
|
async function deployProject(projectDir, cli, token, wantsDatabase) {
|
|
479
550
|
const report = runDoctor(projectDir)
|
|
480
551
|
if (!report.ok) {
|
|
@@ -515,7 +586,8 @@ function printWorkspaceDeployResults(projectDir, results, cli) {
|
|
|
515
586
|
kind: result.project.kind,
|
|
516
587
|
wants_database: result.wantsDatabase,
|
|
517
588
|
app: result.response.app,
|
|
518
|
-
build_job: result.response.build_job
|
|
589
|
+
build_job: result.response.build_job,
|
|
590
|
+
final_build: result.finalBuild || null
|
|
519
591
|
}))
|
|
520
592
|
}, null, 2))
|
|
521
593
|
return
|
|
@@ -527,6 +599,35 @@ function printWorkspaceDeployResults(projectDir, results, cli) {
|
|
|
527
599
|
}
|
|
528
600
|
}
|
|
529
601
|
|
|
602
|
+
async function waitForBuild(cli, token, buildId) {
|
|
603
|
+
const deadline = Date.now() + cli.waitTimeoutSeconds * 1000
|
|
604
|
+
let lastStatus = ''
|
|
605
|
+
|
|
606
|
+
while (Date.now() <= deadline) {
|
|
607
|
+
const response = await apiRequest(cli, 'GET', `/v1/builds/${encodeURIComponent(buildId)}`, token, null)
|
|
608
|
+
const build = response.build
|
|
609
|
+
if (!build?.status) {
|
|
610
|
+
throw agentError('build_status_unavailable', 'Build status is unavailable.', `Retry with \`npx @zerct/zerct logs --build ${buildId}\`.`, cli.json)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (build.status !== lastStatus) {
|
|
614
|
+
progress(cli, `build ${build.id} ${build.status}`)
|
|
615
|
+
lastStatus = build.status
|
|
616
|
+
}
|
|
617
|
+
if (['succeeded', 'failed', 'canceled'].includes(build.status)) {
|
|
618
|
+
return build
|
|
619
|
+
}
|
|
620
|
+
await sleep(3000)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
throw agentError(
|
|
624
|
+
'build_wait_timeout',
|
|
625
|
+
`Timed out waiting for build ${buildId}.`,
|
|
626
|
+
`Run \`npx @zerct/zerct logs --build ${buildId}\` to continue watching.`,
|
|
627
|
+
cli.json
|
|
628
|
+
)
|
|
629
|
+
}
|
|
630
|
+
|
|
530
631
|
async function logs(cli) {
|
|
531
632
|
const token = await readOrLoginToken(process.cwd(), cli)
|
|
532
633
|
const page = pageQuery(cli)
|
|
@@ -1289,9 +1390,9 @@ function deployProjectInfo(dir, rootDir) {
|
|
|
1289
1390
|
const relative = path.relative(rootDir, dir).replace(/\\/gu, '/') || '.'
|
|
1290
1391
|
try {
|
|
1291
1392
|
const config = parseZerctToml(readFileSync(path.join(dir, 'zerct.toml'), 'utf8'), dir)
|
|
1292
|
-
return { dir, relative, kind: config.kind }
|
|
1393
|
+
return { dir, relative, name: config.name || '', kind: config.kind }
|
|
1293
1394
|
} catch (_error) {
|
|
1294
|
-
return { dir, relative, kind: 'unknown' }
|
|
1395
|
+
return { dir, relative, name: '', kind: 'unknown' }
|
|
1295
1396
|
}
|
|
1296
1397
|
}
|
|
1297
1398
|
|