@workerstack/system 0.0.0-stage → 0.2.3

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/package.json CHANGED
@@ -1,6 +1,47 @@
1
1
  {
2
- "name": "@workerstack/system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@workerstack/system",
3
+ "version": "0.2.3",
4
+ "description": "Every OS dependency workerstack needs, installed by one command. Define deps in system.config.ts, run system install.",
5
+ "keywords": [
6
+ "cli",
7
+ "cross-platform",
8
+ "dependencies",
9
+ "installer",
10
+ "system"
11
+ ],
12
+ "homepage": "https://github.com/workerstack/system#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/workerstack/system/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "workerstack",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/workerstack/system.git"
21
+ },
22
+ "bin": {
23
+ "system": "./src/cli.ts"
24
+ },
25
+ "files": [
26
+ "src"
27
+ ],
28
+ "type": "module",
29
+ "sideEffects": false,
30
+ "exports": {
31
+ ".": "./src/index.ts"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "type:check": "tsc --noEmit -p tsconfig.src.json",
40
+ "type:check:tests": "tsc --noEmit"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "catalog:",
44
+ "typescript": "catalog:",
45
+ "vitest": "catalog:"
46
+ }
47
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,593 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * @workerstack/system CLI — cross-platform system dependency manager.
4
+ *
5
+ * Usage:
6
+ * system install Install all deps (auto-detect OS, respects depends order)
7
+ * system install <name> Install single dep
8
+ * system check Check all deps
9
+ * system check <name> Check single dep
10
+ * system env Setup environment variables
11
+ * system sync-pm Sync packageManager field
12
+ */
13
+
14
+ import { execSync } from "node:child_process"
15
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
16
+ import { homedir } from "node:os"
17
+ import { dirname, resolve } from "node:path"
18
+ import type { SystemConfig, EnvVar, OsTarget, SystemDepConfig } from "./config.ts"
19
+ import { resolveCheckCommand, resolveOsCommand, versionsMatch } from "./config.ts"
20
+ import {
21
+ applyEnvBlock,
22
+ ensureParentDir,
23
+ hasEnvBlock,
24
+ expandPath,
25
+ reloadHint,
26
+ renderEnvBlock,
27
+ shellConfigPath,
28
+ shellKind,
29
+ } from "./env.ts"
30
+ import { sortByDependencies } from "./graph.ts"
31
+ import { runShellCommand } from "./exec.ts"
32
+ import { TTY_PATH_FALLBACK_WARN, pathEnv, processPath, snapshotTtyPath } from "./path.ts"
33
+ import { commandExists, detectOs, parseOsTarget, whichBinary } from "./platform.ts"
34
+
35
+ const CONFIG_NAMES = ["system.config.ts", "system.config.js", "system.config.mjs"]
36
+
37
+ const USAGE = `Usage: system <command> [args]
38
+
39
+ Commands:
40
+ install Install all deps (auto-detect OS)
41
+ install <name> Install single dep
42
+ check Check all deps
43
+ check <name> Check single dep
44
+ env Setup environment variables
45
+ sync-pm Sync packageManager field in package.json
46
+
47
+ Flags:
48
+ --config <path> Config file (default: system.config.ts in cwd)
49
+ --os <target> Force OS target (or set WORKERSTACK_SYSTEM_OS)
50
+ --dry-run Print install commands without running them
51
+ --upgrade Re-run install when the detected version does not match
52
+ --here check: use this process PATH instead of an interactive-shell snapshot
53
+ `
54
+
55
+ /**
56
+ * Resolve config file path. Checks --config flag, then convention names in cwd.
57
+ */
58
+ function resolveConfigPath(args: string[]): string {
59
+ const configIdx = args.indexOf("--config")
60
+ if (configIdx !== -1) {
61
+ const configPath = args[configIdx + 1]
62
+ if (!configPath) {
63
+ console.error("--config requires a path argument")
64
+ process.exit(1)
65
+ }
66
+ const resolved = resolve(process.cwd(), configPath)
67
+ if (!existsSync(resolved)) {
68
+ console.error(`Config file not found: ${resolved}`)
69
+ process.exit(1)
70
+ }
71
+ return resolved
72
+ }
73
+
74
+ for (const name of CONFIG_NAMES) {
75
+ const candidate = resolve(process.cwd(), name)
76
+ if (existsSync(candidate)) return candidate
77
+ }
78
+
79
+ console.error(`No system config found. Create one of: ${CONFIG_NAMES.join(", ")}`)
80
+ process.exit(1)
81
+ }
82
+
83
+ /**
84
+ * Strip --config <path> from args so command parsing is clean
85
+ */
86
+ function stripFlag(args: string[], flag: string): { args: string[]; value: string | undefined } {
87
+ const idx = args.indexOf(flag)
88
+ if (idx === -1) return { args, value: undefined }
89
+ return {
90
+ args: [...args.slice(0, idx), ...args.slice(idx + 2)],
91
+ value: args[idx + 1],
92
+ }
93
+ }
94
+
95
+ function stripBoolFlag(args: string[], flag: string): { args: string[]; present: boolean } {
96
+ const idx = args.indexOf(flag)
97
+ if (idx === -1) return { args, present: false }
98
+ return { args: [...args.slice(0, idx), ...args.slice(idx + 1)], present: true }
99
+ }
100
+
101
+ async function loadConfig(configPath: string): Promise<SystemConfig> {
102
+ const mod = (await import(configPath)) as Record<string, unknown>
103
+
104
+ /* support: export default defineConfig(...) */
105
+ if (mod.default && typeof mod.default === "object" && "deps" in (mod.default as Record<string, unknown>)) {
106
+ return mod.default as SystemConfig
107
+ }
108
+
109
+ /* support: export const config = defineConfig(...) */
110
+ for (const value of Object.values(mod)) {
111
+ if (value && typeof value === "object" && "deps" in (value as Record<string, unknown>)) {
112
+ return value as SystemConfig
113
+ }
114
+ }
115
+
116
+ console.error("Config file must export an object with a `deps` field (use defineConfig)")
117
+ process.exit(1)
118
+ }
119
+
120
+ function execCommand(command: string, ignoreError = false): boolean {
121
+ try {
122
+ runShellCommand(command)
123
+ return true
124
+ } catch {
125
+ if (!ignoreError) {
126
+ console.error(`\nCommand failed: ${command}`)
127
+ }
128
+ return false
129
+ }
130
+ }
131
+
132
+ function checkBinaryToken(name: string, config: SystemDepConfig): string | undefined {
133
+ return resolveCheckCommand(name, config).split(/\s+/)[0]
134
+ }
135
+
136
+ function binaryPathNote(name: string, config: SystemDepConfig, searchPath?: string): string {
137
+ const token = checkBinaryToken(name, config)
138
+ const path = token ? whichBinary(token, process.platform, searchPath) : null
139
+ return path ? ` ${path}` : ""
140
+ }
141
+
142
+ function getInstalledVersion(name: string, config: SystemDepConfig, searchPath?: string): string | null {
143
+ const checkCmd = resolveCheckCommand(name, config)
144
+ const binary = checkBinaryToken(name, config)
145
+ if (binary && !commandExists(binary, searchPath)) return null
146
+
147
+ try {
148
+ const execOpts: {
149
+ encoding: "utf-8"
150
+ env?: NodeJS.ProcessEnv
151
+ stdio: ["pipe", "pipe", "pipe"]
152
+ } = {
153
+ encoding: "utf-8",
154
+ stdio: ["pipe", "pipe", "pipe"],
155
+ }
156
+ if (searchPath !== undefined) execOpts.env = pathEnv(searchPath)
157
+ const output = execSync(checkCmd, execOpts).trim()
158
+ const match = output.match(/(\d+\.\d+[\w.-]*)/)
159
+ return match?.[1] ?? null
160
+ } catch {
161
+ return null
162
+ }
163
+ }
164
+
165
+ function resolveEnvValue(env: EnvVar): string | null {
166
+ if (env.detect && env.detect.length > 0) {
167
+ for (const path of env.detect) {
168
+ const expanded = expandPath(path, homedir())
169
+ if (existsSync(expanded)) {
170
+ console.log(` Detected ${env.key} at: ${expanded}`)
171
+ return path
172
+ }
173
+ }
174
+ if (env.fallback) {
175
+ console.log(` Using fallback for ${env.key}: ${env.fallback}`)
176
+ return env.fallback
177
+ }
178
+ console.log(` Could not detect ${env.key}, no fallback specified`)
179
+ return null
180
+ }
181
+
182
+ return env.value || null
183
+ }
184
+
185
+ function configureEnvVars(tool: string, envVars: EnvVar[]): boolean {
186
+ if (envVars.length === 0) return true
187
+
188
+ const kind = shellKind(process.platform)
189
+ const configPath = shellConfigPath(homedir(), kind, process.env.SHELL || "")
190
+ const configName = configPath.split(/[\\/]/).pop()
191
+
192
+ console.log(`\n Configuring environment variables in ${configName}...`)
193
+
194
+ try {
195
+ const existingContent = existsSync(configPath) ? readFileSync(configPath, "utf-8") : ""
196
+
197
+ const resolvedEnvs: Array<{ appendToPath?: boolean; key: string; value: string }> = []
198
+ for (const env of envVars) {
199
+ const value = resolveEnvValue(env)
200
+ if (value) {
201
+ resolvedEnvs.push({ appendToPath: env.appendToPath, key: env.key, value })
202
+ }
203
+ }
204
+
205
+ if (resolvedEnvs.length === 0) {
206
+ console.log(" No environment variables to configure")
207
+ return false
208
+ }
209
+
210
+ if (hasEnvBlock(existingContent, tool)) {
211
+ console.log(` Updating existing ${tool} environment config...`)
212
+ }
213
+
214
+ const next = applyEnvBlock(existingContent, renderEnvBlock(tool, resolvedEnvs, kind), tool)
215
+ ensureParentDir(configPath)
216
+ writeFileSync(configPath, next)
217
+ console.log(` Added to ${configName}:`)
218
+ for (const env of resolvedEnvs) {
219
+ if (env.appendToPath) {
220
+ console.log(` PATH+=:${env.value}`)
221
+ } else {
222
+ console.log(` ${env.key}=${env.value}`)
223
+ }
224
+ }
225
+ console.log(`\n ${reloadHint(kind, configName ?? "")}`)
226
+
227
+ return true
228
+ } catch (error) {
229
+ console.error(" Failed to configure env vars:", error)
230
+ return false
231
+ }
232
+ }
233
+
234
+ function installOne(
235
+ name: string,
236
+ config: SystemDepConfig,
237
+ os: OsTarget,
238
+ dryRun = false,
239
+ upgrade = false,
240
+ ): "installed" | "skipped" | "failed" | "unavailable" {
241
+ const command = resolveOsCommand(name, config, os)
242
+
243
+ if (!command) {
244
+ console.log(`${name}@${config.version} — not available on ${os}`)
245
+ return "unavailable"
246
+ }
247
+
248
+ console.log(`${name}@${config.version}`)
249
+ console.log(` ${config.description}`)
250
+
251
+ if (dryRun) {
252
+ console.log(` dry-run: ${command}`)
253
+ return "installed"
254
+ }
255
+
256
+ const installed = getInstalledVersion(name, config)
257
+ if (installed && versionsMatch(installed, config.version)) {
258
+ console.log(` Already installed (${installed}), skipping`)
259
+ return "skipped"
260
+ }
261
+ if (installed && !upgrade) {
262
+ console.log(
263
+ ` Already installed (${installed}), skipping (expected ${config.version})${binaryPathNote(name, config)}`,
264
+ )
265
+ console.log(` run: system install ${name} --upgrade`)
266
+ return "skipped"
267
+ }
268
+
269
+ const success = execCommand(command, true)
270
+ if (success) {
271
+ console.log(" Installed successfully")
272
+ if (config.env && config.env.length > 0) {
273
+ configureEnvVars(name, config.env)
274
+ }
275
+ /* Installer often does not win PATH — brew/nvm/fnm still first. */
276
+ const after = getInstalledVersion(name, config)
277
+ if (after && !versionsMatch(after, config.version)) {
278
+ console.log(` PATH still has ${after} (expected ${config.version})${binaryPathNote(name, config)}`)
279
+ console.log(" Unpin/unlink that binary or put the pinned one first on PATH")
280
+ }
281
+ return "installed"
282
+ }
283
+
284
+ console.error(" Installation failed")
285
+ return "failed"
286
+ }
287
+
288
+ function cmdInstall(
289
+ deps: Record<string, SystemDepConfig>,
290
+ target: string | undefined,
291
+ dryRun = false,
292
+ upgrade = false,
293
+ ): void {
294
+ const os = detectOs()
295
+
296
+ if (target) {
297
+ const config = deps[target]
298
+ if (!config) {
299
+ console.error(`Unknown dependency: ${target}`)
300
+ console.log(`Available: ${Object.keys(deps).join(", ")}`)
301
+ process.exit(1)
302
+ }
303
+ console.log(`Detected OS: ${os}`)
304
+ const result = installOne(target, config, os, dryRun, upgrade)
305
+ if (result === "failed") process.exit(1)
306
+ return
307
+ }
308
+
309
+ console.log("@workerstack/system — install")
310
+ console.log(`Detected OS: ${os}`)
311
+ console.log("=".repeat(50))
312
+
313
+ const entries = sortByDependencies(Object.entries(deps))
314
+ console.log(`\nFound ${entries.length} dependencies to install:\n`)
315
+ for (const [name, config] of entries) {
316
+ const badge = config.required ? "[required]" : "[optional]"
317
+ const available = resolveOsCommand(name, config, os) !== null
318
+ console.log(` ${badge} ${name}@${config.version}${available ? "" : ` (not available on ${os})`}`)
319
+ }
320
+
321
+ console.log(`\n${"=".repeat(50)}`)
322
+ console.log(dryRun ? "Dry-run — commands only, nothing installed" : "Installing dependencies...")
323
+ console.log("=".repeat(50))
324
+
325
+ let installed = 0
326
+ let skipped = 0
327
+ let failed = 0
328
+ let unavailable = 0
329
+
330
+ for (const [name, config] of entries) {
331
+ const result = installOne(name, config, os, dryRun, upgrade)
332
+
333
+ if (result === "installed") installed++
334
+ else if (result === "skipped") skipped++
335
+ else if (result === "unavailable") unavailable++
336
+ else if (config.required) failed++
337
+ else skipped++
338
+ }
339
+
340
+ console.log(`\n${"=".repeat(50)}`)
341
+ console.log("Installation Summary")
342
+ console.log("=".repeat(50))
343
+ console.log(`Installed: ${installed}`)
344
+ console.log(`Skipped: ${skipped}`)
345
+ console.log(`Unavailable on ${os}: ${unavailable}`)
346
+ console.log(`Failed: ${failed}`)
347
+
348
+ if (failed > 0) {
349
+ console.log("\nSome required dependencies failed to install")
350
+ process.exit(1)
351
+ }
352
+
353
+ console.log("\nAll required system dependencies are ready!")
354
+ }
355
+
356
+ function resolveCheckSearchPath(here: boolean): string {
357
+ if (here) return processPath()
358
+ const snapshot = snapshotTtyPath()
359
+ if (snapshot.source === "process") {
360
+ console.warn(TTY_PATH_FALLBACK_WARN)
361
+ }
362
+ return snapshot.path
363
+ }
364
+
365
+ function cmdCheck(
366
+ deps: Record<string, SystemDepConfig>,
367
+ configDir: string,
368
+ packageJsonPath: string,
369
+ target: string | undefined,
370
+ here = false,
371
+ ): void {
372
+ const os = detectOs()
373
+ const searchPath = resolveCheckSearchPath(here)
374
+
375
+ if (target) {
376
+ const config = deps[target]
377
+ if (!config) {
378
+ console.error(`Unknown dependency: ${target}`)
379
+ console.log(`Available: ${Object.keys(deps).join(", ")}`)
380
+ process.exit(1)
381
+ }
382
+
383
+ const installed = getInstalledVersion(target, config, searchPath)
384
+ const available = resolveOsCommand(target, config, os) !== null
385
+
386
+ if (!available) {
387
+ console.log(`${target} — not available on ${os}`)
388
+ } else if (!installed) {
389
+ console.log(`${target} — not installed (expected ${config.version})`)
390
+ process.exit(1)
391
+ } else {
392
+ console.log(`${target}@${installed} (expected ${config.version})${binaryPathNote(target, config, searchPath)}`)
393
+ }
394
+ return
395
+ }
396
+
397
+ console.log("@workerstack/system — check")
398
+ console.log(`Detected OS: ${os}`)
399
+ console.log("=".repeat(50))
400
+
401
+ let ok = 0
402
+ let warn = 0
403
+ let missing = 0
404
+
405
+ for (const [name, config] of Object.entries(deps)) {
406
+ const installed = getInstalledVersion(name, config, searchPath)
407
+ const badge = config.required ? "[required]" : "[optional]"
408
+ const available = resolveOsCommand(name, config, os) !== null
409
+
410
+ if (!available) {
411
+ console.log(` - ${badge} ${name} — not available on ${os}`)
412
+ continue
413
+ }
414
+
415
+ if (!installed) {
416
+ console.log(` x ${badge} ${name} — not installed (expected ${config.version})`)
417
+ if (config.required) missing++
418
+ else warn++
419
+ } else if (versionsMatch(installed, config.version)) {
420
+ console.log(` v ${badge} ${name}@${installed}`)
421
+ ok++
422
+ } else {
423
+ console.log(
424
+ ` ~ ${badge} ${name}@${installed} (expected ${config.version})${binaryPathNote(name, config, searchPath)}`,
425
+ )
426
+ warn++
427
+ }
428
+ }
429
+
430
+ /* packageManager sync check */
431
+ const bunConfig = deps.bun
432
+ if (bunConfig) {
433
+ const pkgJsonPath = resolve(configDir, packageJsonPath)
434
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, "utf-8")) as Record<string, unknown>
435
+ const current = pkg.packageManager as string | undefined
436
+
437
+ if (bunConfig.version === "latest") {
438
+ if (current?.startsWith("bun@")) {
439
+ console.log(`\n v packageManager: ${current} (latest mode)`)
440
+ } else {
441
+ console.log(`\n ~ packageManager: ${current ?? "(missing)"} (expected bun@*)`)
442
+ warn++
443
+ }
444
+ } else {
445
+ const expected = `bun@${bunConfig.version}`
446
+ if (current === expected) {
447
+ console.log(`\n v packageManager: ${current}`)
448
+ } else {
449
+ console.log(`\n ~ packageManager: ${current ?? "(missing)"} (expected ${expected})`)
450
+ warn++
451
+ }
452
+ }
453
+ }
454
+
455
+ console.log(`\n${"=".repeat(50)}`)
456
+ console.log(`OK: ${ok} Mismatch: ${warn} Missing: ${missing}`)
457
+
458
+ if (missing > 0) {
459
+ console.log("\nRequired dependencies missing — run: system install")
460
+ process.exit(1)
461
+ }
462
+
463
+ if (warn > 0) {
464
+ console.log("\nVersion mismatches found — run: system install --upgrade")
465
+ } else {
466
+ console.log("\nAll system dependencies OK")
467
+ }
468
+ }
469
+
470
+ function cmdEnv(deps: Record<string, SystemDepConfig>): void {
471
+ console.log("Setting up environment variables")
472
+ console.log("=".repeat(50))
473
+
474
+ const envDeps = Object.entries(deps).filter(([_, config]) => config.env && config.env.length > 0)
475
+
476
+ if (envDeps.length === 0) {
477
+ console.log("\nNo dependencies with environment variables configured.")
478
+ return
479
+ }
480
+
481
+ for (const [tool, config] of envDeps) {
482
+ if (config.env) {
483
+ console.log(`\n${tool}`)
484
+ configureEnvVars(tool, config.env)
485
+ }
486
+ }
487
+
488
+ const kind = shellKind(process.platform)
489
+ const configPath = shellConfigPath(homedir(), kind, process.env.SHELL || "")
490
+ console.log(`\n${"=".repeat(50)}`)
491
+ console.log(`Environment variables configured in ${configPath}`)
492
+ console.log(`\n${reloadHint(kind, configPath.split(/[\\/]/).pop() ?? "")}`)
493
+ }
494
+
495
+ function cmdSyncPm(deps: Record<string, SystemDepConfig>, configDir: string, packageJsonPath: string): boolean {
496
+ const bunConfig = deps.bun
497
+ if (!bunConfig) {
498
+ console.log("\nSkipping packageManager sync — bun not in config")
499
+ return true
500
+ }
501
+
502
+ const bunVersion = bunConfig.version
503
+
504
+ if (bunVersion === "latest") {
505
+ console.log('\nSkipping packageManager sync — bun version is "latest"')
506
+ return true
507
+ }
508
+
509
+ const pkgJsonPath = resolve(configDir, packageJsonPath)
510
+ const expected = `bun@${bunVersion}`
511
+
512
+ try {
513
+ const content = readFileSync(pkgJsonPath, "utf-8")
514
+ const pkg = JSON.parse(content) as Record<string, unknown>
515
+ const current = pkg.packageManager as string | undefined
516
+
517
+ if (current === expected) {
518
+ console.log(`\npackageManager already correct: ${expected}`)
519
+ return true
520
+ }
521
+
522
+ console.log(`\nSyncing packageManager: ${current ?? "(missing)"} -> ${expected}`)
523
+ const pkgName = pkg.name as string
524
+ const updated = current
525
+ ? content.replace(`"packageManager": "${current}"`, `"packageManager": "${expected}"`)
526
+ : content.replace(`"name": "${pkgName}"`, `"name": "${pkgName}",\n\t"packageManager": "${expected}"`)
527
+ writeFileSync(pkgJsonPath, updated)
528
+ console.log(`Updated packageManager to ${expected}`)
529
+ return true
530
+ } catch (error) {
531
+ console.error("Failed to sync packageManager:", error)
532
+ return false
533
+ }
534
+ }
535
+
536
+ async function main(): Promise<void> {
537
+ const rawArgs = process.argv.slice(2)
538
+ const osFlag = stripFlag(rawArgs, "--os")
539
+ if (rawArgs.includes("--os") && !osFlag.value) {
540
+ console.error("--os requires a target argument")
541
+ process.exit(1)
542
+ }
543
+ if (osFlag.value) {
544
+ try {
545
+ process.env.WORKERSTACK_SYSTEM_OS = parseOsTarget(osFlag.value)
546
+ } catch (error) {
547
+ console.error(error instanceof Error ? error.message : error)
548
+ process.exit(1)
549
+ }
550
+ }
551
+
552
+ const configPath = resolveConfigPath(osFlag.args)
553
+ const configDir = dirname(configPath)
554
+ const config = await loadConfig(configPath)
555
+ const deps = config.deps
556
+ const packageJsonPath = config.packageJsonPath ?? "package.json"
557
+
558
+ const configStripped = stripFlag(osFlag.args, "--config")
559
+ const dry = stripBoolFlag(configStripped.args, "--dry-run")
560
+ const upgrade = stripBoolFlag(dry.args, "--upgrade")
561
+ const here = stripBoolFlag(upgrade.args, "--here")
562
+ const args = here.args
563
+ const command = args[0]
564
+ const target = args[1]
565
+
566
+ switch (command) {
567
+ case "install":
568
+ cmdInstall(deps, target, dry.present, upgrade.present)
569
+ break
570
+ case "check":
571
+ cmdCheck(deps, configDir, packageJsonPath, target, here.present)
572
+ break
573
+ case "env":
574
+ cmdEnv(deps)
575
+ break
576
+ case "sync-pm":
577
+ cmdSyncPm(deps, configDir, packageJsonPath)
578
+ break
579
+ default:
580
+ console.log(USAGE)
581
+ if (command) {
582
+ console.error(`Unknown command: ${command}`)
583
+ process.exit(1)
584
+ }
585
+ }
586
+ }
587
+
588
+ try {
589
+ await main()
590
+ } catch (error) {
591
+ console.error("\nFatal error:", error)
592
+ process.exit(1)
593
+ }