@zerct/zerct 0.1.3 → 0.1.5

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 (3) hide show
  1. package/README.md +4 -1
  2. package/bin/zerct.js +130 -7
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # zerct
2
2
 
3
- Deploy Rust backends to Zerct.
3
+ Deploy Rust backends and static frontends to Zerct.
4
4
 
5
5
  ```sh
6
6
  npx @zerct/zerct deploy
@@ -17,6 +17,9 @@ npx zerct deploy
17
17
  Zerct expects `Cargo.toml`, `Cargo.lock`, and `zerct.toml`. The app must listen
18
18
  on `0.0.0.0:$PORT` and expose the configured health endpoint.
19
19
 
20
+ From a full-stack repo root, the same deploy command discovers nested
21
+ `zerct.toml` files and deploys the whole workspace in one command.
22
+
20
23
  On first deploy, the CLI opens browser login, waits for GitHub or Google, stores
21
24
  the Zerct session in the OS credential store when available, and continues the
22
25
  deploy. Later commands reuse that session.
package/bin/zerct.js CHANGED
@@ -4,7 +4,7 @@ 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.3'
7
+ const VERSION = '0.1.5'
8
8
  const DEFAULT_API_URL = 'https://api.zerct.com'
9
9
  const ARCHIVE_LIMIT_BYTES = 48 * 1024 * 1024
10
10
  const SESSION_DIR = '.zerct'
@@ -46,6 +46,16 @@ const ARCHIVE_EXCLUDES = [
46
46
  '.DS_Store'
47
47
  ]
48
48
  const WALK_EXCLUDED_DIRS = new Set(['.git', 'target', 'node_modules', '.zerct'])
49
+ const WORKSPACE_EXCLUDED_DIRS = new Set([
50
+ ...WALK_EXCLUDED_DIRS,
51
+ '.cache',
52
+ '.next',
53
+ '.turbo',
54
+ 'build',
55
+ 'coverage',
56
+ 'dist',
57
+ 'vendor'
58
+ ])
49
59
 
50
60
  const HELP = `Zerct ${VERSION}
51
61
 
@@ -65,6 +75,7 @@ Usage:
65
75
  Agent contract:
66
76
  - Rust backends keep Cargo.lock committed, listen on 0.0.0.0:$PORT, and return HTTP 200 from health.
67
77
  - Static frontends set kind = "static_frontend", keep TypeScript source, a package lockfile, and typecheck + lint scripts.
78
+ - Run deploy from a repo root with nested zerct.toml files to deploy the whole workspace in one command.
68
79
  - Keep direct unsafe out of Rust source.
69
80
  `
70
81
 
@@ -369,26 +380,64 @@ async function login(cli) {
369
380
  }
370
381
 
371
382
  async function deploy(projectDir, cli) {
383
+ const projects = discoverDeployProjects(projectDir)
384
+ if (projects.length === 0) {
385
+ throw agentError('missing_project_contract', 'No zerct.toml was found.', 'Run `npx @zerct/zerct init` in each app directory, or pass a project path.', cli.json)
386
+ }
387
+
388
+ if (projects.length === 1) {
389
+ const project = projects[0]
390
+ if (project.kind === 'static_frontend' && cli.database) {
391
+ 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)
392
+ }
393
+ const token = await readOrLoginToken(project.dir, cli)
394
+ const result = await deployProject(project.dir, cli, token, cli.database)
395
+ printDeployResult(result, cli)
396
+ return
397
+ }
398
+
399
+ const token = await readOrLoginToken(projectDir, cli)
400
+ const results = []
401
+ if (!cli.json) {
402
+ console.log(`deploying ${projects.length} projects`)
403
+ }
404
+
405
+ for (const project of projects) {
406
+ const wantsDatabase = cli.database && project.kind === 'rust_backend'
407
+ if (!cli.json) {
408
+ console.log(`checking ${project.relative}`)
409
+ }
410
+ const response = await deployProject(project.dir, cli, token, wantsDatabase)
411
+ results.push({ project, wantsDatabase, response })
412
+ if (!cli.json) {
413
+ console.log(`${project.relative} queued ${response.build_job.id}`)
414
+ console.log(`${project.relative} url ${response.app.url}`)
415
+ }
416
+ }
417
+
418
+ printWorkspaceDeployResults(projectDir, results, cli)
419
+ }
420
+
421
+ async function deployProject(projectDir, cli, token, wantsDatabase) {
372
422
  const report = runDoctor(projectDir)
373
423
  if (!report.ok) {
374
424
  const firstFailure = report.checks.find((check) => !check.ok)
375
425
  throw agentError('doctor_failed', 'Zerct doctor failed.', firstFailure?.agent_instruction || 'Fix the failed checks and retry.', cli.json)
376
426
  }
377
- if (report.config?.kind === 'static_frontend' && cli.database) {
378
- 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)
379
- }
380
427
 
381
- const token = await readOrLoginToken(projectDir, cli)
382
428
  const archive = createArchiveBase64(projectDir)
383
429
  const commitSha = gitCommitSha(projectDir)
384
430
  const body = {
385
431
  config: report.config,
386
432
  commit_sha: commitSha,
387
- wants_database: cli.database,
433
+ wants_database: wantsDatabase,
388
434
  source_archive_base64: archive
389
435
  }
390
436
 
391
- const response = await apiRequest(cli, 'POST', '/v1/deployments', token, body)
437
+ return apiRequest(cli, 'POST', '/v1/deploy', token, body)
438
+ }
439
+
440
+ function printDeployResult(response, cli) {
392
441
  if (cli.json) {
393
442
  console.log(JSON.stringify(response, null, 2))
394
443
  return
@@ -400,6 +449,27 @@ async function deploy(projectDir, cli) {
400
449
  console.log(`next npx @zerct/zerct logs --app ${response.app.id}`)
401
450
  }
402
451
 
452
+ function printWorkspaceDeployResults(projectDir, results, cli) {
453
+ if (cli.json) {
454
+ console.log(JSON.stringify({
455
+ workspace: projectDir,
456
+ deploys: results.map((result) => ({
457
+ path: result.project.relative,
458
+ kind: result.project.kind,
459
+ wants_database: result.wantsDatabase,
460
+ app: result.response.app,
461
+ build_job: result.response.build_job
462
+ }))
463
+ }, null, 2))
464
+ return
465
+ }
466
+
467
+ const firstApp = results[0]?.response?.app?.id
468
+ if (firstApp) {
469
+ console.log(`next npx @zerct/zerct logs --app ${firstApp}`)
470
+ }
471
+ }
472
+
403
473
  async function logs(cli) {
404
474
  const response = await appGet(cli, 'logs')
405
475
  if (cli.json) {
@@ -983,6 +1053,59 @@ function packageScriptCheck(projectDir, script) {
983
1053
  }
984
1054
  }
985
1055
 
1056
+ function discoverDeployProjects(rootDir) {
1057
+ ensureDirectory(rootDir)
1058
+ if (existsSync(path.join(rootDir, 'zerct.toml'))) {
1059
+ return [deployProjectInfo(rootDir, rootDir)]
1060
+ }
1061
+
1062
+ const projectDirs = []
1063
+ discoverProjectDirs(rootDir, projectDirs)
1064
+ return projectDirs
1065
+ .map((dir) => deployProjectInfo(dir, rootDir))
1066
+ .sort(compareDeployProjects)
1067
+ }
1068
+
1069
+ function discoverProjectDirs(dir, projectDirs) {
1070
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1071
+ if (!entry.isDirectory() || WORKSPACE_EXCLUDED_DIRS.has(entry.name)) {
1072
+ continue
1073
+ }
1074
+
1075
+ const child = path.join(dir, entry.name)
1076
+ if (existsSync(path.join(child, 'zerct.toml'))) {
1077
+ projectDirs.push(child)
1078
+ continue
1079
+ }
1080
+ discoverProjectDirs(child, projectDirs)
1081
+ }
1082
+ }
1083
+
1084
+ function deployProjectInfo(dir, rootDir) {
1085
+ const relative = path.relative(rootDir, dir).replace(/\\/gu, '/') || '.'
1086
+ try {
1087
+ const config = parseZerctToml(readFileSync(path.join(dir, 'zerct.toml'), 'utf8'), dir)
1088
+ return { dir, relative, kind: config.kind }
1089
+ } catch (_error) {
1090
+ return { dir, relative, kind: 'unknown' }
1091
+ }
1092
+ }
1093
+
1094
+ function compareDeployProjects(left, right) {
1095
+ return kindOrder(left.kind) - kindOrder(right.kind)
1096
+ || left.relative.localeCompare(right.relative)
1097
+ }
1098
+
1099
+ function kindOrder(kind) {
1100
+ if (kind === 'rust_backend') {
1101
+ return 0
1102
+ }
1103
+ if (kind === 'static_frontend') {
1104
+ return 1
1105
+ }
1106
+ return 2
1107
+ }
1108
+
986
1109
  function isSafeRelativePath(value) {
987
1110
  return value
988
1111
  && !path.isAbsolute(value)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@zerct/zerct",
3
- "version": "0.1.3",
4
- "description": "Deploy Rust backends to Zerct.",
3
+ "version": "0.1.5",
4
+ "description": "Deploy Rust backends and static frontends to Zerct.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "zerct": "bin/zerct.js"