@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.
- package/README.md +4 -1
- package/bin/zerct.js +130 -7
- 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.
|
|
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:
|
|
433
|
+
wants_database: wantsDatabase,
|
|
388
434
|
source_archive_base64: archive
|
|
389
435
|
}
|
|
390
436
|
|
|
391
|
-
|
|
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