@anvia/sandbox 0.4.1 → 0.5.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.
package/dist/cli.js ADDED
@@ -0,0 +1,836 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ assertDockerCli
4
+ } from "./chunk-FTNNCT6S.js";
5
+
6
+ // src/cli.ts
7
+ import { lstat, mkdir, readFile, rm, writeFile } from "fs/promises";
8
+ import path from "path";
9
+ import { fileURLToPath } from "url";
10
+
11
+ // src/image-builder.ts
12
+ var defaultSandboxImageVersions = {
13
+ node: "24.18.0",
14
+ pnpm: "11.0.4",
15
+ bun: "1.3.14",
16
+ python: "3.13.14",
17
+ uv: "0.11.29",
18
+ playwright: "1.61.0"
19
+ };
20
+ var artifactPythonPackages = [
21
+ "matplotlib==3.11.1",
22
+ "seaborn==0.13.2",
23
+ "Pillow==12.3.0",
24
+ "ReportLab==5.0.0",
25
+ "pypdf==6.14.2",
26
+ "pandas==3.0.3",
27
+ "openpyxl==3.1.5",
28
+ "XlsxWriter==3.2.9",
29
+ "python-docx==1.2.0"
30
+ ];
31
+ var runtimeOrder = ["node", "bun", "python"];
32
+ var featureOrder = ["artifacts", "playwright"];
33
+ var commonAptPackages = ["bash", "ca-certificates", "findutils", "libstdc++6", "procps"];
34
+ function resolveSandboxImageSpec(input) {
35
+ validateName(input.name);
36
+ const runtimes = new Set(input.runtimes ?? []);
37
+ const features = new Set(input.features ?? []);
38
+ const packages = {
39
+ apt: unique(input.packages?.apt ?? []),
40
+ npm: unique(input.packages?.npm ?? []),
41
+ uv: unique(input.packages?.uv ?? [])
42
+ };
43
+ for (const runtime of runtimes) validateRuntime(runtime);
44
+ for (const feature of features) validateFeature(feature);
45
+ for (const packageName of packages.apt) validateAptPackage(packageName);
46
+ for (const packageSpec of packages.npm) parseNpmPackageSpec(packageSpec);
47
+ for (const requirement of packages.uv) validateUvRequirement(requirement);
48
+ if (features.has("artifacts") || packages.uv.length > 0) runtimes.add("python");
49
+ if (features.has("playwright")) runtimes.add("node");
50
+ if (packages.npm.length > 0 && !runtimes.has("node") && !runtimes.has("bun")) {
51
+ runtimes.add("node");
52
+ }
53
+ if (runtimes.size === 0) {
54
+ throw new Error("Select at least one runtime or feature.");
55
+ }
56
+ const versions = { ...defaultSandboxImageVersions, ...input.versions };
57
+ for (const [name, version] of Object.entries(versions)) validateVersion(name, version);
58
+ const tag = input.tag ?? `anvia-sandbox-${input.name}:latest`;
59
+ validateImageTag(tag);
60
+ return {
61
+ name: input.name,
62
+ tag,
63
+ runtimes: runtimeOrder.filter((runtime) => runtimes.has(runtime)),
64
+ features: featureOrder.filter((feature) => features.has(feature)),
65
+ packages,
66
+ versions
67
+ };
68
+ }
69
+ function renderSandboxImageContext(spec, generatorVersion) {
70
+ const files = /* @__PURE__ */ new Map();
71
+ const pythonRequirements = [
72
+ ...spec.features.includes("artifacts") ? artifactPythonPackages : [],
73
+ ...spec.packages.uv
74
+ ];
75
+ const npmDependencies = npmDependenciesFor(spec);
76
+ files.set("Dockerfile", `${renderDockerfile(spec, pythonRequirements, npmDependencies)}
77
+ `);
78
+ files.set(
79
+ ".dockerignore",
80
+ renderDockerignore(pythonRequirements.length > 0, npmDependencies.size > 0)
81
+ );
82
+ if (pythonRequirements.length > 0) {
83
+ files.set("pyproject.toml", renderPythonProject(spec, unique(pythonRequirements)));
84
+ }
85
+ if (npmDependencies.size > 0) {
86
+ files.set(
87
+ "package.json",
88
+ `${JSON.stringify(
89
+ {
90
+ private: true,
91
+ description: `Generated dependencies for ${spec.name}`,
92
+ dependencies: Object.fromEntries(npmDependencies)
93
+ },
94
+ null,
95
+ 2
96
+ )}
97
+ `
98
+ );
99
+ }
100
+ const generatedFiles = [...files.keys(), "anvia-sandbox.json"].sort();
101
+ const manifest = {
102
+ schemaVersion: 1,
103
+ generatedBy: {
104
+ package: "@anvia/sandbox",
105
+ version: generatorVersion
106
+ },
107
+ ...spec,
108
+ generatedFiles
109
+ };
110
+ files.set("anvia-sandbox.json", `${JSON.stringify(manifest, null, 2)}
111
+ `);
112
+ return { manifest, files };
113
+ }
114
+ function unpinnedSandboxImagePackages(spec) {
115
+ const unpinned = [];
116
+ for (const value of spec.packages.npm) {
117
+ if (parseNpmPackageSpec(value).version === "latest") unpinned.push(value);
118
+ }
119
+ for (const value of spec.packages.uv) {
120
+ if (!/===|==|@\s*https?:/i.test(value)) unpinned.push(value);
121
+ }
122
+ return unpinned;
123
+ }
124
+ function renderDockerfile(spec, pythonRequirements, npmDependencies) {
125
+ const hasNode = spec.runtimes.includes("node");
126
+ const hasBun = spec.runtimes.includes("bun");
127
+ const hasPython = spec.runtimes.includes("python");
128
+ const hasPlaywright = spec.features.includes("playwright");
129
+ const lines = [
130
+ "# Generated by @anvia/sandbox. Edit the manifest and regenerate instead of editing this file."
131
+ ];
132
+ if (hasNode) lines.push(`FROM node:${spec.versions.node}-bookworm-slim AS node-runtime`);
133
+ if (hasPython) lines.push(`FROM python:${spec.versions.python}-slim-bookworm AS python-runtime`);
134
+ if (hasBun) lines.push(`FROM oven/bun:${spec.versions.bun}-slim AS bun-runtime`);
135
+ if (hasPython) lines.push(`FROM ghcr.io/astral-sh/uv:${spec.versions.uv} AS uv-runtime`);
136
+ if (hasPlaywright) {
137
+ lines.push(`FROM mcr.microsoft.com/playwright:v${spec.versions.playwright}-noble AS final`);
138
+ } else if (hasPython) {
139
+ lines.push("FROM python-runtime AS final");
140
+ } else if (hasNode) {
141
+ lines.push("FROM node-runtime AS final");
142
+ } else {
143
+ lines.push("FROM bun-runtime AS final");
144
+ }
145
+ lines.push("", "USER root");
146
+ if (hasPython && (hasPlaywright || !isFinalRuntime(spec, "python"))) {
147
+ lines.push("COPY --from=python-runtime /usr/local/ /usr/local/");
148
+ }
149
+ if (hasNode && (hasPlaywright || !isFinalRuntime(spec, "node"))) {
150
+ lines.push("COPY --from=node-runtime /usr/local/bin/ /usr/local/bin/");
151
+ lines.push(
152
+ "COPY --from=node-runtime /usr/local/lib/node_modules/ /usr/local/lib/node_modules/"
153
+ );
154
+ }
155
+ if (hasBun && !isFinalRuntime(spec, "bun")) {
156
+ lines.push("COPY --from=bun-runtime /usr/local/bin/bun /usr/local/bin/bun");
157
+ lines.push("RUN ln -sf /usr/local/bin/bun /usr/local/bin/bunx");
158
+ }
159
+ if (hasPython) {
160
+ lines.push("COPY --from=uv-runtime /uv /uvx /usr/local/bin/");
161
+ }
162
+ const aptPackages = unique([
163
+ ...commonAptPackages,
164
+ ...spec.features.includes("artifacts") ? ["fonts-dejavu-core"] : [],
165
+ ...spec.packages.apt
166
+ ]).sort();
167
+ lines.push(
168
+ "",
169
+ "RUN apt-get update \\",
170
+ ` && apt-get install -y --no-install-recommends ${aptPackages.map(shellQuote).join(" ")} \\`,
171
+ " && rm -rf /var/lib/apt/lists/*"
172
+ );
173
+ if (hasNode) {
174
+ lines.push(
175
+ "",
176
+ `RUN npm install --global ${shellQuote(`pnpm@${spec.versions.pnpm}`)} --no-audit --no-fund \\`,
177
+ " && npm cache clean --force"
178
+ );
179
+ }
180
+ if (pythonRequirements.length > 0) {
181
+ lines.push(
182
+ "",
183
+ "COPY pyproject.toml /opt/anvia-python/pyproject.toml",
184
+ "RUN cd /opt/anvia-python \\",
185
+ " && uv sync --no-dev --no-cache --no-install-project",
186
+ "ENV VIRTUAL_ENV=/opt/anvia-python/.venv",
187
+ "ENV PATH=/opt/anvia-python/.venv/bin:$PATH"
188
+ );
189
+ }
190
+ if (npmDependencies.size > 0) {
191
+ lines.push("", "COPY package.json /opt/anvia-js/package.json");
192
+ if (hasNode) {
193
+ lines.push(
194
+ "RUN npm install --prefix /opt/anvia-js --omit=dev --no-audit --no-fund \\",
195
+ " && ln -s /opt/anvia-js/node_modules /node_modules \\",
196
+ " && npm cache clean --force"
197
+ );
198
+ } else {
199
+ lines.push(
200
+ "RUN cd /opt/anvia-js \\",
201
+ " && bun install --production --no-save \\",
202
+ " && ln -s /opt/anvia-js/node_modules /node_modules"
203
+ );
204
+ }
205
+ lines.push("ENV NODE_PATH=/opt/anvia-js/node_modules");
206
+ lines.push("ENV PATH=/opt/anvia-js/node_modules/.bin:$PATH");
207
+ }
208
+ lines.push(
209
+ "",
210
+ ...hasPlaywright ? ["ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright"] : [],
211
+ "RUN mkdir -p /workspace",
212
+ "WORKDIR /workspace",
213
+ "ENTRYPOINT []",
214
+ `CMD ["sh", "-c", "trap 'exit 0' TERM INT; while :; do sleep 3600 & wait $!; done"]`
215
+ );
216
+ return lines.join("\n");
217
+ }
218
+ function renderDockerignore(hasPython, hasNpm) {
219
+ return [
220
+ "**",
221
+ "!Dockerfile",
222
+ "!.dockerignore",
223
+ "!anvia-sandbox.json",
224
+ ...hasPython ? ["!pyproject.toml"] : [],
225
+ ...hasNpm ? ["!package.json"] : [],
226
+ ""
227
+ ].join("\n");
228
+ }
229
+ function renderPythonProject(spec, requirements) {
230
+ const [major, minor] = spec.versions.python.split(".");
231
+ return [
232
+ "[project]",
233
+ `name = ${JSON.stringify(`anvia-sandbox-${spec.name}`)}`,
234
+ 'version = "0.0.0"',
235
+ `requires-python = ${JSON.stringify(`>=${major}.${minor}`)}`,
236
+ "dependencies = [",
237
+ ...requirements.map((requirement) => ` ${JSON.stringify(requirement)},`),
238
+ "]",
239
+ ""
240
+ ].join("\n");
241
+ }
242
+ function npmDependenciesFor(spec) {
243
+ const dependencies = /* @__PURE__ */ new Map();
244
+ if (spec.features.includes("playwright")) {
245
+ dependencies.set("playwright", spec.versions.playwright);
246
+ }
247
+ for (const packageSpec of spec.packages.npm) {
248
+ const parsed = parseNpmPackageSpec(packageSpec);
249
+ if (dependencies.has(parsed.name)) {
250
+ throw new Error(`Duplicate npm package: ${parsed.name}`);
251
+ }
252
+ dependencies.set(parsed.name, parsed.version);
253
+ }
254
+ return new Map([...dependencies.entries()].sort(([left], [right]) => left.localeCompare(right)));
255
+ }
256
+ function isFinalRuntime(spec, runtime) {
257
+ if (spec.features.includes("playwright")) return false;
258
+ if (spec.runtimes.includes("python")) return runtime === "python";
259
+ if (spec.runtimes.includes("node")) return runtime === "node";
260
+ return runtime === "bun";
261
+ }
262
+ function validateName(name) {
263
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) {
264
+ throw new Error(
265
+ "Image name must be 1-63 lowercase letters, numbers, or hyphens and cannot start with a hyphen."
266
+ );
267
+ }
268
+ }
269
+ function validateRuntime(runtime) {
270
+ if (!runtimeOrder.includes(runtime)) {
271
+ throw new Error(`Unknown runtime: ${runtime}. Expected node, bun, or python.`);
272
+ }
273
+ }
274
+ function validateFeature(feature) {
275
+ if (!featureOrder.includes(feature)) {
276
+ throw new Error(`Unknown feature: ${feature}. Expected artifacts or playwright.`);
277
+ }
278
+ }
279
+ function validateAptPackage(packageName) {
280
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9.+:~=-]*$/.test(packageName)) {
281
+ throw new Error(`Invalid apt package: ${packageName}`);
282
+ }
283
+ }
284
+ function parseNpmPackageSpec(packageSpec) {
285
+ if (/\s/.test(packageSpec) || hasControlCharacter(packageSpec)) {
286
+ throw new Error(`Invalid npm package spec: ${packageSpec}`);
287
+ }
288
+ const separator = packageSpec.startsWith("@") ? packageSpec.indexOf("@", packageSpec.indexOf("/") + 1) : packageSpec.indexOf("@");
289
+ const name = separator === -1 ? packageSpec : packageSpec.slice(0, separator);
290
+ const version = separator === -1 ? "latest" : packageSpec.slice(separator + 1);
291
+ if (!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(name) || !version) {
292
+ throw new Error(`Invalid npm package spec: ${packageSpec}`);
293
+ }
294
+ if (/[\s'"`$;&|<>\\]/.test(version) || hasControlCharacter(version)) {
295
+ throw new Error(`Invalid npm package version in: ${packageSpec}`);
296
+ }
297
+ return { name, version };
298
+ }
299
+ function validateUvRequirement(requirement) {
300
+ if (!requirement || requirement.startsWith("-") || hasControlCharacter(requirement)) {
301
+ throw new Error(`Invalid uv package requirement: ${requirement}`);
302
+ }
303
+ }
304
+ function validateVersion(name, version) {
305
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(version)) {
306
+ throw new Error(`Invalid ${name} version: ${version}`);
307
+ }
308
+ }
309
+ function validateImageTag(tag) {
310
+ if (!tag || tag.startsWith("-") || tag.includes("@") || /\s/.test(tag) || hasControlCharacter(tag)) {
311
+ throw new Error(`Invalid Docker image tag: ${tag}`);
312
+ }
313
+ }
314
+ function unique(values) {
315
+ return [...new Set(values)];
316
+ }
317
+ function hasControlCharacter(value) {
318
+ return [...value].some((character) => character.charCodeAt(0) < 32);
319
+ }
320
+ function shellQuote(value) {
321
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
322
+ }
323
+
324
+ // src/cli.ts
325
+ var generatedFileNames = /* @__PURE__ */ new Set([
326
+ ".dockerignore",
327
+ "Dockerfile",
328
+ "anvia-sandbox.json",
329
+ "package.json",
330
+ "pyproject.toml"
331
+ ]);
332
+ var commonAptPackages2 = [
333
+ { value: "git", label: "Git", hint: "Source control and repository operations" },
334
+ { value: "curl", label: "curl", hint: "HTTP downloads and API debugging" },
335
+ { value: "jq", label: "jq", hint: "JSON processing from the shell" },
336
+ { value: "ffmpeg", label: "FFmpeg", hint: "Audio and video processing" },
337
+ { value: "imagemagick", label: "ImageMagick", hint: "Image conversion and editing" },
338
+ { value: "poppler-utils", label: "Poppler tools", hint: "PDF inspection and conversion" },
339
+ { value: "libreoffice", label: "LibreOffice", hint: "Large; office document conversion" }
340
+ ];
341
+ var commonNpmPackages = [
342
+ { value: "pdfkit@0.19.1", label: "PDFKit", hint: "Generate PDF documents" },
343
+ { value: "sharp@0.35.3", label: "Sharp", hint: "Resize and transform images" },
344
+ { value: "exceljs@4.4.0", label: "ExcelJS", hint: "Read and write Excel workbooks" },
345
+ { value: "docx@9.7.1", label: "docx", hint: "Generate Word documents" },
346
+ { value: "pptxgenjs@4.0.1", label: "PptxGenJS", hint: "Generate PowerPoint presentations" }
347
+ ];
348
+ var commonUvPackages = [
349
+ { value: "httpx==0.28.1", label: "HTTPX", hint: "Modern HTTP client" },
350
+ { value: "beautifulsoup4==4.15.0", label: "Beautiful Soup", hint: "HTML and XML parsing" },
351
+ { value: "scipy==1.18.0", label: "SciPy", hint: "Scientific computing" },
352
+ { value: "scikit-learn==1.9.0", label: "scikit-learn", hint: "Machine learning utilities" },
353
+ { value: "polars==1.42.1", label: "Polars", hint: "Fast dataframe processing" }
354
+ ];
355
+ var defaultIo = {
356
+ log: console.log,
357
+ warn: console.warn,
358
+ error: console.error,
359
+ stdout: (chunk) => process.stdout.write(chunk),
360
+ stderr: (chunk) => process.stderr.write(chunk)
361
+ };
362
+ async function runCli(argv = process.argv.slice(2), cwd = process.cwd(), io = defaultIo, dependencies = {}) {
363
+ let options;
364
+ try {
365
+ options = parseArgs(argv);
366
+ } catch (error) {
367
+ io.error(errorMessage(error));
368
+ io.log(helpText());
369
+ return 1;
370
+ }
371
+ if (options.help || options.command === void 0) {
372
+ io.log(helpText());
373
+ return 0;
374
+ }
375
+ if (options.command !== "create-image") {
376
+ io.error(`Unknown command: ${options.command}`);
377
+ io.log(helpText());
378
+ return 1;
379
+ }
380
+ try {
381
+ const shouldPrompt = options.name === void 0 || options.runtimes.length === 0 && options.features.length === 0 && options.npm.length === 0 && options.uv.length === 0;
382
+ if (shouldPrompt) {
383
+ const isTTY = dependencies.isTTY ?? (process.stdin.isTTY && process.stdout.isTTY);
384
+ if (!isTTY) {
385
+ throw new Error(
386
+ "Non-interactive create-image requires --name and at least one --runtime or --feature."
387
+ );
388
+ }
389
+ const prompt = dependencies.prompt ?? promptForImage;
390
+ const result = await prompt(options);
391
+ if (result === void 0) return 130;
392
+ options = applyPromptResult(options, result);
393
+ }
394
+ const input = {
395
+ name: required(options.name, "Image name is required."),
396
+ runtimes: options.runtimes,
397
+ features: options.features,
398
+ packages: {
399
+ apt: options.apt,
400
+ npm: options.npm,
401
+ uv: options.uv
402
+ },
403
+ versions: options.versions
404
+ };
405
+ if (options.tag !== void 0) input.tag = options.tag;
406
+ const spec = resolveSandboxImageSpec(input);
407
+ const packageVersion = dependencies.packageVersion ?? await readPackageVersion();
408
+ const context = renderSandboxImageContext(spec, packageVersion);
409
+ const outputPath = path.resolve(
410
+ cwd,
411
+ options.output ?? path.join(".anvia", "sandbox-images", spec.name)
412
+ );
413
+ const displayPath = relativeDisplayPath(cwd, outputPath);
414
+ const unpinned = unpinnedSandboxImagePackages(spec);
415
+ if (unpinned.length > 0) {
416
+ io.warn(`Unpinned custom packages may change on rebuild: ${unpinned.join(", ")}`);
417
+ }
418
+ if (options.dryRun) {
419
+ printDryRun(context.files, displayPath, spec.tag, io);
420
+ return 0;
421
+ }
422
+ await writeImageContext(outputPath, context.manifest, context.files, options.force);
423
+ io.log(`Created ${displayPath}`);
424
+ if (!options.build) {
425
+ io.log("");
426
+ io.log("Build later:");
427
+ io.log(` ${shellCommand([options.dockerPath, "build", "--tag", spec.tag, displayPath])}`);
428
+ printUsageSnippet(spec.tag, io);
429
+ return 0;
430
+ }
431
+ const buildImage = dependencies.buildImage ?? buildDockerImage;
432
+ await buildImage({
433
+ contextPath: outputPath,
434
+ tag: spec.tag,
435
+ dockerPath: options.dockerPath,
436
+ io
437
+ });
438
+ io.log(`Built ${spec.tag}`);
439
+ printUsageSnippet(spec.tag, io);
440
+ return 0;
441
+ } catch (error) {
442
+ io.error(errorMessage(error));
443
+ return 1;
444
+ }
445
+ }
446
+ async function promptForImage(options) {
447
+ assertInteractiveNodeVersion();
448
+ const prompts = await import("@clack/prompts");
449
+ prompts.intro("Create an Anvia sandbox image");
450
+ const nameResult = options.name ?? await prompts.text({
451
+ message: "Image name",
452
+ placeholder: "reports",
453
+ validate: (value) => /^[a-z0-9][a-z0-9-]{0,62}$/.test(value ?? "") ? void 0 : "Use 1-63 lowercase letters, numbers, or hyphens."
454
+ });
455
+ if (prompts.isCancel(nameResult)) return cancelPrompt(prompts);
456
+ const name = String(nameResult);
457
+ let runtimes = [...options.runtimes];
458
+ let features = [...options.features];
459
+ if (runtimes.length === 0 && features.length === 0 && options.npm.length === 0 && options.uv.length === 0) {
460
+ const capabilities = await prompts.multiselect({
461
+ message: "Select runtimes and features",
462
+ required: true,
463
+ options: [
464
+ { value: "node", label: "Node.js", hint: "Includes npm and pnpm" },
465
+ { value: "bun", label: "Bun", hint: "Includes bun and bunx" },
466
+ { value: "python", label: "Python", hint: "Includes uv and uvx" },
467
+ { value: "artifacts", label: "Reporting and artifacts", hint: "Adds Python automatically" },
468
+ { value: "playwright", label: "Playwright + Chromium", hint: "Adds Node.js automatically" }
469
+ ]
470
+ });
471
+ if (prompts.isCancel(capabilities)) return cancelPrompt(prompts);
472
+ runtimes = capabilities.filter(isRuntime);
473
+ features = capabilities.filter(isFeature);
474
+ }
475
+ const aptResult = await selectCommonPackages(
476
+ prompts,
477
+ "Select common apt tools (optional)",
478
+ commonAptPackages2,
479
+ options.apt
480
+ );
481
+ if (aptResult === void 0) return cancelPrompt(prompts);
482
+ const npmResult = await selectCommonPackages(
483
+ prompts,
484
+ "Select common npm libraries (optional)",
485
+ commonNpmPackages,
486
+ options.npm
487
+ );
488
+ if (npmResult === void 0) return cancelPrompt(prompts);
489
+ const uvResult = await selectCommonPackages(
490
+ prompts,
491
+ "Select common Python libraries with uv (optional)",
492
+ commonUvPackages,
493
+ options.uv
494
+ );
495
+ if (uvResult === void 0) return cancelPrompt(prompts);
496
+ const tagResult = options.tag ?? await prompts.text({
497
+ message: "Docker image tag",
498
+ initialValue: `anvia-sandbox-${name}:latest`
499
+ });
500
+ if (prompts.isCancel(tagResult)) return cancelPrompt(prompts);
501
+ const outputResult = options.output ?? await prompts.text({
502
+ message: "Generated source directory",
503
+ initialValue: path.join(".anvia", "sandbox-images", name)
504
+ });
505
+ if (prompts.isCancel(outputResult)) return cancelPrompt(prompts);
506
+ const buildResult = options.buildExplicit ? options.build : await prompts.confirm({ message: "Build the image now?", initialValue: true });
507
+ if (prompts.isCancel(buildResult)) return cancelPrompt(prompts);
508
+ const confirmed = await prompts.confirm({
509
+ message: "Create this sandbox image?",
510
+ initialValue: true
511
+ });
512
+ if (prompts.isCancel(confirmed) || !confirmed) return cancelPrompt(prompts);
513
+ prompts.outro("Configuration ready");
514
+ return {
515
+ name,
516
+ runtimes,
517
+ features,
518
+ apt: aptResult,
519
+ npm: npmResult,
520
+ uv: uvResult,
521
+ tag: String(tagResult),
522
+ output: String(outputResult),
523
+ build: Boolean(buildResult)
524
+ };
525
+ }
526
+ async function selectCommonPackages(prompts, message, options, existing) {
527
+ const commonValues = new Set(options.map((option) => option.value));
528
+ const result = await prompts.multiselect({
529
+ message,
530
+ required: false,
531
+ options: [...options],
532
+ initialValues: existing.filter((value) => commonValues.has(value))
533
+ });
534
+ if (prompts.isCancel(result)) return void 0;
535
+ return [.../* @__PURE__ */ new Set([...existing, ...result])];
536
+ }
537
+ function cancelPrompt(prompts) {
538
+ prompts.cancel("Image creation cancelled.");
539
+ return void 0;
540
+ }
541
+ async function buildDockerImage(input) {
542
+ await assertDockerCli(["build", "--tag", input.tag, input.contextPath], {
543
+ dockerPath: input.dockerPath,
544
+ onStdout: input.io.stdout,
545
+ onStderr: input.io.stderr
546
+ });
547
+ }
548
+ async function writeImageContext(outputPath, manifest, files, force) {
549
+ const outputStat = await safeLstat(outputPath);
550
+ if (outputStat?.isSymbolicLink()) {
551
+ throw new Error(`Refusing to write through symlink: ${outputPath}`);
552
+ }
553
+ if (outputStat !== void 0) {
554
+ if (!outputStat.isDirectory()) throw new Error(`Output path is not a directory: ${outputPath}`);
555
+ const previousManifest = await readGeneratedManifest(outputPath);
556
+ if (previousManifest === void 0) {
557
+ throw new Error(
558
+ `Output directory already exists but was not generated by @anvia/sandbox: ${outputPath}`
559
+ );
560
+ }
561
+ if (!force) throw new Error(`Output directory already exists. Use --force to regenerate it.`);
562
+ for (const filename of previousManifest.generatedFiles) {
563
+ const target = path.join(outputPath, filename);
564
+ const targetStat = await safeLstat(target);
565
+ if (targetStat?.isSymbolicLink()) throw new Error(`Refusing to replace symlink: ${target}`);
566
+ if (targetStat !== void 0) await rm(target);
567
+ }
568
+ } else {
569
+ await mkdir(outputPath, { recursive: true });
570
+ }
571
+ for (const [filename, content] of files) {
572
+ if (!generatedFileNames.has(filename))
573
+ throw new Error(`Unexpected generated filename: ${filename}`);
574
+ const target = path.join(outputPath, filename);
575
+ const targetStat = await safeLstat(target);
576
+ if (targetStat?.isSymbolicLink()) throw new Error(`Refusing to replace symlink: ${target}`);
577
+ await writeFile(target, content, "utf8");
578
+ }
579
+ if (!files.has("anvia-sandbox.json") || manifest.generatedFiles.length !== files.size) {
580
+ throw new Error("Generated image context manifest is inconsistent.");
581
+ }
582
+ }
583
+ async function readGeneratedManifest(outputPath) {
584
+ const manifestPath = path.join(outputPath, "anvia-sandbox.json");
585
+ const stat = await safeLstat(manifestPath);
586
+ if (stat === void 0) return void 0;
587
+ if (stat.isSymbolicLink() || !stat.isFile()) {
588
+ throw new Error(`Invalid generated manifest: ${manifestPath}`);
589
+ }
590
+ let value;
591
+ try {
592
+ value = JSON.parse(await readFile(manifestPath, "utf8"));
593
+ } catch (error) {
594
+ throw new Error(`Unable to read generated manifest: ${manifestPath}`, { cause: error });
595
+ }
596
+ if (!isGeneratedManifest(value)) {
597
+ throw new Error(
598
+ `Output manifest is not recognized as an @anvia/sandbox manifest: ${manifestPath}`
599
+ );
600
+ }
601
+ return value;
602
+ }
603
+ function isGeneratedManifest(value) {
604
+ if (typeof value !== "object" || value === null) return false;
605
+ const record = value;
606
+ const generatedBy = record.generatedBy;
607
+ const files = record.generatedFiles;
608
+ return record.schemaVersion === 1 && generatedBy?.package === "@anvia/sandbox" && Array.isArray(files) && files.every((file) => typeof file === "string" && generatedFileNames.has(file));
609
+ }
610
+ async function safeLstat(target) {
611
+ try {
612
+ return await lstat(target);
613
+ } catch (error) {
614
+ if (error.code === "ENOENT") return void 0;
615
+ throw error;
616
+ }
617
+ }
618
+ function printDryRun(files, outputPath, tag, io) {
619
+ io.log(`Would create ${outputPath}`);
620
+ for (const [filename, content] of files) {
621
+ io.log("");
622
+ io.log(`--- ${filename}`);
623
+ io.log(content.trimEnd());
624
+ }
625
+ io.log("");
626
+ io.log(`Would build ${tag}`);
627
+ }
628
+ function printUsageSnippet(tag, io) {
629
+ io.log("");
630
+ io.log("Use with @anvia/sandbox:");
631
+ io.log(" const sandbox = new DockerSandbox({");
632
+ io.log(` image: ${JSON.stringify(tag)},`);
633
+ io.log(' pull: "never",');
634
+ io.log(" });");
635
+ }
636
+ function applyPromptResult(options, result) {
637
+ const next = {
638
+ ...options,
639
+ name: result.name,
640
+ runtimes: result.runtimes,
641
+ features: result.features,
642
+ apt: result.apt,
643
+ npm: result.npm,
644
+ uv: result.uv,
645
+ build: result.build,
646
+ buildExplicit: true
647
+ };
648
+ if (result.tag !== void 0) next.tag = result.tag;
649
+ if (result.output !== void 0) next.output = result.output;
650
+ return next;
651
+ }
652
+ function parseArgs(argv) {
653
+ const options = {
654
+ runtimes: [],
655
+ features: [],
656
+ apt: [],
657
+ npm: [],
658
+ uv: [],
659
+ versions: {},
660
+ dockerPath: "docker",
661
+ build: true,
662
+ buildExplicit: false,
663
+ dryRun: false,
664
+ force: false,
665
+ help: false
666
+ };
667
+ for (let index = 0; index < argv.length; index += 1) {
668
+ const argument = argv[index] ?? "";
669
+ if (!argument.startsWith("-")) {
670
+ if (options.command !== void 0) throw new Error(`Unexpected argument: ${argument}`);
671
+ options.command = argument;
672
+ continue;
673
+ }
674
+ const [flag, inlineValue] = splitFlag(argument);
675
+ const value = () => inlineValue ?? required(argv[++index], `${flag} requires a value.`);
676
+ switch (flag) {
677
+ case "-h":
678
+ case "--help":
679
+ options.help = true;
680
+ break;
681
+ case "--name":
682
+ options.name = value();
683
+ break;
684
+ case "--tag":
685
+ options.tag = value();
686
+ break;
687
+ case "--output":
688
+ options.output = value();
689
+ break;
690
+ case "--runtime":
691
+ options.runtimes.push(parseRuntime(value()));
692
+ break;
693
+ case "--feature":
694
+ options.features.push(parseFeature(value()));
695
+ break;
696
+ case "--apt":
697
+ options.apt.push(value());
698
+ break;
699
+ case "--npm":
700
+ options.npm.push(value());
701
+ break;
702
+ case "--uv":
703
+ options.uv.push(value());
704
+ break;
705
+ case "--node-version":
706
+ options.versions.node = value();
707
+ break;
708
+ case "--pnpm-version":
709
+ options.versions.pnpm = value();
710
+ break;
711
+ case "--bun-version":
712
+ options.versions.bun = value();
713
+ break;
714
+ case "--python-version":
715
+ options.versions.python = value();
716
+ break;
717
+ case "--uv-version":
718
+ options.versions.uv = value();
719
+ break;
720
+ case "--playwright-version":
721
+ options.versions.playwright = value();
722
+ break;
723
+ case "--docker-path":
724
+ options.dockerPath = value();
725
+ break;
726
+ case "--no-build":
727
+ rejectInlineValue(flag, inlineValue);
728
+ options.build = false;
729
+ options.buildExplicit = true;
730
+ break;
731
+ case "--dry-run":
732
+ rejectInlineValue(flag, inlineValue);
733
+ options.dryRun = true;
734
+ break;
735
+ case "--force":
736
+ rejectInlineValue(flag, inlineValue);
737
+ options.force = true;
738
+ break;
739
+ default:
740
+ throw new Error(`Unknown option: ${flag}`);
741
+ }
742
+ }
743
+ return options;
744
+ }
745
+ function splitFlag(argument) {
746
+ const separator = argument.indexOf("=");
747
+ return separator === -1 ? [argument, void 0] : [argument.slice(0, separator), argument.slice(separator + 1)];
748
+ }
749
+ function rejectInlineValue(flag, value) {
750
+ if (value !== void 0) throw new Error(`${flag} does not accept a value.`);
751
+ }
752
+ function parseRuntime(value) {
753
+ if (isRuntime(value)) return value;
754
+ throw new Error(`Unknown runtime: ${value}. Expected node, bun, or python.`);
755
+ }
756
+ function parseFeature(value) {
757
+ if (isFeature(value)) return value;
758
+ throw new Error(`Unknown feature: ${value}. Expected artifacts or playwright.`);
759
+ }
760
+ function isRuntime(value) {
761
+ return value === "node" || value === "bun" || value === "python";
762
+ }
763
+ function isFeature(value) {
764
+ return value === "artifacts" || value === "playwright";
765
+ }
766
+ function assertInteractiveNodeVersion() {
767
+ const [major = 0, minor = 0] = process.versions.node.split(".").map(Number);
768
+ if (major < 20 || major === 20 && minor < 12) {
769
+ throw new Error("The interactive create-image wizard requires Node.js 20.12 or newer.");
770
+ }
771
+ }
772
+ async function readPackageVersion() {
773
+ const packageJson = JSON.parse(
774
+ await readFile(new URL("../package.json", import.meta.url), "utf8")
775
+ );
776
+ return typeof packageJson.version === "string" ? packageJson.version : "unknown";
777
+ }
778
+ function relativeDisplayPath(cwd, target) {
779
+ const relative = path.relative(cwd, target);
780
+ return relative && !relative.startsWith("..") ? relative : target;
781
+ }
782
+ function shellCommand(args) {
783
+ return args.map(shellArgument).join(" ");
784
+ }
785
+ function shellArgument(value) {
786
+ if (/^[a-zA-Z0-9_./:@+-]+$/.test(value)) return value;
787
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
788
+ }
789
+ function required(value, message) {
790
+ if (value === void 0 || value === "") throw new Error(message);
791
+ return value;
792
+ }
793
+ function errorMessage(error) {
794
+ return error instanceof Error ? error.message : String(error);
795
+ }
796
+ function helpText() {
797
+ return `Usage:
798
+ pnpm dlx @anvia/sandbox create-image [options]
799
+
800
+ Options:
801
+ --name <slug> Image profile name
802
+ --runtime <node|bun|python> Runtime to include; repeatable
803
+ --feature <artifacts|playwright>
804
+ Curated feature to include; repeatable
805
+ --apt <package> Additional apt package; repeatable
806
+ --npm <package[@version]> Additional npm package; repeatable
807
+ --uv <requirement> Additional Python package installed with uv; repeatable
808
+ --tag <tag> Local image tag
809
+ --output <directory> Generated context directory
810
+ --node-version <version> Override Node.js version
811
+ --pnpm-version <version> Override pnpm version
812
+ --bun-version <version> Override Bun version
813
+ --python-version <version> Override Python version
814
+ --uv-version <version> Override uv version
815
+ --playwright-version <version> Override Playwright and browser image version
816
+ --docker-path <path> Docker CLI path (default: docker)
817
+ --no-build Generate files without building
818
+ --dry-run Print generated files without writing or building
819
+ --force Regenerate a context previously created by this CLI
820
+ -h, --help Show this help
821
+
822
+ Examples:
823
+ pnpm dlx @anvia/sandbox create-image
824
+ pnpm dlx @anvia/sandbox create-image --name reports --feature artifacts
825
+ pnpm dlx @anvia/sandbox create-image --name browser --runtime bun --feature playwright`;
826
+ }
827
+ var invokedPath = process.argv[1] === void 0 ? void 0 : path.resolve(process.argv[1]);
828
+ if (invokedPath !== void 0 && invokedPath === fileURLToPath(import.meta.url)) {
829
+ runCli().then((code) => {
830
+ process.exitCode = code;
831
+ });
832
+ }
833
+ export {
834
+ runCli
835
+ };
836
+ //# sourceMappingURL=cli.js.map