@zerct/zerct 0.1.0

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 +21 -0
  2. package/bin/zerct.js +652 -0
  3. package/package.json +40 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # zerct
2
+
3
+ Deploy Rust backends to Zerct.
4
+
5
+ ```sh
6
+ npx @zerct/zerct deploy
7
+ ```
8
+
9
+ Target unscoped command, pending npm approval:
10
+
11
+ ```sh
12
+ npx zerct init
13
+ npx zerct doctor
14
+ npx zerct deploy
15
+ ```
16
+
17
+ Zerct expects `Cargo.toml`, `Cargo.lock`, and `zerct.toml`. The app must listen
18
+ on `0.0.0.0:$PORT` and expose the configured health endpoint.
19
+
20
+ Use `ZERCT_TOKEN` or `npx zerct login --token <token>` for authenticated
21
+ commands.
package/bin/zerct.js ADDED
@@ -0,0 +1,652 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process'
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import path from 'node:path'
6
+
7
+ const VERSION = '0.1.0'
8
+ const DEFAULT_API_URL = 'https://api.zerct.com'
9
+ const ARCHIVE_LIMIT_BYTES = 48 * 1024 * 1024
10
+ const SESSION_DIR = '.zerct'
11
+ const SESSION_FILE = 'session-token'
12
+
13
+ const HELP = `Zerct ${VERSION}
14
+
15
+ Usage:
16
+ zerct init [path]
17
+ zerct install [path]
18
+ zerct doctor [path] [--json]
19
+ zerct login [--token <token>] [--api <url>]
20
+ zerct deploy [path] [--database] [--api <url>] [--json]
21
+ zerct logs --app <app_id> [--api <url>] [--json]
22
+ zerct status --app <app_id> [--api <url>] [--json]
23
+ zerct inspect --app <app_id> [--api <url>] [--json]
24
+ zerct db --app <app_id> [--api <url>] [--json]
25
+ zerct env set --app <app_id> KEY=value [--api <url>] [--json]
26
+ zerct billing [--api <url>] [--json]
27
+
28
+ Agent contract:
29
+ - Keep Cargo.lock committed.
30
+ - Keep direct unsafe out of workspace source.
31
+ - Listen on 0.0.0.0:$PORT.
32
+ - Return HTTP 200 from the configured health endpoint.
33
+ `
34
+
35
+ main().catch((error) => {
36
+ if (error instanceof ZerctError) {
37
+ printAgentError(error.payload, error.json)
38
+ process.exitCode = error.exitCode
39
+ return
40
+ }
41
+
42
+ console.error(`zerct failed: ${error.message}`)
43
+ process.exitCode = 1
44
+ })
45
+
46
+ async function main() {
47
+ const cli = parseArgs(process.argv.slice(2))
48
+
49
+ if (cli.help) {
50
+ console.log(HELP)
51
+ return
52
+ }
53
+
54
+ if (cli.version) {
55
+ console.log(VERSION)
56
+ return
57
+ }
58
+
59
+ switch (cli.command) {
60
+ case 'init':
61
+ initProject(projectPath(cli.args[0]))
62
+ break
63
+ case 'install':
64
+ installProject(projectPath(cli.args[0]))
65
+ break
66
+ case 'doctor':
67
+ doctorProject(projectPath(cli.args[0]), cli.json)
68
+ break
69
+ case 'login':
70
+ await login(cli)
71
+ break
72
+ case 'deploy':
73
+ await deploy(projectPath(cli.args[0]), cli)
74
+ break
75
+ case 'logs':
76
+ await logs(cli)
77
+ break
78
+ case 'status':
79
+ await status(cli)
80
+ break
81
+ case 'inspect':
82
+ await inspect(cli)
83
+ break
84
+ case 'db':
85
+ case 'database':
86
+ await database(cli)
87
+ break
88
+ case 'env':
89
+ await envCommand(cli)
90
+ break
91
+ case 'billing':
92
+ await billing(cli)
93
+ break
94
+ default:
95
+ throw agentError('unknown_command', 'Unknown Zerct command.', 'Run `npx zerct --help` and retry with a supported command.', cli.json)
96
+ }
97
+ }
98
+
99
+ function parseArgs(argv) {
100
+ const cli = {
101
+ command: 'help',
102
+ args: [],
103
+ apiUrl: DEFAULT_API_URL,
104
+ app: '',
105
+ token: '',
106
+ json: false,
107
+ database: false,
108
+ help: false,
109
+ version: false
110
+ }
111
+
112
+ const positional = []
113
+ for (let index = 0; index < argv.length; index += 1) {
114
+ const arg = argv[index]
115
+ if (arg === '--help' || arg === '-h') {
116
+ cli.help = true
117
+ } else if (arg === '--version' || arg === '-v') {
118
+ cli.version = true
119
+ } else if (arg === '--json') {
120
+ cli.json = true
121
+ } else if (arg === '--database') {
122
+ cli.database = true
123
+ } else if (arg === '--no-database') {
124
+ cli.database = false
125
+ } else if (arg === '--api') {
126
+ cli.apiUrl = requireValue(argv, index, '--api')
127
+ index += 1
128
+ } else if (arg === '--app') {
129
+ cli.app = requireValue(argv, index, '--app')
130
+ index += 1
131
+ } else if (arg === '--token') {
132
+ cli.token = requireValue(argv, index, '--token')
133
+ index += 1
134
+ } else {
135
+ positional.push(arg)
136
+ }
137
+ }
138
+
139
+ if (positional.length > 0) {
140
+ cli.command = positional[0]
141
+ cli.args = positional.slice(1)
142
+ }
143
+
144
+ cli.apiUrl = trimTrailingSlash(cli.apiUrl)
145
+ return cli
146
+ }
147
+
148
+ function requireValue(argv, index, name) {
149
+ const value = argv[index + 1]
150
+ if (!value || value.startsWith('--')) {
151
+ throw agentError('missing_argument', `${name} requires a value.`, `Pass a value after ${name}.`, false)
152
+ }
153
+ return value
154
+ }
155
+
156
+ function projectPath(value) {
157
+ return path.resolve(value || process.cwd())
158
+ }
159
+
160
+ function initProject(projectDir) {
161
+ ensureDirectory(projectDir)
162
+ const configPath = path.join(projectDir, 'zerct.toml')
163
+ if (existsSync(configPath)) {
164
+ console.log('zerct.toml already exists')
165
+ return
166
+ }
167
+
168
+ const name = serviceNameFromDir(projectDir)
169
+ const source = `name = "${name}"
170
+
171
+ [build]
172
+ command = "cargo build --release"
173
+
174
+ [run]
175
+ command = "./target/release/${name}"
176
+ port = 3000
177
+ health = "/healthz"
178
+
179
+ [resources]
180
+ memory = "512mb"
181
+ cpu = "0.25"
182
+ idle_timeout_minutes = 15
183
+ `
184
+
185
+ writeFileSync(configPath, source, { mode: 0o644 })
186
+ console.log(`created ${path.relative(process.cwd(), configPath)}`)
187
+ }
188
+
189
+ function installProject(projectDir) {
190
+ initProject(projectDir)
191
+ doctorProject(projectDir, false)
192
+ }
193
+
194
+ function doctorProject(projectDir, json) {
195
+ const report = runDoctor(projectDir)
196
+ if (json) {
197
+ console.log(JSON.stringify(report, null, 2))
198
+ if (!report.ok) {
199
+ process.exitCode = 1
200
+ }
201
+ return
202
+ }
203
+
204
+ for (const check of report.checks) {
205
+ console.log(`${check.ok ? 'ok' : 'fail'} ${check.name}${check.message ? ` - ${check.message}` : ''}`)
206
+ }
207
+
208
+ if (!report.ok) {
209
+ const firstFailure = report.checks.find((check) => !check.ok)
210
+ throw agentError('doctor_failed', 'Zerct doctor failed.', firstFailure?.agent_instruction || 'Fix the failed checks and retry `npx zerct doctor`.', json)
211
+ }
212
+ }
213
+
214
+ function runDoctor(projectDir) {
215
+ const checks = []
216
+ const requiredFiles = ['Cargo.toml', 'Cargo.lock', 'zerct.toml']
217
+ for (const file of requiredFiles) {
218
+ const ok = existsSync(path.join(projectDir, file))
219
+ checks.push({
220
+ name: file,
221
+ ok,
222
+ message: ok ? 'found' : 'missing',
223
+ agent_instruction: `Create and commit ${file}, then retry.`
224
+ })
225
+ }
226
+
227
+ let config = null
228
+ const configPath = path.join(projectDir, 'zerct.toml')
229
+ if (existsSync(configPath)) {
230
+ try {
231
+ config = parseZerctToml(readFileSync(configPath, 'utf8'))
232
+ validateConfig(config)
233
+ checks.push({ name: 'zerct.toml', ok: true, message: 'valid' })
234
+ } catch (error) {
235
+ checks.push({
236
+ name: 'zerct.toml',
237
+ ok: false,
238
+ message: error.message,
239
+ agent_instruction: 'Fix zerct.toml so it matches the Zerct deploy contract.'
240
+ })
241
+ }
242
+ }
243
+
244
+ const unsafeHits = scanUnsafe(projectDir)
245
+ checks.push({
246
+ name: 'unsafe',
247
+ ok: unsafeHits.length === 0,
248
+ message: unsafeHits.length === 0 ? 'no direct unsafe found' : unsafeHits.slice(0, 5).join(', '),
249
+ agent_instruction: 'Remove direct unsafe usage from workspace Rust source before deploying.'
250
+ })
251
+
252
+ return {
253
+ ok: checks.every((check) => check.ok),
254
+ project: projectDir,
255
+ config,
256
+ checks
257
+ }
258
+ }
259
+
260
+ async function login(cli) {
261
+ if (cli.token) {
262
+ writeSessionToken(process.cwd(), cli.token)
263
+ console.log('saved Zerct session token to .zerct/session-token')
264
+ return
265
+ }
266
+
267
+ const response = await apiRequest(cli, 'POST', '/v1/login/device', null, null)
268
+ openUrl(response.login_url)
269
+ console.log(`opened ${response.login_url}`)
270
+ console.log('After login, retry your deploy. If the CLI cannot finish automatically yet, set ZERCT_TOKEN or run `npx zerct login --token <token>`.')
271
+ }
272
+
273
+ async function deploy(projectDir, cli) {
274
+ const report = runDoctor(projectDir)
275
+ if (!report.ok) {
276
+ const firstFailure = report.checks.find((check) => !check.ok)
277
+ throw agentError('doctor_failed', 'Zerct doctor failed.', firstFailure?.agent_instruction || 'Fix the failed checks and retry.', cli.json)
278
+ }
279
+
280
+ const token = readToken(projectDir, cli)
281
+ const archive = createArchiveBase64(projectDir)
282
+ const commitSha = gitCommitSha(projectDir)
283
+ const body = {
284
+ config: report.config,
285
+ commit_sha: commitSha,
286
+ wants_database: cli.database,
287
+ source_archive_base64: archive
288
+ }
289
+
290
+ const response = await apiRequest(cli, 'POST', '/v1/deployments', token, body)
291
+ if (cli.json) {
292
+ console.log(JSON.stringify(response, null, 2))
293
+ return
294
+ }
295
+
296
+ console.log(`queued ${response.build_job.id}`)
297
+ console.log(`app ${response.app.id}`)
298
+ console.log(`url ${response.app.url}`)
299
+ console.log(`next npx zerct logs --app ${response.app.id}`)
300
+ }
301
+
302
+ async function logs(cli) {
303
+ const response = await appGet(cli, 'logs')
304
+ if (cli.json) {
305
+ console.log(JSON.stringify(response, null, 2))
306
+ return
307
+ }
308
+ for (const line of response.lines || []) {
309
+ console.log(`[${line.timestamp}] ${line.stream}: ${line.message}`)
310
+ }
311
+ }
312
+
313
+ async function status(cli) {
314
+ const response = await appGet(cli, 'status')
315
+ printJsonOrPretty(cli, response)
316
+ }
317
+
318
+ async function inspect(cli) {
319
+ const response = await appGet(cli, 'inspect')
320
+ printJsonOrPretty(cli, response)
321
+ }
322
+
323
+ async function database(cli) {
324
+ const response = await appGet(cli, 'database')
325
+ printJsonOrPretty(cli, response)
326
+ }
327
+
328
+ async function envCommand(cli) {
329
+ if (cli.args[0] !== 'set') {
330
+ throw agentError('unknown_command', 'Unknown env command.', 'Use `npx zerct env set --app <app_id> KEY=value`.', cli.json)
331
+ }
332
+
333
+ const assignment = cli.args[1] || ''
334
+ const separator = assignment.indexOf('=')
335
+ if (separator <= 0) {
336
+ throw agentError('invalid_env', 'Environment assignment must be KEY=value.', 'Pass one uppercase shell-safe environment assignment, for example `API_KEY=value`.', cli.json)
337
+ }
338
+
339
+ const name = assignment.slice(0, separator)
340
+ const value = assignment.slice(separator + 1)
341
+ const token = readToken(process.cwd(), cli)
342
+ const app = requireApp(cli)
343
+ const response = await apiRequest(cli, 'PUT', `/v1/apps/${encodeURIComponent(app)}/env`, token, { name, value })
344
+ printJsonOrPretty(cli, response)
345
+ }
346
+
347
+ async function billing(cli) {
348
+ const token = readToken(process.cwd(), cli)
349
+ const response = await apiRequest(cli, 'POST', '/v1/billing/checkout', token, {
350
+ target_plan: 'pro',
351
+ reason: 'Upgrade to Zerct Pro.'
352
+ })
353
+ if (cli.json) {
354
+ console.log(JSON.stringify(response, null, 2))
355
+ return
356
+ }
357
+ console.log(response.checkout.url)
358
+ openUrl(response.checkout.url)
359
+ }
360
+
361
+ async function appGet(cli, kind) {
362
+ const token = readToken(process.cwd(), cli)
363
+ const app = requireApp(cli)
364
+ return apiRequest(cli, 'GET', `/v1/apps/${encodeURIComponent(app)}/${kind}`, token, null)
365
+ }
366
+
367
+ function requireApp(cli) {
368
+ if (!cli.app) {
369
+ throw agentError('missing_app', 'App id is required.', 'Pass `--app <app_id>`. Use the app id printed by `npx zerct deploy`.', cli.json)
370
+ }
371
+ return cli.app
372
+ }
373
+
374
+ async function apiRequest(cli, method, route, token, body) {
375
+ const headers = {
376
+ accept: 'application/json'
377
+ }
378
+ if (token) {
379
+ headers.authorization = `Bearer ${token}`
380
+ }
381
+ if (body !== null) {
382
+ headers['content-type'] = 'application/json'
383
+ }
384
+
385
+ const response = await fetch(`${cli.apiUrl}${route}`, {
386
+ method,
387
+ headers,
388
+ body: body === null ? undefined : JSON.stringify(body)
389
+ })
390
+
391
+ const text = await response.text()
392
+ const data = parseJson(text)
393
+ if (!response.ok) {
394
+ const payload = data && typeof data === 'object' ? data : {
395
+ code: 'api_error',
396
+ message: `Zerct API returned HTTP ${response.status}.`,
397
+ agent_instruction: 'Retry the command. If it keeps failing, check Zerct status before changing your project.',
398
+ docs_url: null,
399
+ checkout_url: null
400
+ }
401
+ throw new ZerctError(payload, cli.json, response.status >= 500 ? 2 : 1)
402
+ }
403
+
404
+ return data
405
+ }
406
+
407
+ function parseJson(text) {
408
+ if (!text.trim()) {
409
+ return null
410
+ }
411
+ try {
412
+ return JSON.parse(text)
413
+ } catch (_error) {
414
+ return null
415
+ }
416
+ }
417
+
418
+ function createArchiveBase64(projectDir) {
419
+ const tar = spawnSync('tar', [
420
+ '--exclude=.git',
421
+ '--exclude=target',
422
+ '--exclude=node_modules',
423
+ '--exclude=.zerct',
424
+ '--exclude=.env',
425
+ '--exclude=.env.*',
426
+ '-czf',
427
+ '-',
428
+ '-C',
429
+ projectDir,
430
+ '.'
431
+ ], {
432
+ encoding: 'buffer',
433
+ maxBuffer: ARCHIVE_LIMIT_BYTES + 1024 * 1024
434
+ })
435
+
436
+ if (tar.error) {
437
+ throw agentError('archive_failed', 'Could not create source archive.', 'Install `tar`, remove local build outputs, then retry `npx zerct deploy`.', false)
438
+ }
439
+ if (tar.status !== 0) {
440
+ throw agentError('archive_failed', 'Could not create source archive.', String(tar.stderr || 'Check project files and retry.'), false)
441
+ }
442
+ if (tar.stdout.length > ARCHIVE_LIMIT_BYTES) {
443
+ throw agentError('archive_too_large', 'Source archive is too large.', 'Remove build outputs, target directories, logs, and local caches before deploying.', false)
444
+ }
445
+
446
+ return tar.stdout.toString('base64')
447
+ }
448
+
449
+ function gitCommitSha(projectDir) {
450
+ const git = spawnSync('git', ['rev-parse', 'HEAD'], {
451
+ cwd: projectDir,
452
+ encoding: 'utf8',
453
+ stdio: ['ignore', 'pipe', 'ignore']
454
+ })
455
+ return git.status === 0 ? git.stdout.trim() || null : null
456
+ }
457
+
458
+ function readToken(projectDir, cli) {
459
+ if (cli.token) {
460
+ return cli.token
461
+ }
462
+ if (process.env.ZERCT_TOKEN) {
463
+ return process.env.ZERCT_TOKEN
464
+ }
465
+
466
+ const projectToken = path.join(projectDir, SESSION_DIR, SESSION_FILE)
467
+ if (existsSync(projectToken)) {
468
+ return readFileSync(projectToken, 'utf8').trim()
469
+ }
470
+
471
+ const homeToken = path.join(homedir(), SESSION_DIR, SESSION_FILE)
472
+ if (existsSync(homeToken)) {
473
+ return readFileSync(homeToken, 'utf8').trim()
474
+ }
475
+
476
+ throw agentError('login_required', 'Zerct login is required.', 'Run `npx zerct login`, set `ZERCT_TOKEN`, or run `npx zerct login --token <token>`, then retry.', cli.json)
477
+ }
478
+
479
+ function writeSessionToken(projectDir, token) {
480
+ const dir = path.join(projectDir, SESSION_DIR)
481
+ mkdirSync(dir, { recursive: true, mode: 0o700 })
482
+ writeFileSync(path.join(dir, SESSION_FILE), `${token.trim()}\n`, { mode: 0o600 })
483
+ }
484
+
485
+ function parseZerctToml(source) {
486
+ const config = {
487
+ build: {},
488
+ run: {},
489
+ resources: {}
490
+ }
491
+ let section = config
492
+
493
+ for (const rawLine of source.split(/\r?\n/u)) {
494
+ const line = rawLine.trim()
495
+ if (!line || line.startsWith('#')) {
496
+ continue
497
+ }
498
+
499
+ const sectionMatch = line.match(/^\[([a-z_]+)\]$/u)
500
+ if (sectionMatch) {
501
+ const name = sectionMatch[1]
502
+ if (!['build', 'run', 'resources'].includes(name)) {
503
+ throw new Error(`unsupported section [${name}]`)
504
+ }
505
+ section = config[name]
506
+ continue
507
+ }
508
+
509
+ const assignment = line.match(/^([a-z_]+)\s*=\s*(.+)$/u)
510
+ if (!assignment) {
511
+ throw new Error(`invalid line: ${line}`)
512
+ }
513
+
514
+ section[assignment[1]] = parseTomlValue(assignment[2])
515
+ }
516
+
517
+ config.build.command ||= 'cargo build --release'
518
+ config.run.port ||= 3000
519
+ config.run.health ||= '/healthz'
520
+ config.resources.memory ||= '512mb'
521
+ config.resources.cpu ||= '0.25'
522
+ config.resources.idle_timeout_minutes ||= 15
523
+ return config
524
+ }
525
+
526
+ function parseTomlValue(raw) {
527
+ const value = raw.trim()
528
+ if (value.startsWith('"') && value.endsWith('"')) {
529
+ return value.slice(1, -1).replace(/\\"/gu, '"')
530
+ }
531
+ if (value === 'true') {
532
+ return true
533
+ }
534
+ if (value === 'false') {
535
+ return false
536
+ }
537
+ if (/^\d+$/u.test(value)) {
538
+ return Number(value)
539
+ }
540
+ throw new Error(`unsupported TOML value: ${value}`)
541
+ }
542
+
543
+ function validateConfig(config) {
544
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,46}[a-z0-9])?$/u.test(config.name || '')) {
545
+ throw new Error('name must be lowercase DNS-safe text up to 48 characters')
546
+ }
547
+ if (!config.run.command || typeof config.run.command !== 'string') {
548
+ throw new Error('[run].command is required')
549
+ }
550
+ if (!Number.isInteger(config.run.port) || config.run.port < 1 || config.run.port > 65535) {
551
+ throw new Error('[run].port must be between 1 and 65535')
552
+ }
553
+ if (typeof config.run.health !== 'string' || !config.run.health.startsWith('/')) {
554
+ throw new Error('[run].health must be an absolute path')
555
+ }
556
+ if (!/^\d+\s*(mb|mib|gb|gib)$/iu.test(config.resources.memory)) {
557
+ throw new Error('[resources].memory must look like 512mb or 1gb')
558
+ }
559
+ if (!/^\d+(?:\.\d{1,3})?$/u.test(config.resources.cpu)) {
560
+ throw new Error('[resources].cpu must look like 0.25, 0.5, 1, or 2')
561
+ }
562
+ }
563
+
564
+ function scanUnsafe(projectDir) {
565
+ const hits = []
566
+ walk(projectDir, (file) => {
567
+ if (!file.endsWith('.rs')) {
568
+ return
569
+ }
570
+ const source = readFileSync(file, 'utf8')
571
+ if (/\bunsafe\b/u.test(source)) {
572
+ hits.push(path.relative(projectDir, file))
573
+ }
574
+ })
575
+ return hits
576
+ }
577
+
578
+ function walk(dir, visit) {
579
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
580
+ if (['.git', 'target', 'node_modules', '.zerct'].includes(entry.name)) {
581
+ continue
582
+ }
583
+ const fullPath = path.join(dir, entry.name)
584
+ if (entry.isDirectory()) {
585
+ walk(fullPath, visit)
586
+ } else if (entry.isFile()) {
587
+ visit(fullPath)
588
+ }
589
+ }
590
+ }
591
+
592
+ function ensureDirectory(dir) {
593
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
594
+ throw agentError('missing_project', 'Project directory does not exist.', 'Run Zerct from the root of a Rust project or pass the project path.', false)
595
+ }
596
+ }
597
+
598
+ function serviceNameFromDir(projectDir) {
599
+ const name = path.basename(projectDir).toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '')
600
+ return name || 'api'
601
+ }
602
+
603
+ function printJsonOrPretty(cli, value) {
604
+ console.log(JSON.stringify(value, null, cli.json ? 2 : 2))
605
+ }
606
+
607
+ function openUrl(url) {
608
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
609
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
610
+ spawnSync(command, args, { stdio: 'ignore', detached: true })
611
+ }
612
+
613
+ function trimTrailingSlash(value) {
614
+ return value.replace(/\/+$/u, '')
615
+ }
616
+
617
+ function agentError(code, message, agentInstruction, json) {
618
+ return new ZerctError({
619
+ code,
620
+ message,
621
+ agent_instruction: agentInstruction,
622
+ docs_url: null,
623
+ checkout_url: null
624
+ }, json, 1)
625
+ }
626
+
627
+ function printAgentError(payload, json) {
628
+ if (json) {
629
+ console.error(JSON.stringify(payload, null, 2))
630
+ return
631
+ }
632
+
633
+ console.error(payload.message || 'Zerct command failed.')
634
+ if (payload.agent_instruction) {
635
+ console.error(`agent_instruction: ${payload.agent_instruction}`)
636
+ }
637
+ if (payload.docs_url) {
638
+ console.error(`docs: ${payload.docs_url}`)
639
+ }
640
+ if (payload.checkout_url) {
641
+ console.error(`checkout: ${payload.checkout_url}`)
642
+ }
643
+ }
644
+
645
+ class ZerctError extends Error {
646
+ constructor(payload, json, exitCode) {
647
+ super(payload.message || 'Zerct command failed.')
648
+ this.payload = payload
649
+ this.json = json
650
+ this.exitCode = exitCode
651
+ }
652
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@zerct/zerct",
3
+ "version": "0.1.0",
4
+ "description": "Deploy Rust backends to Zerct.",
5
+ "type": "module",
6
+ "bin": {
7
+ "zerct": "bin/zerct.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.17"
15
+ },
16
+ "keywords": [
17
+ "zerct",
18
+ "rust",
19
+ "deploy",
20
+ "backend",
21
+ "hosting"
22
+ ],
23
+ "homepage": "https://zerct.com",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/Zerct/zerct.git",
27
+ "directory": "packages/zerct"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/Zerct/zerct/issues"
31
+ },
32
+ "license": "MIT",
33
+ "scripts": {
34
+ "check": "node --check bin/zerct.js",
35
+ "pack:dry": "npm pack --dry-run"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }