@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.
Files changed (2) hide show
  1. package/bin/zerct.js +106 -5
  2. 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.9'
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerct/zerct",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Deploy Rust backends and static frontends to Zerct.",
5
5
  "type": "module",
6
6
  "bin": {