@zerct/zerct 0.1.2 → 0.1.4
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 +10 -6
- package/bin/zerct.js +622 -55
- package/package.json +5 -5
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
|
|
@@ -9,13 +9,17 @@ npx @zerct/zerct deploy
|
|
|
9
9
|
Target unscoped command, pending npm approval:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npx
|
|
13
|
-
npx
|
|
14
|
-
npx
|
|
12
|
+
npx zerct init
|
|
13
|
+
npx zerct doctor
|
|
14
|
+
npx zerct deploy
|
|
15
15
|
```
|
|
16
16
|
|
|
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
|
-
|
|
21
|
-
|
|
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
|
+
|
|
23
|
+
On first deploy, the CLI opens browser login, waits for GitHub or Google, stores
|
|
24
|
+
the Zerct session in the OS credential store when available, and continues the
|
|
25
|
+
deploy. Later commands reuse that session.
|
package/bin/zerct.js
CHANGED
|
@@ -4,11 +4,19 @@ 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.4'
|
|
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'
|
|
11
11
|
const SESSION_FILE = 'session-token'
|
|
12
|
+
const SESSION_SERVICE = 'com.zerct.cli'
|
|
13
|
+
const SESSION_ACCOUNT = 'session-token'
|
|
14
|
+
const SESSION_LABEL = 'Zerct session'
|
|
15
|
+
const DEFAULT_LOGIN_EXPIRES_SECONDS = 600
|
|
16
|
+
const DEFAULT_LOGIN_INTERVAL_SECONDS = 5
|
|
17
|
+
const DEFAULT_RUST_CHECK_COMMAND = 'cargo check --locked && cargo clippy --locked --all-targets --all-features -- -D warnings'
|
|
18
|
+
const DEFAULT_NPM_FRONTEND_CHECK_COMMAND = 'npm ci --prefer-offline --no-audit --fund=false && npm run typecheck && npm run lint'
|
|
19
|
+
const DEFAULT_BUN_FRONTEND_CHECK_COMMAND = 'bun ci && bun run typecheck && bun run lint'
|
|
12
20
|
const ARCHIVE_EXCLUDES = [
|
|
13
21
|
'.git',
|
|
14
22
|
'target',
|
|
@@ -34,8 +42,20 @@ const ARCHIVE_EXCLUDES = [
|
|
|
34
42
|
'*.sqlite3',
|
|
35
43
|
'*.db',
|
|
36
44
|
'*.log',
|
|
45
|
+
'._*',
|
|
37
46
|
'.DS_Store'
|
|
38
47
|
]
|
|
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
|
+
])
|
|
39
59
|
|
|
40
60
|
const HELP = `Zerct ${VERSION}
|
|
41
61
|
|
|
@@ -53,23 +73,12 @@ Usage:
|
|
|
53
73
|
zerct billing [--api <url>] [--json]
|
|
54
74
|
|
|
55
75
|
Agent contract:
|
|
56
|
-
-
|
|
57
|
-
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
76
|
+
- Rust backends keep Cargo.lock committed, listen on 0.0.0.0:$PORT, and return HTTP 200 from health.
|
|
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.
|
|
79
|
+
- Keep direct unsafe out of Rust source.
|
|
60
80
|
`
|
|
61
81
|
|
|
62
|
-
main().catch((error) => {
|
|
63
|
-
if (error instanceof ZerctError) {
|
|
64
|
-
printAgentError(error.payload, error.json)
|
|
65
|
-
process.exitCode = error.exitCode
|
|
66
|
-
return
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
console.error(`zerct failed: ${error.message}`)
|
|
70
|
-
process.exitCode = 1
|
|
71
|
-
})
|
|
72
|
-
|
|
73
82
|
async function main() {
|
|
74
83
|
const cli = parseArgs(process.argv.slice(2))
|
|
75
84
|
|
|
@@ -240,22 +249,11 @@ function doctorProject(projectDir, json) {
|
|
|
240
249
|
|
|
241
250
|
function runDoctor(projectDir) {
|
|
242
251
|
const checks = []
|
|
243
|
-
const requiredFiles = ['Cargo.toml', 'Cargo.lock', 'zerct.toml']
|
|
244
|
-
for (const file of requiredFiles) {
|
|
245
|
-
const ok = existsSync(path.join(projectDir, file))
|
|
246
|
-
checks.push({
|
|
247
|
-
name: file,
|
|
248
|
-
ok,
|
|
249
|
-
message: ok ? 'found' : 'missing',
|
|
250
|
-
agent_instruction: `Create and commit ${file}, then retry.`
|
|
251
|
-
})
|
|
252
|
-
}
|
|
253
|
-
|
|
254
252
|
let config = null
|
|
255
253
|
const configPath = path.join(projectDir, 'zerct.toml')
|
|
256
254
|
if (existsSync(configPath)) {
|
|
257
255
|
try {
|
|
258
|
-
config = parseZerctToml(readFileSync(configPath, 'utf8'))
|
|
256
|
+
config = parseZerctToml(readFileSync(configPath, 'utf8'), projectDir)
|
|
259
257
|
validateConfig(config)
|
|
260
258
|
checks.push({ name: 'zerct.toml', ok: true, message: 'valid' })
|
|
261
259
|
} catch (error) {
|
|
@@ -263,9 +261,42 @@ function runDoctor(projectDir) {
|
|
|
263
261
|
name: 'zerct.toml',
|
|
264
262
|
ok: false,
|
|
265
263
|
message: error.message,
|
|
266
|
-
agent_instruction:
|
|
264
|
+
agent_instruction: `Fix zerct.toml: ${error.message}.`
|
|
267
265
|
})
|
|
268
266
|
}
|
|
267
|
+
} else {
|
|
268
|
+
checks.push({
|
|
269
|
+
name: 'zerct.toml',
|
|
270
|
+
ok: false,
|
|
271
|
+
message: 'missing',
|
|
272
|
+
agent_instruction: 'Create and commit zerct.toml, then retry.'
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const kind = config?.kind || 'rust_backend'
|
|
277
|
+
const requiredFiles = kind === 'static_frontend'
|
|
278
|
+
? ['package.json']
|
|
279
|
+
: ['Cargo.toml', 'Cargo.lock']
|
|
280
|
+
for (const file of requiredFiles) {
|
|
281
|
+
const ok = existsSync(path.join(projectDir, file))
|
|
282
|
+
checks.push({
|
|
283
|
+
name: file,
|
|
284
|
+
ok,
|
|
285
|
+
message: ok ? 'found' : 'missing',
|
|
286
|
+
agent_instruction: `Create and commit ${file}, then retry.`
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (kind === 'static_frontend') {
|
|
291
|
+
const hasLockfile = frontendLockfileExists(projectDir)
|
|
292
|
+
checks.push({
|
|
293
|
+
name: 'frontend lockfile',
|
|
294
|
+
ok: hasLockfile,
|
|
295
|
+
message: hasLockfile ? 'found' : 'missing',
|
|
296
|
+
agent_instruction: 'Commit package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock, or bun.lockb, then retry.'
|
|
297
|
+
})
|
|
298
|
+
checks.push(...frontendSourceChecks(projectDir))
|
|
299
|
+
checks.push(...frontendScriptChecks(projectDir))
|
|
269
300
|
}
|
|
270
301
|
|
|
271
302
|
const unsafeHits = scanUnsafe(projectDir)
|
|
@@ -275,6 +306,10 @@ function runDoctor(projectDir) {
|
|
|
275
306
|
message: unsafeHits.length === 0 ? 'no direct unsafe found' : unsafeHits.slice(0, 5).join(', '),
|
|
276
307
|
agent_instruction: 'Remove direct unsafe usage from workspace Rust source before deploying.'
|
|
277
308
|
})
|
|
309
|
+
if (kind === 'rust_backend') {
|
|
310
|
+
checks.push(cargoCheck(projectDir))
|
|
311
|
+
checks.push(cargoClippy(projectDir))
|
|
312
|
+
}
|
|
278
313
|
|
|
279
314
|
return {
|
|
280
315
|
ok: checks.every((check) => check.ok),
|
|
@@ -284,37 +319,125 @@ function runDoctor(projectDir) {
|
|
|
284
319
|
}
|
|
285
320
|
}
|
|
286
321
|
|
|
322
|
+
function cargoCheck(projectDir) {
|
|
323
|
+
const cargo = spawnSync('cargo', ['check', '--locked', '--quiet'], {
|
|
324
|
+
cwd: projectDir,
|
|
325
|
+
encoding: 'utf8',
|
|
326
|
+
env: { ...process.env, CARGO_TERM_COLOR: 'never' },
|
|
327
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
if (cargo.error) {
|
|
331
|
+
return {
|
|
332
|
+
name: 'cargo check',
|
|
333
|
+
ok: false,
|
|
334
|
+
message: cargo.error.message,
|
|
335
|
+
agent_instruction: 'Install Rust and Cargo, then run `cargo check --locked` locally before deploying.'
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
name: 'cargo check',
|
|
341
|
+
ok: cargo.status === 0,
|
|
342
|
+
message: cargo.status === 0 ? 'passed' : (cargo.stderr || cargo.stdout || 'cargo check failed').trim().slice(0, 240),
|
|
343
|
+
agent_instruction: 'Run `cargo check --locked`, fix every compiler error and warning, then redeploy.'
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function cargoClippy(projectDir) {
|
|
348
|
+
const cargo = spawnSync('cargo', ['clippy', '--locked', '--all-targets', '--all-features', '--quiet', '--', '-D', 'warnings'], {
|
|
349
|
+
cwd: projectDir,
|
|
350
|
+
encoding: 'utf8',
|
|
351
|
+
env: { ...process.env, CARGO_TERM_COLOR: 'never' },
|
|
352
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
if (cargo.error) {
|
|
356
|
+
return {
|
|
357
|
+
name: 'cargo clippy',
|
|
358
|
+
ok: false,
|
|
359
|
+
message: cargo.error.message,
|
|
360
|
+
agent_instruction: 'Install Rust clippy, then run `cargo clippy --locked --all-targets --all-features -- -D warnings` before deploying.'
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
name: 'cargo clippy',
|
|
366
|
+
ok: cargo.status === 0,
|
|
367
|
+
message: cargo.status === 0 ? 'passed' : (cargo.stderr || cargo.stdout || 'cargo clippy failed').trim().slice(0, 240),
|
|
368
|
+
agent_instruction: 'Run `cargo clippy --locked --all-targets --all-features -- -D warnings`, fix every warning, then redeploy.'
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
287
372
|
async function login(cli) {
|
|
288
373
|
if (cli.token) {
|
|
289
|
-
writeSessionToken(
|
|
290
|
-
console.log('saved Zerct session token
|
|
374
|
+
writeSessionToken(cli.token)
|
|
375
|
+
console.log('saved Zerct session token')
|
|
291
376
|
return
|
|
292
377
|
}
|
|
293
378
|
|
|
294
|
-
|
|
295
|
-
openUrl(response.login_url)
|
|
296
|
-
console.log(`opened ${response.login_url}`)
|
|
297
|
-
console.log('After login, retry your deploy. If the CLI cannot finish automatically yet, set ZERCT_TOKEN or run `npx @zerct/zerct login --token <token>`.')
|
|
379
|
+
await loginAndStore(cli)
|
|
298
380
|
}
|
|
299
381
|
|
|
300
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) {
|
|
301
422
|
const report = runDoctor(projectDir)
|
|
302
423
|
if (!report.ok) {
|
|
303
424
|
const firstFailure = report.checks.find((check) => !check.ok)
|
|
304
425
|
throw agentError('doctor_failed', 'Zerct doctor failed.', firstFailure?.agent_instruction || 'Fix the failed checks and retry.', cli.json)
|
|
305
426
|
}
|
|
306
427
|
|
|
307
|
-
const token = readToken(projectDir, cli)
|
|
308
428
|
const archive = createArchiveBase64(projectDir)
|
|
309
429
|
const commitSha = gitCommitSha(projectDir)
|
|
310
430
|
const body = {
|
|
311
431
|
config: report.config,
|
|
312
432
|
commit_sha: commitSha,
|
|
313
|
-
wants_database:
|
|
433
|
+
wants_database: wantsDatabase,
|
|
314
434
|
source_archive_base64: archive
|
|
315
435
|
}
|
|
316
436
|
|
|
317
|
-
|
|
437
|
+
return apiRequest(cli, 'POST', '/v1/deploy', token, body)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function printDeployResult(response, cli) {
|
|
318
441
|
if (cli.json) {
|
|
319
442
|
console.log(JSON.stringify(response, null, 2))
|
|
320
443
|
return
|
|
@@ -326,6 +449,27 @@ async function deploy(projectDir, cli) {
|
|
|
326
449
|
console.log(`next npx @zerct/zerct logs --app ${response.app.id}`)
|
|
327
450
|
}
|
|
328
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
|
+
|
|
329
473
|
async function logs(cli) {
|
|
330
474
|
const response = await appGet(cli, 'logs')
|
|
331
475
|
if (cli.json) {
|
|
@@ -365,14 +509,14 @@ async function envCommand(cli) {
|
|
|
365
509
|
|
|
366
510
|
const name = assignment.slice(0, separator)
|
|
367
511
|
const value = assignment.slice(separator + 1)
|
|
368
|
-
const token =
|
|
512
|
+
const token = await readOrLoginToken(process.cwd(), cli)
|
|
369
513
|
const app = requireApp(cli)
|
|
370
514
|
const response = await apiRequest(cli, 'PUT', `/v1/apps/${encodeURIComponent(app)}/env`, token, { name, value })
|
|
371
515
|
printJsonOrPretty(cli, response)
|
|
372
516
|
}
|
|
373
517
|
|
|
374
518
|
async function billing(cli) {
|
|
375
|
-
const token =
|
|
519
|
+
const token = await readOrLoginToken(process.cwd(), cli)
|
|
376
520
|
const response = await apiRequest(cli, 'POST', '/v1/billing/checkout', token, {
|
|
377
521
|
target_plan: 'pro',
|
|
378
522
|
reason: 'Upgrade to Zerct Pro.'
|
|
@@ -386,11 +530,68 @@ async function billing(cli) {
|
|
|
386
530
|
}
|
|
387
531
|
|
|
388
532
|
async function appGet(cli, kind) {
|
|
389
|
-
const token =
|
|
533
|
+
const token = await readOrLoginToken(process.cwd(), cli)
|
|
390
534
|
const app = requireApp(cli)
|
|
391
535
|
return apiRequest(cli, 'GET', `/v1/apps/${encodeURIComponent(app)}/${kind}`, token, null)
|
|
392
536
|
}
|
|
393
537
|
|
|
538
|
+
async function readOrLoginToken(projectDir, cli) {
|
|
539
|
+
const token = readStoredToken(projectDir, cli)
|
|
540
|
+
if (token) {
|
|
541
|
+
return token
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
return loginAndStore(cli)
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function loginAndStore(cli) {
|
|
548
|
+
const start = await apiRequest(cli, 'POST', '/v1/login/device', null, null)
|
|
549
|
+
const loginUrl = start.loginUrl || start.login_url
|
|
550
|
+
if (!loginUrl) {
|
|
551
|
+
throw agentError('login_failed', 'Zerct login did not return a browser URL.', 'Retry `npx @zerct/zerct login`. If it keeps failing, check Zerct status.', cli.json)
|
|
552
|
+
}
|
|
553
|
+
openUrl(loginUrl)
|
|
554
|
+
progress(cli, 'opened browser login')
|
|
555
|
+
progress(cli, `waiting for browser login code ${start.userCode || start.user_code || 'ZERCT'}`)
|
|
556
|
+
|
|
557
|
+
const session = await pollLogin(cli, start)
|
|
558
|
+
if (!session.token) {
|
|
559
|
+
throw agentError('login_failed', 'Zerct login did not return a session token.', 'Run `npx @zerct/zerct login` again and complete the browser login.', cli.json)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
writeSessionToken(session.token)
|
|
563
|
+
progress(cli, `logged in as ${session.email || 'Zerct user'}`)
|
|
564
|
+
return session.token
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function pollLogin(cli, start) {
|
|
568
|
+
const deviceCode = start.deviceCode || start.device_code
|
|
569
|
+
if (!deviceCode) {
|
|
570
|
+
throw agentError('login_failed', 'Zerct login did not return a device code.', 'Retry `npx @zerct/zerct login`. If it keeps failing, check Zerct status.', cli.json)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const expiresMs = Number(start.expiresInSeconds || start.expires_in_seconds || DEFAULT_LOGIN_EXPIRES_SECONDS) * 1000
|
|
574
|
+
const deadline = Date.now() + expiresMs
|
|
575
|
+
let intervalMs = Number(start.intervalSeconds || start.interval_seconds || DEFAULT_LOGIN_INTERVAL_SECONDS) * 1000
|
|
576
|
+
|
|
577
|
+
while (Date.now() < deadline) {
|
|
578
|
+
await sleep(intervalMs)
|
|
579
|
+
const response = await apiRequest(cli, 'GET', `/v1/login/device/${encodeURIComponent(deviceCode)}`, null, null)
|
|
580
|
+
if (response.status === 'complete') {
|
|
581
|
+
return response
|
|
582
|
+
}
|
|
583
|
+
if (response.status === 'expired') {
|
|
584
|
+
throw agentError('login_expired', 'Zerct login expired before it completed.', 'Run `npx @zerct/zerct login` again and finish the browser login in the newly opened tab.', cli.json)
|
|
585
|
+
}
|
|
586
|
+
intervalMs = Math.max(
|
|
587
|
+
DEFAULT_LOGIN_INTERVAL_SECONDS * 1000,
|
|
588
|
+
Number(response.intervalSeconds || response.interval_seconds || DEFAULT_LOGIN_INTERVAL_SECONDS) * 1000
|
|
589
|
+
)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
throw agentError('login_expired', 'Zerct login expired before it completed.', 'Run `npx @zerct/zerct login` again and finish the browser login in the newly opened tab.', cli.json)
|
|
593
|
+
}
|
|
594
|
+
|
|
394
595
|
function requireApp(cli) {
|
|
395
596
|
if (!cli.app) {
|
|
396
597
|
throw agentError('missing_app', 'App id is required.', 'Pass `--app <app_id>`. Use the app id printed by `npx @zerct/zerct deploy`.', cli.json)
|
|
@@ -446,6 +647,7 @@ function createArchiveBase64(projectDir) {
|
|
|
446
647
|
const excludeArgs = ARCHIVE_EXCLUDES.map((pattern) => `--exclude=${pattern}`)
|
|
447
648
|
const tar = spawnSync('tar', [...excludeArgs, '-czf', '-', '-C', projectDir, '.'], {
|
|
448
649
|
encoding: 'buffer',
|
|
650
|
+
env: { ...process.env, COPYFILE_DISABLE: '1' },
|
|
449
651
|
maxBuffer: ARCHIVE_LIMIT_BYTES + 1024 * 1024
|
|
450
652
|
})
|
|
451
653
|
|
|
@@ -471,7 +673,7 @@ function gitCommitSha(projectDir) {
|
|
|
471
673
|
return git.status === 0 ? git.stdout.trim() || null : null
|
|
472
674
|
}
|
|
473
675
|
|
|
474
|
-
function
|
|
676
|
+
function readStoredToken(projectDir, cli) {
|
|
475
677
|
if (cli.token) {
|
|
476
678
|
return cli.token
|
|
477
679
|
}
|
|
@@ -479,26 +681,123 @@ function readToken(projectDir, cli) {
|
|
|
479
681
|
return process.env.ZERCT_TOKEN
|
|
480
682
|
}
|
|
481
683
|
|
|
684
|
+
const keychainToken = readKeychainToken()
|
|
685
|
+
if (keychainToken) {
|
|
686
|
+
return keychainToken
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const userToken = readTokenFile(userSessionPath())
|
|
690
|
+
if (userToken) {
|
|
691
|
+
return userToken
|
|
692
|
+
}
|
|
693
|
+
|
|
482
694
|
const projectToken = path.join(projectDir, SESSION_DIR, SESSION_FILE)
|
|
483
|
-
|
|
484
|
-
|
|
695
|
+
const legacyProjectToken = readTokenFile(projectToken)
|
|
696
|
+
if (legacyProjectToken) {
|
|
697
|
+
return legacyProjectToken
|
|
485
698
|
}
|
|
486
699
|
|
|
487
700
|
const homeToken = path.join(homedir(), SESSION_DIR, SESSION_FILE)
|
|
488
|
-
|
|
489
|
-
|
|
701
|
+
return readTokenFile(homeToken)
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function writeSessionToken(token) {
|
|
705
|
+
const cleanToken = token.trim()
|
|
706
|
+
if (!cleanToken) {
|
|
707
|
+
throw agentError('login_failed', 'Zerct session token is empty.', 'Run `npx @zerct/zerct login` again and complete the browser login.', false)
|
|
708
|
+
}
|
|
709
|
+
if (writeKeychainToken(cleanToken)) {
|
|
710
|
+
return
|
|
490
711
|
}
|
|
491
712
|
|
|
492
|
-
|
|
713
|
+
writeTokenFile(userSessionPath(), cleanToken)
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function readTokenFile(filePath) {
|
|
717
|
+
if (!existsSync(filePath)) {
|
|
718
|
+
return ''
|
|
719
|
+
}
|
|
720
|
+
return readFileSync(filePath, 'utf8').trim()
|
|
493
721
|
}
|
|
494
722
|
|
|
495
|
-
function
|
|
496
|
-
const dir = path.
|
|
723
|
+
function writeTokenFile(filePath, token) {
|
|
724
|
+
const dir = path.dirname(filePath)
|
|
497
725
|
mkdirSync(dir, { recursive: true, mode: 0o700 })
|
|
498
|
-
writeFileSync(
|
|
726
|
+
writeFileSync(filePath, `${token}\n`, { mode: 0o600 })
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function userSessionPath() {
|
|
730
|
+
if (process.platform === 'win32' && process.env.APPDATA) {
|
|
731
|
+
return path.join(process.env.APPDATA, 'Zerct', SESSION_FILE)
|
|
732
|
+
}
|
|
733
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(homedir(), '.config')
|
|
734
|
+
return path.join(configHome, 'zerct', SESSION_FILE)
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function readKeychainToken() {
|
|
738
|
+
if (process.platform === 'darwin') {
|
|
739
|
+
const result = spawnSync('security', ['find-generic-password', '-s', SESSION_SERVICE, '-a', SESSION_ACCOUNT, '-w'], {
|
|
740
|
+
encoding: 'utf8',
|
|
741
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
742
|
+
})
|
|
743
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (process.platform === 'linux' && hasCommand('secret-tool')) {
|
|
747
|
+
const result = spawnSync('secret-tool', ['lookup', 'service', SESSION_SERVICE, 'account', SESSION_ACCOUNT], {
|
|
748
|
+
encoding: 'utf8',
|
|
749
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
750
|
+
})
|
|
751
|
+
return result.status === 0 ? result.stdout.trim() : ''
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return ''
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function writeKeychainToken(token) {
|
|
758
|
+
if (process.platform === 'darwin') {
|
|
759
|
+
const result = spawnSync('security', [
|
|
760
|
+
'add-generic-password',
|
|
761
|
+
'-U',
|
|
762
|
+
'-s',
|
|
763
|
+
SESSION_SERVICE,
|
|
764
|
+
'-a',
|
|
765
|
+
SESSION_ACCOUNT,
|
|
766
|
+
'-l',
|
|
767
|
+
SESSION_LABEL,
|
|
768
|
+
'-w',
|
|
769
|
+
token
|
|
770
|
+
], { stdio: 'ignore' })
|
|
771
|
+
return result.status === 0
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
if (process.platform === 'linux' && hasCommand('secret-tool')) {
|
|
775
|
+
const result = spawnSync('secret-tool', [
|
|
776
|
+
'store',
|
|
777
|
+
'--label',
|
|
778
|
+
SESSION_LABEL,
|
|
779
|
+
'service',
|
|
780
|
+
SESSION_SERVICE,
|
|
781
|
+
'account',
|
|
782
|
+
SESSION_ACCOUNT
|
|
783
|
+
], {
|
|
784
|
+
input: token,
|
|
785
|
+
stdio: ['pipe', 'ignore', 'ignore']
|
|
786
|
+
})
|
|
787
|
+
return result.status === 0
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return false
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function hasCommand(command) {
|
|
794
|
+
return (process.env.PATH || '')
|
|
795
|
+
.split(path.delimiter)
|
|
796
|
+
.filter(Boolean)
|
|
797
|
+
.some((directory) => existsSync(path.join(directory, command)))
|
|
499
798
|
}
|
|
500
799
|
|
|
501
|
-
function parseZerctToml(source) {
|
|
800
|
+
function parseZerctToml(source, projectDir) {
|
|
502
801
|
const config = {
|
|
503
802
|
build: {},
|
|
504
803
|
run: {},
|
|
@@ -530,7 +829,12 @@ function parseZerctToml(source) {
|
|
|
530
829
|
section[assignment[1]] = parseTomlValue(assignment[2])
|
|
531
830
|
}
|
|
532
831
|
|
|
533
|
-
config.
|
|
832
|
+
config.kind ||= 'rust_backend'
|
|
833
|
+
config.build.check ||= config.kind === 'static_frontend' ? frontendCheckCommand(projectDir) : DEFAULT_RUST_CHECK_COMMAND
|
|
834
|
+
config.build.command ||= config.kind === 'static_frontend' ? frontendBuildCommand(projectDir) : 'cargo build --release'
|
|
835
|
+
if (config.kind === 'static_frontend') {
|
|
836
|
+
config.build.output ||= 'dist'
|
|
837
|
+
}
|
|
534
838
|
config.run.port ||= 3000
|
|
535
839
|
config.run.health ||= '/healthz'
|
|
536
840
|
config.resources.memory ||= '512mb'
|
|
@@ -560,6 +864,22 @@ function validateConfig(config) {
|
|
|
560
864
|
if (!/^[a-z0-9](?:[a-z0-9-]{0,46}[a-z0-9])?$/u.test(config.name || '')) {
|
|
561
865
|
throw new Error('name must be lowercase DNS-safe text up to 48 characters')
|
|
562
866
|
}
|
|
867
|
+
if (!['rust_backend', 'static_frontend'].includes(config.kind)) {
|
|
868
|
+
throw new Error('kind must be rust_backend or static_frontend')
|
|
869
|
+
}
|
|
870
|
+
if (typeof config.build.command !== 'string' || !config.build.command.trim()) {
|
|
871
|
+
throw new Error('[build].command is required')
|
|
872
|
+
}
|
|
873
|
+
if (typeof config.build.check !== 'string' || !config.build.check.trim()) {
|
|
874
|
+
throw new Error('[build].check is required')
|
|
875
|
+
}
|
|
876
|
+
validateCheckCommand(config.kind, config.build.check)
|
|
877
|
+
if (config.kind === 'static_frontend') {
|
|
878
|
+
if (typeof config.build.output !== 'string' || !isSafeRelativePath(config.build.output)) {
|
|
879
|
+
throw new Error('[build].output must be a safe relative directory like dist')
|
|
880
|
+
}
|
|
881
|
+
return
|
|
882
|
+
}
|
|
563
883
|
if (!config.run.command || typeof config.run.command !== 'string') {
|
|
564
884
|
throw new Error('[run].command is required')
|
|
565
885
|
}
|
|
@@ -577,23 +897,245 @@ function validateConfig(config) {
|
|
|
577
897
|
}
|
|
578
898
|
}
|
|
579
899
|
|
|
900
|
+
function validateCheckCommand(kind, command) {
|
|
901
|
+
if (kind === 'static_frontend' && usesJavascriptLinter(command)) {
|
|
902
|
+
throw new Error('[build].check must not run JavaScript-based linters; use oxlint, biome, or deno lint')
|
|
903
|
+
}
|
|
904
|
+
const required = kind === 'static_frontend'
|
|
905
|
+
? ['typecheck', 'lint']
|
|
906
|
+
: ['cargo check --locked', 'cargo clippy --locked', '--all-targets', '--all-features', '-D warnings']
|
|
907
|
+
if (required.every((fragment) => command.includes(fragment))) {
|
|
908
|
+
return
|
|
909
|
+
}
|
|
910
|
+
throw new Error(kind === 'static_frontend'
|
|
911
|
+
? '[build].check must run frontend typecheck and lint'
|
|
912
|
+
: '[build].check must include cargo check --locked and cargo clippy --locked --all-targets --all-features -- -D warnings')
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function frontendLockfileExists(projectDir) {
|
|
916
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lock', 'bun.lockb']
|
|
917
|
+
.some((file) => existsSync(path.join(projectDir, file)))
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function frontendPackageManager(projectDir) {
|
|
921
|
+
return existsSync(path.join(projectDir, 'bun.lock')) || existsSync(path.join(projectDir, 'bun.lockb'))
|
|
922
|
+
? 'bun'
|
|
923
|
+
: 'npm'
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function frontendCheckCommand(projectDir) {
|
|
927
|
+
return frontendPackageManager(projectDir) === 'bun'
|
|
928
|
+
? DEFAULT_BUN_FRONTEND_CHECK_COMMAND
|
|
929
|
+
: DEFAULT_NPM_FRONTEND_CHECK_COMMAND
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function frontendBuildCommand(projectDir) {
|
|
933
|
+
return frontendPackageManager(projectDir) === 'bun'
|
|
934
|
+
? 'bun run build'
|
|
935
|
+
: 'npm run build'
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function frontendScriptChecks(projectDir) {
|
|
939
|
+
const manifest = readPackageJson(projectDir)
|
|
940
|
+
const missing = (script) => !manifest?.scripts || typeof manifest.scripts[script] !== 'string' || !manifest.scripts[script].trim()
|
|
941
|
+
const checks = ['typecheck', 'lint'].map((script) => ({
|
|
942
|
+
name: `package script ${script}`,
|
|
943
|
+
ok: !missing(script),
|
|
944
|
+
message: missing(script) ? 'missing' : 'found',
|
|
945
|
+
agent_instruction: `Add a non-empty "${script}" script to package.json, then retry.`
|
|
946
|
+
}))
|
|
947
|
+
const lintScript = manifest?.scripts?.lint || ''
|
|
948
|
+
const nativeLint = !lintScript || !usesJavascriptLinter(lintScript)
|
|
949
|
+
checks.push({
|
|
950
|
+
name: 'native frontend lint',
|
|
951
|
+
ok: nativeLint,
|
|
952
|
+
message: nativeLint ? 'accepted' : 'JavaScript linter found',
|
|
953
|
+
agent_instruction: 'Replace the lint script with native tooling such as `oxlint src vite.config.ts --deny-warnings`, `biome check .`, or `deno lint`, then retry.'
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
if (checks.every((check) => check.ok)) {
|
|
957
|
+
checks.push(packageScriptCheck(projectDir, 'typecheck'))
|
|
958
|
+
checks.push(packageScriptCheck(projectDir, 'lint'))
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
return checks
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function frontendSourceChecks(projectDir) {
|
|
965
|
+
const report = frontendSourceReport(projectDir)
|
|
966
|
+
return [
|
|
967
|
+
{
|
|
968
|
+
name: 'typescript source',
|
|
969
|
+
ok: report.typescript.length > 0,
|
|
970
|
+
message: report.typescript.length > 0 ? report.typescript.slice(0, 3).join(', ') : 'missing',
|
|
971
|
+
agent_instruction: 'Add browser source as .ts or .tsx under src, app, pages, routes, or components, then retry.'
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: 'javascript source',
|
|
975
|
+
ok: report.javascript.length === 0,
|
|
976
|
+
message: report.javascript.length === 0 ? 'none found' : report.javascript.slice(0, 5).join(', '),
|
|
977
|
+
agent_instruction: 'Rename browser .js, .jsx, .mjs, or .cjs source files to .ts or .tsx and fix type errors before deploying.'
|
|
978
|
+
}
|
|
979
|
+
]
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function frontendSourceReport(projectDir) {
|
|
983
|
+
const report = { typescript: [], javascript: [] }
|
|
984
|
+
walkProjectFiles(projectDir, (file, relative) => {
|
|
985
|
+
if (!isFrontendSourcePath(relative)) {
|
|
986
|
+
return
|
|
987
|
+
}
|
|
988
|
+
if (isFrontendTypescriptSource(relative)) {
|
|
989
|
+
report.typescript.push(relative)
|
|
990
|
+
} else if (isFrontendJavascriptSource(relative)) {
|
|
991
|
+
report.javascript.push(relative)
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
return report
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function isFrontendSourcePath(relative) {
|
|
998
|
+
const [root] = relative.split('/')
|
|
999
|
+
return ['src', 'app', 'pages', 'routes', 'components'].includes(root)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function isFrontendTypescriptSource(relative) {
|
|
1003
|
+
return !relative.endsWith('.d.ts') && (relative.endsWith('.ts') || relative.endsWith('.tsx'))
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function isFrontendJavascriptSource(relative) {
|
|
1007
|
+
return ['.js', '.jsx', '.mjs', '.cjs'].some((extension) => relative.endsWith(extension))
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function readPackageJson(projectDir) {
|
|
1011
|
+
try {
|
|
1012
|
+
return JSON.parse(readFileSync(path.join(projectDir, 'package.json'), 'utf8'))
|
|
1013
|
+
} catch (_error) {
|
|
1014
|
+
return null
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function usesJavascriptLinter(command) {
|
|
1019
|
+
const tokens = command
|
|
1020
|
+
.replace(/[&|;()]/gu, ' ')
|
|
1021
|
+
.split(/\s+/u)
|
|
1022
|
+
.map((token) => token.trim().replace(/^["']|["']$/gu, ''))
|
|
1023
|
+
.filter(Boolean)
|
|
1024
|
+
return tokens.some((token, index) => {
|
|
1025
|
+
const commandName = token.split('/').pop()
|
|
1026
|
+
return ['eslint', 'eslint_d', 'standard', 'xo'].includes(commandName)
|
|
1027
|
+
|| (commandName === 'next' && tokens[index + 1] === 'lint')
|
|
1028
|
+
})
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function packageScriptCheck(projectDir, script) {
|
|
1032
|
+
const manager = frontendPackageManager(projectDir)
|
|
1033
|
+
const args = manager === 'bun' ? ['run', script] : ['run', '--silent', script]
|
|
1034
|
+
const result = spawnSync(manager, args, {
|
|
1035
|
+
cwd: projectDir,
|
|
1036
|
+
encoding: 'utf8',
|
|
1037
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
1038
|
+
})
|
|
1039
|
+
if (result.error) {
|
|
1040
|
+
return {
|
|
1041
|
+
name: `${manager} run ${script}`,
|
|
1042
|
+
ok: false,
|
|
1043
|
+
message: result.error.message,
|
|
1044
|
+
agent_instruction: `Install ${manager === 'bun' ? 'Bun' : 'Node.js and npm'}, then run \`${manager} run ${script}\` before deploying.`
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return {
|
|
1049
|
+
name: `${manager} run ${script}`,
|
|
1050
|
+
ok: result.status === 0,
|
|
1051
|
+
message: result.status === 0 ? 'passed' : (result.stderr || result.stdout || `${manager} run ${script} failed`).trim().slice(0, 240),
|
|
1052
|
+
agent_instruction: `Run \`${manager} run ${script}\`, fix every error, then redeploy.`
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
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
|
+
|
|
1109
|
+
function isSafeRelativePath(value) {
|
|
1110
|
+
return value
|
|
1111
|
+
&& !path.isAbsolute(value)
|
|
1112
|
+
&& !value.includes('\\')
|
|
1113
|
+
&& value.split('/').every((part) => part && part !== '.' && part !== '..')
|
|
1114
|
+
}
|
|
1115
|
+
|
|
580
1116
|
function scanUnsafe(projectDir) {
|
|
581
1117
|
const hits = []
|
|
582
|
-
|
|
1118
|
+
walkProjectFiles(projectDir, (file, relative) => {
|
|
583
1119
|
if (!file.endsWith('.rs')) {
|
|
584
1120
|
return
|
|
585
1121
|
}
|
|
586
1122
|
const source = readFileSync(file, 'utf8')
|
|
587
1123
|
if (/\bunsafe\b/u.test(source)) {
|
|
588
|
-
hits.push(
|
|
1124
|
+
hits.push(relative)
|
|
589
1125
|
}
|
|
590
1126
|
})
|
|
591
1127
|
return hits
|
|
592
1128
|
}
|
|
593
1129
|
|
|
1130
|
+
function walkProjectFiles(projectDir, visit) {
|
|
1131
|
+
walk(projectDir, (file) => {
|
|
1132
|
+
visit(file, path.relative(projectDir, file).replace(/\\/gu, '/'))
|
|
1133
|
+
})
|
|
1134
|
+
}
|
|
1135
|
+
|
|
594
1136
|
function walk(dir, visit) {
|
|
595
1137
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
596
|
-
if (
|
|
1138
|
+
if (WALK_EXCLUDED_DIRS.has(entry.name)) {
|
|
597
1139
|
continue
|
|
598
1140
|
}
|
|
599
1141
|
const fullPath = path.join(dir, entry.name)
|
|
@@ -626,6 +1168,20 @@ function openUrl(url) {
|
|
|
626
1168
|
spawnSync(command, args, { stdio: 'ignore', detached: true })
|
|
627
1169
|
}
|
|
628
1170
|
|
|
1171
|
+
function sleep(milliseconds) {
|
|
1172
|
+
return new Promise((resolve) => {
|
|
1173
|
+
setTimeout(resolve, milliseconds)
|
|
1174
|
+
})
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
function progress(cli, message) {
|
|
1178
|
+
if (cli.json) {
|
|
1179
|
+
console.error(message)
|
|
1180
|
+
return
|
|
1181
|
+
}
|
|
1182
|
+
console.log(message)
|
|
1183
|
+
}
|
|
1184
|
+
|
|
629
1185
|
function trimTrailingSlash(value) {
|
|
630
1186
|
return value.replace(/\/+$/u, '')
|
|
631
1187
|
}
|
|
@@ -666,3 +1222,14 @@ class ZerctError extends Error {
|
|
|
666
1222
|
this.exitCode = exitCode
|
|
667
1223
|
}
|
|
668
1224
|
}
|
|
1225
|
+
|
|
1226
|
+
main().catch((error) => {
|
|
1227
|
+
if (error instanceof ZerctError) {
|
|
1228
|
+
printAgentError(error.payload, error.json)
|
|
1229
|
+
process.exitCode = error.exitCode
|
|
1230
|
+
return
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
console.error(`zerct failed: ${error.message}`)
|
|
1234
|
+
process.exitCode = 1
|
|
1235
|
+
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zerct/zerct",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Deploy Rust backends to Zerct.",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Deploy Rust backends and static frontends to Zerct.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"zerct": "bin/zerct.js"
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"bin",
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
13
16
|
"engines": {
|
|
14
17
|
"node": ">=18.17"
|
|
15
18
|
},
|
|
@@ -33,8 +36,5 @@
|
|
|
33
36
|
"scripts": {
|
|
34
37
|
"check": "node --check bin/zerct.js",
|
|
35
38
|
"pack:dry": "npm pack --dry-run"
|
|
36
|
-
},
|
|
37
|
-
"publishConfig": {
|
|
38
|
-
"access": "public"
|
|
39
39
|
}
|
|
40
40
|
}
|