@namzu/sandbox 24.0.0 → 26.0.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 (158) hide show
  1. package/CHANGELOG.md +284 -0
  2. package/dist/backends/http-worker-client.d.ts +3 -1
  3. package/dist/backends/http-worker-client.d.ts.map +1 -1
  4. package/dist/backends/http-worker-client.js +15 -4
  5. package/dist/backends/http-worker-client.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/local-virtual-computer/client.d.ts +21 -0
  11. package/dist/local-virtual-computer/client.d.ts.map +1 -0
  12. package/dist/local-virtual-computer/client.js +503 -0
  13. package/dist/local-virtual-computer/client.js.map +1 -0
  14. package/dist/local-virtual-computer/detached-process.d.ts +15 -0
  15. package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
  16. package/dist/local-virtual-computer/detached-process.js +111 -0
  17. package/dist/local-virtual-computer/detached-process.js.map +1 -0
  18. package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
  19. package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
  20. package/dist/local-virtual-computer/detached-worker.js +97 -0
  21. package/dist/local-virtual-computer/detached-worker.js.map +1 -0
  22. package/dist/local-virtual-computer/dialect.d.ts +26 -0
  23. package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
  24. package/dist/local-virtual-computer/dialect.js +61 -0
  25. package/dist/local-virtual-computer/dialect.js.map +1 -0
  26. package/dist/local-virtual-computer/engine.d.ts +63 -0
  27. package/dist/local-virtual-computer/engine.d.ts.map +1 -0
  28. package/dist/local-virtual-computer/engine.js +125 -0
  29. package/dist/local-virtual-computer/engine.js.map +1 -0
  30. package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
  31. package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
  32. package/dist/local-virtual-computer/guest-stdio.js +330 -0
  33. package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
  34. package/dist/local-virtual-computer/index.d.ts +36 -0
  35. package/dist/local-virtual-computer/index.d.ts.map +1 -0
  36. package/dist/local-virtual-computer/index.js +341 -0
  37. package/dist/local-virtual-computer/index.js.map +1 -0
  38. package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
  39. package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
  40. package/dist/local-virtual-computer/podman-machine.js +181 -0
  41. package/dist/local-virtual-computer/podman-machine.js.map +1 -0
  42. package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
  43. package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
  44. package/dist/local-virtual-computer/setup/contract.js +3 -0
  45. package/dist/local-virtual-computer/setup/contract.js.map +1 -0
  46. package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
  47. package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
  48. package/dist/local-virtual-computer/setup/elevated.js +548 -0
  49. package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
  50. package/dist/local-virtual-computer/setup/host.d.ts +12 -0
  51. package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
  52. package/dist/local-virtual-computer/setup/host.js +93 -0
  53. package/dist/local-virtual-computer/setup/host.js.map +1 -0
  54. package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
  55. package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
  56. package/dist/local-virtual-computer/setup/idle.js +38 -0
  57. package/dist/local-virtual-computer/setup/idle.js.map +1 -0
  58. package/dist/local-virtual-computer/setup/image.d.ts +7 -0
  59. package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
  60. package/dist/local-virtual-computer/setup/image.js +124 -0
  61. package/dist/local-virtual-computer/setup/image.js.map +1 -0
  62. package/dist/local-virtual-computer/setup/index.d.ts +17 -0
  63. package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
  64. package/dist/local-virtual-computer/setup/index.js +17 -0
  65. package/dist/local-virtual-computer/setup/index.js.map +1 -0
  66. package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
  67. package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
  68. package/dist/local-virtual-computer/setup/limits.js +58 -0
  69. package/dist/local-virtual-computer/setup/limits.js.map +1 -0
  70. package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
  71. package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
  72. package/dist/local-virtual-computer/setup/plan.js +131 -0
  73. package/dist/local-virtual-computer/setup/plan.js.map +1 -0
  74. package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
  75. package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
  76. package/dist/local-virtual-computer/setup/readiness.js +320 -0
  77. package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
  78. package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
  79. package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
  80. package/dist/local-virtual-computer/setup/registry.js +50 -0
  81. package/dist/local-virtual-computer/setup/registry.js.map +1 -0
  82. package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
  83. package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
  84. package/dist/local-virtual-computer/setup/restart.js +60 -0
  85. package/dist/local-virtual-computer/setup/restart.js.map +1 -0
  86. package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
  87. package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
  88. package/dist/local-virtual-computer/setup/resume.js +58 -0
  89. package/dist/local-virtual-computer/setup/resume.js.map +1 -0
  90. package/dist/local-virtual-computer/setup/run.d.ts +8 -0
  91. package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
  92. package/dist/local-virtual-computer/setup/run.js +200 -0
  93. package/dist/local-virtual-computer/setup/run.js.map +1 -0
  94. package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
  95. package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
  96. package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
  97. package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
  98. package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
  99. package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
  100. package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
  101. package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
  102. package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
  103. package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
  104. package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
  105. package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
  106. package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
  107. package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
  108. package/dist/local-virtual-computer/ssh-forward.js +179 -0
  109. package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
  110. package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
  111. package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
  112. package/dist/local-virtual-computer/wslc-engine.js +98 -0
  113. package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
  114. package/local-computer/Dockerfile +99 -0
  115. package/local-computer/app-catalog.cjs +240 -0
  116. package/local-computer/app-native-host.cjs +144 -0
  117. package/local-computer/browser-home.cjs +249 -0
  118. package/local-computer/build-image.mjs +310 -0
  119. package/local-computer/desktop-worker.cjs +477 -0
  120. package/local-computer/entrypoint.sh +161 -0
  121. package/local-computer/home.html +28 -0
  122. package/local-computer/new-tab/home.css +255 -0
  123. package/local-computer/new-tab/home.js +65 -0
  124. package/local-computer/new-tab/manifest.json +12 -0
  125. package/local-computer/new-tab/wordmark.svg +33 -0
  126. package/local-computer/openbox-rc.xml +41 -0
  127. package/local-computer/prepare-home.cjs +121 -0
  128. package/local-computer/tint2rc +44 -0
  129. package/package.json +13 -4
  130. package/src/backends/http-worker-client.ts +31 -3
  131. package/src/index.ts +13 -0
  132. package/src/local-virtual-computer/client.ts +573 -0
  133. package/src/local-virtual-computer/detached-process.ts +133 -0
  134. package/src/local-virtual-computer/detached-worker.ts +124 -0
  135. package/src/local-virtual-computer/dialect.ts +83 -0
  136. package/src/local-virtual-computer/engine.ts +197 -0
  137. package/src/local-virtual-computer/guest-stdio.ts +332 -0
  138. package/src/local-virtual-computer/index.ts +447 -0
  139. package/src/local-virtual-computer/podman-machine.ts +264 -0
  140. package/src/local-virtual-computer/setup/contract.ts +395 -0
  141. package/src/local-virtual-computer/setup/elevated.ts +655 -0
  142. package/src/local-virtual-computer/setup/host.ts +100 -0
  143. package/src/local-virtual-computer/setup/idle.ts +39 -0
  144. package/src/local-virtual-computer/setup/image.ts +159 -0
  145. package/src/local-virtual-computer/setup/index.ts +16 -0
  146. package/src/local-virtual-computer/setup/limits.ts +68 -0
  147. package/src/local-virtual-computer/setup/plan.ts +144 -0
  148. package/src/local-virtual-computer/setup/readiness.ts +386 -0
  149. package/src/local-virtual-computer/setup/registry.ts +50 -0
  150. package/src/local-virtual-computer/setup/restart.ts +64 -0
  151. package/src/local-virtual-computer/setup/resume.ts +65 -0
  152. package/src/local-virtual-computer/setup/run.ts +225 -0
  153. package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
  154. package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
  155. package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
  156. package/src/local-virtual-computer/ssh-forward.ts +195 -0
  157. package/src/local-virtual-computer/wslc-engine.ts +124 -0
  158. package/worker/server.js +1499 -0
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env node
2
+ // Build one stage of the Pal computer image with a container engine, save it,
3
+ // compress it and write a manifest the installer carries beside it.
4
+ //
5
+ // node build-image.mjs --engine docker --stage base --out ./out
6
+ // node build-image.mjs --engine wslc --stage base --tag namzu-local-computer:2 --out ./out --format zst
7
+ //
8
+ // The engine is any binary with `build`, `image inspect` and `save` (docker, podman,
9
+ // wslc). Every external command goes through an injected runner, so the tests drive
10
+ // this module without an engine.
11
+ import { spawn } from 'node:child_process'
12
+ import { createHash } from 'node:crypto'
13
+ import { createReadStream, createWriteStream } from 'node:fs'
14
+ import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
15
+ import { basename, dirname, join, resolve } from 'node:path'
16
+ import { pipeline } from 'node:stream/promises'
17
+ import { fileURLToPath } from 'node:url'
18
+ import zlib from 'node:zlib'
19
+
20
+ export const STAGES = ['base', 'creative']
21
+ export const PROTOCOL_LABEL = 'org.namzu.local-computer.protocol'
22
+ /** `wslc load -i` sniffs the content, so any of these loads without a separate decompression step. */
23
+ export const FORMATS = ['zst', 'xz', 'gz', 'tar']
24
+ const DEFAULT_TAGS = { base: 'namzu-local-computer:2', creative: 'namzu-local-computer:2-creative' }
25
+ const EXTENSIONS = { zst: 'tar.zst', xz: 'tar.xz', gz: 'tar.gz', tar: 'tar' }
26
+ const DEFAULT_LEVELS = { zst: 19, xz: 9, gz: 9, tar: 0 }
27
+
28
+ export class BuildImageError extends Error {
29
+ constructor(message) {
30
+ super(message)
31
+ this.name = 'BuildImageError'
32
+ }
33
+ }
34
+
35
+ const HERE = dirname(fileURLToPath(import.meta.url))
36
+
37
+ export function defaultTag(stage) {
38
+ return DEFAULT_TAGS[stage]
39
+ }
40
+
41
+ /** The integer revision a tag names: `ns/name:2` and `ns/name:2-creative` are both revision 2. */
42
+ export function revisionFromTag(tag) {
43
+ const match = /:(\d+)(?:-[A-Za-z0-9._]+)?$/.exec(tag)
44
+ return match ? Number(match[1]) : undefined
45
+ }
46
+
47
+ export function usage() {
48
+ return [
49
+ 'Usage: build-image.mjs --engine <docker|podman|wslc|path> --out <dir> [options]',
50
+ '',
51
+ ' --engine <bin> container engine binary that builds, inspects and saves',
52
+ ' --stage <name> base (default) or creative',
53
+ ' --tag <ref> image reference (default namzu-local-computer:2, or :2-creative)',
54
+ ' --out <dir> directory for the archive and manifest.json',
55
+ ' --format <fmt> zst (default), xz, gz or tar; all load directly with wslc load -i',
56
+ ' --level <n> compression level (zst 19, xz 9, gz 9)',
57
+ ' --revision <n> manifest revision (default: the number in the tag)',
58
+ ' --context <dir> build context (default packages/sandbox)',
59
+ ' --dockerfile <file> Dockerfile (default local-computer/Dockerfile)',
60
+ ' --no-cache build without the engine cache',
61
+ ' --skip-build save the existing image instead of building it',
62
+ ].join('\n')
63
+ }
64
+
65
+ export function parseArguments(argv) {
66
+ const options = {
67
+ engine: undefined,
68
+ stage: 'base',
69
+ tag: undefined,
70
+ out: undefined,
71
+ format: 'zst',
72
+ level: undefined,
73
+ revision: undefined,
74
+ context: resolve(HERE, '..'),
75
+ dockerfile: join(HERE, 'Dockerfile'),
76
+ noCache: false,
77
+ skipBuild: false,
78
+ }
79
+ const value = (index, flag) => {
80
+ const next = argv[index + 1]
81
+ if (next === undefined || next.startsWith('--'))
82
+ throw new BuildImageError(`${flag} needs a value`)
83
+ return next
84
+ }
85
+ for (let index = 0; index < argv.length; index += 1) {
86
+ const flag = argv[index]
87
+ switch (flag) {
88
+ case '--engine':
89
+ options.engine = value(index++, flag)
90
+ break
91
+ case '--stage':
92
+ options.stage = value(index++, flag)
93
+ break
94
+ case '--tag':
95
+ options.tag = value(index++, flag)
96
+ break
97
+ case '--out':
98
+ options.out = resolve(value(index++, flag))
99
+ break
100
+ case '--format':
101
+ options.format = value(index++, flag)
102
+ break
103
+ case '--level':
104
+ options.level = Number(value(index++, flag))
105
+ break
106
+ case '--revision':
107
+ options.revision = Number(value(index++, flag))
108
+ break
109
+ case '--context':
110
+ options.context = resolve(value(index++, flag))
111
+ break
112
+ case '--dockerfile':
113
+ options.dockerfile = resolve(value(index++, flag))
114
+ break
115
+ case '--no-cache':
116
+ options.noCache = true
117
+ break
118
+ case '--skip-build':
119
+ options.skipBuild = true
120
+ break
121
+ default:
122
+ throw new BuildImageError(`Unknown argument: ${flag}`)
123
+ }
124
+ }
125
+ if (!options.engine) throw new BuildImageError('--engine is required')
126
+ if (!options.out) throw new BuildImageError('--out is required')
127
+ if (!STAGES.includes(options.stage))
128
+ throw new BuildImageError(`--stage must be one of ${STAGES.join(', ')}`)
129
+ if (!FORMATS.includes(options.format))
130
+ throw new BuildImageError(`--format must be one of ${FORMATS.join(', ')}`)
131
+ options.tag ??= defaultTag(options.stage)
132
+ if (!/^[a-z0-9][a-z0-9._/-]*:[A-Za-z0-9._-]+$/.test(options.tag))
133
+ throw new BuildImageError(`--tag is not an image reference: ${options.tag}`)
134
+ options.revision ??= revisionFromTag(options.tag)
135
+ if (!Number.isInteger(options.revision) || options.revision < 1)
136
+ throw new BuildImageError('--revision must be a positive integer (or a tag ending in :<n>)')
137
+ options.level ??= DEFAULT_LEVELS[options.format]
138
+ if (!Number.isInteger(options.level) || options.level < 0 || options.level > 22)
139
+ throw new BuildImageError('--level must be an integer from 0 to 22')
140
+ return options
141
+ }
142
+
143
+ /** Real command runner: argv array, no shell, output captured (inherited for long builds). */
144
+ export function spawnRunner(file, args, { cwd, inherit = false } = {}) {
145
+ return new Promise((resolvePromise, reject) => {
146
+ const child = spawn(file, args, { cwd, stdio: inherit ? ['ignore', 'inherit', 'inherit'] : 'pipe' })
147
+ let stdout = ''
148
+ let stderr = ''
149
+ child.stdout?.on('data', (chunk) => {
150
+ stdout += chunk
151
+ })
152
+ child.stderr?.on('data', (chunk) => {
153
+ stderr += chunk
154
+ })
155
+ child.once('error', reject)
156
+ child.once('close', (code) => resolvePromise({ code: code ?? 1, stdout, stderr }))
157
+ })
158
+ }
159
+
160
+ async function exec(run, file, args, options) {
161
+ const result = await run(file, args, options)
162
+ if (result.code !== 0)
163
+ throw new BuildImageError(
164
+ `${basename(file)} ${args[0]} failed (${result.code}): ${(result.stderr || result.stdout || '').trim().slice(-500)}`,
165
+ )
166
+ return result
167
+ }
168
+
169
+ async function sha256File(path) {
170
+ const hash = createHash('sha256')
171
+ await pipeline(createReadStream(path), hash)
172
+ return hash.digest('hex')
173
+ }
174
+
175
+ /** Compress `source` to `destination`. xz has no Node codec, so it runs the engine host's `xz`. */
176
+ export async function compressFile({ run, source, destination, format, level }) {
177
+ if (format === 'tar') {
178
+ await rename(source, destination)
179
+ return
180
+ }
181
+ if (format === 'xz') {
182
+ await exec(run, 'xz', [`-${level}e`, '--keep', '--force', '--threads=0', source])
183
+ await rename(`${source}.xz`, destination)
184
+ await rm(source, { force: true })
185
+ return
186
+ }
187
+ let codec
188
+ if (format === 'gz') codec = zlib.createGzip({ level })
189
+ else {
190
+ if (typeof zlib.createZstdCompress !== 'function')
191
+ throw new BuildImageError('zstd needs Node 22.15 or newer, or use --format gz or xz')
192
+ codec = zlib.createZstdCompress({
193
+ params: {
194
+ // ZSTD_c_nbWorkers stays at its default of none: Node's zstd stream fails with
195
+ // "stream.push() after EOF" when workers are on (checked on Node 22.20 and 24.19). One
196
+ // thread takes about 5 minutes at level 19 (measured on the base stage).
197
+ [zlib.constants.ZSTD_c_compressionLevel]: level,
198
+ [zlib.constants.ZSTD_c_checksumFlag]: 1,
199
+ },
200
+ })
201
+ }
202
+ await pipeline(createReadStream(source), codec, createWriteStream(destination))
203
+ await rm(source, { force: true })
204
+ }
205
+
206
+ /**
207
+ * Build, save and compress one stage, then write manifest.json beside the archive.
208
+ * `run(file, args, { cwd, inherit })` resolves { code, stdout, stderr }.
209
+ */
210
+ export async function buildImage(options, { run = spawnRunner, log = () => {} } = {}) {
211
+ const { engine, stage, tag, out, format, level, revision, context, dockerfile } = options
212
+ await mkdir(out, { recursive: true })
213
+ const started = Date.now()
214
+ if (!options.skipBuild) {
215
+ const args = ['build', '--file', dockerfile, '--target', stage, '--tag', tag]
216
+ if (options.noCache) args.push('--no-cache')
217
+ args.push(context)
218
+ log(`building ${tag} (${stage})`)
219
+ await exec(run, engine, args, { inherit: true })
220
+ }
221
+ const inspected = await exec(run, engine, ['image', 'inspect', tag])
222
+ let record
223
+ try {
224
+ record = JSON.parse(inspected.stdout)
225
+ if (Array.isArray(record)) record = record[0]
226
+ } catch {
227
+ throw new BuildImageError(`image inspect ${tag} did not print JSON`)
228
+ }
229
+ const imageId = record?.Id ?? record?.ID
230
+ const protocol = record?.Config?.Labels?.[PROTOCOL_LABEL]
231
+ if (typeof imageId !== 'string' || !imageId) throw new BuildImageError(`${tag} has no image id`)
232
+ if (typeof protocol !== 'string' || !protocol)
233
+ throw new BuildImageError(`${tag} has no ${PROTOCOL_LABEL} label`)
234
+ const built = Date.now()
235
+
236
+ const stem = `${tag.replace(/[/:]/g, '-')}`
237
+ const tarPath = join(out, `${stem}.tar`)
238
+ log(`saving ${tag}`)
239
+ await exec(run, engine, ['save', '--output', tarPath, tag])
240
+ const sizeBytes = (await stat(tarPath)).size
241
+ const tarSha256 = await sha256File(tarPath)
242
+ const archive = join(out, `${stem}.${EXTENSIONS[format]}`)
243
+ log(`compressing to ${basename(archive)}`)
244
+ await compressFile({ run, source: tarPath, destination: archive, format, level })
245
+ const compressedBytes = (await stat(archive)).size
246
+ const sha256 = await sha256File(archive)
247
+ const manifest = {
248
+ revision,
249
+ imageRef: tag,
250
+ imageId,
251
+ protocol,
252
+ sha256,
253
+ sizeBytes,
254
+ compressedBytes,
255
+ format,
256
+ file: basename(archive),
257
+ stage,
258
+ tarSha256,
259
+ }
260
+ const manifestPath = join(out, 'manifest.json')
261
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`)
262
+ return {
263
+ manifest,
264
+ manifestPath,
265
+ archive,
266
+ buildMs: built - started,
267
+ totalMs: Date.now() - started,
268
+ }
269
+ }
270
+
271
+ /** Read a manifest back and check it against the archive beside it. */
272
+ export async function verifyManifest(manifestPath) {
273
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
274
+ const archive = join(dirname(manifestPath), manifest.file)
275
+ const info = await stat(archive)
276
+ if (info.size !== manifest.compressedBytes)
277
+ throw new BuildImageError(`${manifest.file} is ${info.size} bytes, manifest says ${manifest.compressedBytes}`)
278
+ if ((await sha256File(archive)) !== manifest.sha256)
279
+ throw new BuildImageError(`${manifest.file} does not match the manifest sha256`)
280
+ return manifest
281
+ }
282
+
283
+ export async function main(argv, deps = {}) {
284
+ if (argv.includes('--help') || argv.includes('-h')) {
285
+ process.stdout.write(`${usage()}\n`)
286
+ return 0
287
+ }
288
+ let options
289
+ try {
290
+ options = parseArguments(argv)
291
+ } catch (error) {
292
+ process.stderr.write(`${error.message}\n\n${usage()}\n`)
293
+ return 2
294
+ }
295
+ try {
296
+ const result = await buildImage(options, {
297
+ ...deps,
298
+ log: (line) => process.stderr.write(`${line}\n`),
299
+ })
300
+ process.stdout.write(`${JSON.stringify({ ...result.manifest, buildMs: result.buildMs, totalMs: result.totalMs })}\n`)
301
+ return 0
302
+ } catch (error) {
303
+ process.stderr.write(`${error.message}\n`)
304
+ return 1
305
+ }
306
+ }
307
+
308
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
309
+ process.exitCode = await main(process.argv.slice(2))
310
+ }