astroshot 0.2.1

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 (119) hide show
  1. package/README.md +87 -0
  2. package/bin/astroshot.mjs +71 -0
  3. package/node_modules/@archastro/astroshot/LICENSE +21 -0
  4. package/node_modules/@archastro/astroshot/README.md +96 -0
  5. package/node_modules/@archastro/astroshot/bin/astroshot.mjs +276 -0
  6. package/node_modules/@archastro/astroshot/bin/demo.mjs +275 -0
  7. package/node_modules/@archastro/astroshot/bin/doctor.mjs +492 -0
  8. package/node_modules/@archastro/astroshot/bin/mac-preferences.mjs +346 -0
  9. package/node_modules/@archastro/astroshot/bin/templates.mjs +186 -0
  10. package/node_modules/@archastro/astroshot/fixtures/demo/fixtures.json +42 -0
  11. package/node_modules/@archastro/astroshot/fixtures/demo/journey.png +0 -0
  12. package/node_modules/@archastro/astroshot/fixtures/demo/journey.webm +0 -0
  13. package/node_modules/@archastro/astroshot/fixtures/demo/next-steps.png +0 -0
  14. package/node_modules/@archastro/astroshot/fixtures/demo/welcome.png +0 -0
  15. package/node_modules/@archastro/astroshot/ink.d.ts +5 -0
  16. package/node_modules/@archastro/astroshot/ink.js +1 -0
  17. package/node_modules/@archastro/astroshot/package.json +79 -0
  18. package/node_modules/@archastro/astroshot/pty.d.ts +8 -0
  19. package/node_modules/@archastro/astroshot/pty.js +1 -0
  20. package/node_modules/@archastro/astroshot/react.d.ts +1 -0
  21. package/node_modules/@archastro/astroshot/react.js +1 -0
  22. package/node_modules/@archastro/astroshot/tui.d.ts +1 -0
  23. package/node_modules/@archastro/astroshot/tui.js +1 -0
  24. package/node_modules/@archastro/movie-harness/README.md +138 -0
  25. package/node_modules/@archastro/movie-harness/bin/astroshot-movie.mjs +5 -0
  26. package/node_modules/@archastro/movie-harness/dist/cli.d.ts +1 -0
  27. package/node_modules/@archastro/movie-harness/dist/cli.js +389 -0
  28. package/node_modules/@archastro/movie-harness/dist/encode.d.ts +14 -0
  29. package/node_modules/@archastro/movie-harness/dist/encode.js +184 -0
  30. package/node_modules/@archastro/movie-harness/dist/index.d.ts +14 -0
  31. package/node_modules/@archastro/movie-harness/dist/index.js +11 -0
  32. package/node_modules/@archastro/movie-harness/dist/paths.d.ts +9 -0
  33. package/node_modules/@archastro/movie-harness/dist/paths.js +57 -0
  34. package/node_modules/@archastro/movie-harness/dist/png.d.ts +8 -0
  35. package/node_modules/@archastro/movie-harness/dist/png.js +79 -0
  36. package/node_modules/@archastro/movie-harness/dist/session.d.ts +45 -0
  37. package/node_modules/@archastro/movie-harness/dist/session.js +170 -0
  38. package/node_modules/@archastro/movie-harness/dist/sink.d.ts +7 -0
  39. package/node_modules/@archastro/movie-harness/dist/sink.js +113 -0
  40. package/node_modules/@archastro/movie-harness/dist/source-help.d.ts +29 -0
  41. package/node_modules/@archastro/movie-harness/dist/source-help.js +222 -0
  42. package/node_modules/@archastro/movie-harness/dist/sources/browser.d.ts +7 -0
  43. package/node_modules/@archastro/movie-harness/dist/sources/browser.js +87 -0
  44. package/node_modules/@archastro/movie-harness/dist/sources/desktop-macos.d.ts +110 -0
  45. package/node_modules/@archastro/movie-harness/dist/sources/desktop-macos.js +505 -0
  46. package/node_modules/@archastro/movie-harness/dist/sources/frames-store.d.ts +19 -0
  47. package/node_modules/@archastro/movie-harness/dist/sources/frames-store.js +165 -0
  48. package/node_modules/@archastro/movie-harness/dist/sources/pty.d.ts +15 -0
  49. package/node_modules/@archastro/movie-harness/dist/sources/pty.js +330 -0
  50. package/node_modules/@archastro/movie-harness/dist/terminal-paint.d.ts +27 -0
  51. package/node_modules/@archastro/movie-harness/dist/terminal-paint.js +190 -0
  52. package/node_modules/@archastro/movie-harness/dist/types.d.ts +140 -0
  53. package/node_modules/@archastro/movie-harness/dist/types.js +1 -0
  54. package/node_modules/@archastro/movie-harness/native/macos/WindowTools.swift +164 -0
  55. package/node_modules/@archastro/movie-harness/package.json +67 -0
  56. package/node_modules/@archastro/react-shot/LICENSE +21 -0
  57. package/node_modules/@archastro/react-shot/README.md +181 -0
  58. package/node_modules/@archastro/react-shot/bin/react-shot.mjs +2 -0
  59. package/node_modules/@archastro/react-shot/dist/batch-paths.d.ts +4 -0
  60. package/node_modules/@archastro/react-shot/dist/batch-paths.d.ts.map +1 -0
  61. package/node_modules/@archastro/react-shot/dist/batch-paths.js +34 -0
  62. package/node_modules/@archastro/react-shot/dist/batch-paths.js.map +1 -0
  63. package/node_modules/@archastro/react-shot/dist/cli.d.ts +3 -0
  64. package/node_modules/@archastro/react-shot/dist/cli.d.ts.map +1 -0
  65. package/node_modules/@archastro/react-shot/dist/cli.js +217 -0
  66. package/node_modules/@archastro/react-shot/dist/cli.js.map +1 -0
  67. package/node_modules/@archastro/react-shot/dist/config.d.ts +5 -0
  68. package/node_modules/@archastro/react-shot/dist/config.d.ts.map +1 -0
  69. package/node_modules/@archastro/react-shot/dist/config.js +75 -0
  70. package/node_modules/@archastro/react-shot/dist/config.js.map +1 -0
  71. package/node_modules/@archastro/react-shot/dist/create-server.d.ts +13 -0
  72. package/node_modules/@archastro/react-shot/dist/create-server.d.ts.map +1 -0
  73. package/node_modules/@archastro/react-shot/dist/create-server.js +254 -0
  74. package/node_modules/@archastro/react-shot/dist/create-server.js.map +1 -0
  75. package/node_modules/@archastro/react-shot/dist/index.d.ts +4 -0
  76. package/node_modules/@archastro/react-shot/dist/index.d.ts.map +1 -0
  77. package/node_modules/@archastro/react-shot/dist/index.js +3 -0
  78. package/node_modules/@archastro/react-shot/dist/index.js.map +1 -0
  79. package/node_modules/@archastro/react-shot/dist/meta.d.ts +9 -0
  80. package/node_modules/@archastro/react-shot/dist/meta.d.ts.map +1 -0
  81. package/node_modules/@archastro/react-shot/dist/meta.js +23 -0
  82. package/node_modules/@archastro/react-shot/dist/meta.js.map +1 -0
  83. package/node_modules/@archastro/react-shot/dist/shot.d.ts +4 -0
  84. package/node_modules/@archastro/react-shot/dist/shot.d.ts.map +1 -0
  85. package/node_modules/@archastro/react-shot/dist/shot.js +259 -0
  86. package/node_modules/@archastro/react-shot/dist/shot.js.map +1 -0
  87. package/node_modules/@archastro/react-shot/dist/stubs.d.ts +6 -0
  88. package/node_modules/@archastro/react-shot/dist/stubs.d.ts.map +1 -0
  89. package/node_modules/@archastro/react-shot/dist/stubs.js +49 -0
  90. package/node_modules/@archastro/react-shot/dist/stubs.js.map +1 -0
  91. package/node_modules/@archastro/react-shot/dist/types.d.ts +79 -0
  92. package/node_modules/@archastro/react-shot/dist/types.d.ts.map +1 -0
  93. package/node_modules/@archastro/react-shot/dist/types.js +2 -0
  94. package/node_modules/@archastro/react-shot/dist/types.js.map +1 -0
  95. package/node_modules/@archastro/react-shot/host/index.html +22 -0
  96. package/node_modules/@archastro/react-shot/package.json +81 -0
  97. package/node_modules/@archastro/tui-shot/LICENSE +21 -0
  98. package/node_modules/@archastro/tui-shot/README.md +145 -0
  99. package/node_modules/@archastro/tui-shot/bin/tui-shot.mjs +23 -0
  100. package/node_modules/@archastro/tui-shot/dist/batch-paths.d.ts +3 -0
  101. package/node_modules/@archastro/tui-shot/dist/batch-paths.js +33 -0
  102. package/node_modules/@archastro/tui-shot/dist/cli.d.ts +2 -0
  103. package/node_modules/@archastro/tui-shot/dist/cli.js +224 -0
  104. package/node_modules/@archastro/tui-shot/dist/index.d.ts +3 -0
  105. package/node_modules/@archastro/tui-shot/dist/index.js +2 -0
  106. package/node_modules/@archastro/tui-shot/dist/pty-exit-wrapper.d.ts +1 -0
  107. package/node_modules/@archastro/tui-shot/dist/pty-exit-wrapper.js +50 -0
  108. package/node_modules/@archastro/tui-shot/dist/pty-shot.d.ts +2 -0
  109. package/node_modules/@archastro/tui-shot/dist/pty-shot.js +483 -0
  110. package/node_modules/@archastro/tui-shot/dist/render-ink.d.ts +10 -0
  111. package/node_modules/@archastro/tui-shot/dist/render-ink.js +87 -0
  112. package/node_modules/@archastro/tui-shot/dist/shot.d.ts +24 -0
  113. package/node_modules/@archastro/tui-shot/dist/shot.js +250 -0
  114. package/node_modules/@archastro/tui-shot/dist/terminal-html.d.ts +14 -0
  115. package/node_modules/@archastro/tui-shot/dist/terminal-html.js +144 -0
  116. package/node_modules/@archastro/tui-shot/dist/types.d.ts +81 -0
  117. package/node_modules/@archastro/tui-shot/dist/types.js +1 -0
  118. package/node_modules/@archastro/tui-shot/package.json +90 -0
  119. package/package.json +68 -0
@@ -0,0 +1,145 @@
1
+ # @archastro/tui-shot
2
+
3
+ The terminal rendering engine behind `@archastro/astroshot`. It supports two
4
+ capture boundaries:
5
+
6
+ - Ink fixture trees rendered in-process and interpreted with xterm.
7
+ - Arbitrary executables launched in a real pseudoterminal, driven by scripted
8
+ input, interpreted with the same xterm model, and captured in Chromium.
9
+
10
+ Use the unified `@archastro/astroshot` package for command-line capture.
11
+
12
+ ## Quick start
13
+
14
+ Run from a Node.js 22.14 or newer project. Install the project-local peers only
15
+ when using Ink mode:
16
+
17
+ ```bash
18
+ npm install --save-dev ink@^7.1 react@^19
19
+ npx astroshot install-browser
20
+ npx astroshot init ink
21
+ npx astroshot init pty
22
+ ```
23
+
24
+ Then capture either boundary:
25
+
26
+ ```bash
27
+ npx astroshot ink ./ink.shot.tsx -o ./ink.png
28
+ npx astroshot pty ./pty.shot.yaml -o ./ratatui.png
29
+ ```
30
+
31
+ On Linux hosts missing Chromium's system libraries, install the browser with
32
+ `install-browser --with-deps`.
33
+
34
+ ## Ink fixture contract
35
+
36
+ ```tsx
37
+ import { Box, Text } from "ink";
38
+ import React from "react";
39
+ import type { InkShotFixture } from "@archastro/astroshot/ink";
40
+
41
+ export default {
42
+ cols: 80,
43
+ rows: 12,
44
+ background: "#090a12",
45
+ scale: 2,
46
+ expectText: ["Ready to deploy"],
47
+ component: (
48
+ <Box borderStyle="round">
49
+ <Text color="#b9a8ff">Ready to deploy</Text>
50
+ </Box>
51
+ ),
52
+ } satisfies InkShotFixture;
53
+ ```
54
+
55
+ `expectText` makes capture fail unless every listed string exists in the final
56
+ terminal screen. `--cols`, `--rows`, and `--scale` override fixture values.
57
+ Ink capture also supports `batch <manifest.yaml|json>`; manifest paths resolve
58
+ relative to the manifest.
59
+
60
+ ## PTY fixture contract
61
+
62
+ PTY fixtures work with any executable that behaves like a terminal program:
63
+
64
+ ```yaml
65
+ version: 1
66
+ command: ./target/debug/my-ratatui-app
67
+ args: []
68
+ cwd: .
69
+ cols: 100
70
+ rows: 30
71
+ timeoutMs: 15000
72
+ settleMs: 100
73
+ actions:
74
+ - waitFor: Choose an option
75
+ - key: down
76
+ - key: enter
77
+ - waitFor: Ready
78
+ expectText:
79
+ - Ready
80
+ ```
81
+
82
+ The child receives `TERM=xterm-256color`, a fixed terminal grid, and the
83
+ fixture environment. `command` is spawned directly—not through a shell—and
84
+ `cwd` resolves relative to the fixture. On Windows, use an `.exe`; `.cmd` and
85
+ `.bat` scripts are rejected because they require a shell. Supported actions
86
+ are:
87
+
88
+ - `waitFor` with an optional per-action `timeoutMs`;
89
+ - `waitForExit: true` with an optional `timeoutMs` when the documented program
90
+ is expected to terminate before capture;
91
+ - `key`: `enter`, arrows, `tab`, `escape`, `backspace`, `space`, `ctrl-c`, or
92
+ `ctrl-d`;
93
+ - `text` for literal input;
94
+ - `pauseMs` for a bounded delay.
95
+
96
+ Actions run in order. The final xterm screen must contain every `expectText`
97
+ value before Chromium captures it. Astroshot terminates a still-running child
98
+ after the screenshot.
99
+
100
+ A child that exits nonzero before capture fails by default, even when expected
101
+ text rendered. Set `allowNonZeroExit: true` only to document an intentional
102
+ failure state. Use a final `waitForExit: true` action when process completion is
103
+ part of the documented state; this waits for the authoritative child status
104
+ instead of relying on a fixed settle delay.
105
+
106
+ The renderer targets text and ANSI/VT terminal interfaces. Mouse events,
107
+ mid-journey resize actions, and terminal graphics protocols such as Sixel or
108
+ Kitty images are not currently modeled.
109
+
110
+ `node-pty` is an optional, lazily loaded native dependency so React and Ink
111
+ capture still work on an unsupported PTY host. This release pins the exact
112
+ official `1.2.0-beta.14` build; PTY capture reports an actionable error if its
113
+ native addon is unavailable.
114
+
115
+ ## Reproducibility and trust
116
+
117
+ Playwright is pinned so each release selects one Chromium version. PNG bytes
118
+ can still differ across operating systems because the default stage uses host
119
+ fonts. Set `fontFamily` and use a consistent OS/container for pixel baselines.
120
+
121
+ Ink fixture modules and PTY commands are executable code with the current
122
+ user's permissions. Only capture trusted repositories. A PTY is an I/O
123
+ boundary, not a security sandbox; use a container for untrusted programs.
124
+
125
+ ## Programmatic API
126
+
127
+ ```ts
128
+ import {
129
+ closeSharedBrowser,
130
+ takePtyShot,
131
+ takeTuiShot,
132
+ } from "@archastro/tui-shot";
133
+
134
+ await takeTuiShot({
135
+ fixturePath: "./screenshots/welcome.tsx",
136
+ outPath: "./screenshots/welcome.png",
137
+ });
138
+ await takePtyShot({
139
+ fixturePath: "./screenshots/ratatui.yaml",
140
+ outPath: "./screenshots/ratatui.png",
141
+ });
142
+ await closeSharedBrowser();
143
+ ```
144
+
145
+ Ink fixtures support Ink 7 and React 19 as peer dependencies.
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+
8
+ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
9
+ const cli = path.join(root, "dist", "cli.js");
10
+ const require = createRequire(import.meta.url);
11
+ const tsx = require.resolve("tsx/cli");
12
+ const result = spawnSync(process.execPath, [tsx, cli, ...process.argv.slice(2)], {
13
+ stdio: "inherit",
14
+ });
15
+
16
+ if (result.error) {
17
+ console.error(`tui-shot could not start: ${result.error.message}`);
18
+ process.exit(1);
19
+ }
20
+ if (result.signal) {
21
+ process.kill(process.pid, result.signal);
22
+ }
23
+ process.exit(result.status ?? 1);
@@ -0,0 +1,3 @@
1
+ import type { BatchEntry } from "./types.js";
2
+ /** Resolve batch destinations once, rejecting traversal and overwrite risks. */
3
+ export declare function resolveBatchOutputPaths(entries: BatchEntry[], manifestDir: string, outDir: string | null): string[];
@@ -0,0 +1,33 @@
1
+ import path from "node:path";
2
+ function collisionKey(filePath) {
3
+ // Reject case-only collisions on every platform so a manifest behaves the
4
+ // same on case-sensitive and case-insensitive filesystems.
5
+ return path.resolve(filePath).normalize("NFC").toLowerCase();
6
+ }
7
+ function safeRelativeOutput(output) {
8
+ const portable = output.replaceAll("\\", "/");
9
+ const normalized = path.posix.normalize(portable);
10
+ if (path.posix.isAbsolute(normalized) ||
11
+ path.win32.isAbsolute(output) ||
12
+ normalized === ".." ||
13
+ normalized.startsWith("../")) {
14
+ throw new Error(`Batch output must be a safe relative path when --out-dir is used: ${output}`);
15
+ }
16
+ return normalized;
17
+ }
18
+ /** Resolve batch destinations once, rejecting traversal and overwrite risks. */
19
+ export function resolveBatchOutputPaths(entries, manifestDir, outDir) {
20
+ const destinations = entries.map((entry) => outDir
21
+ ? path.resolve(outDir, safeRelativeOutput(entry.out))
22
+ : path.resolve(manifestDir, entry.out));
23
+ const seen = new Map();
24
+ for (const destination of destinations) {
25
+ const key = collisionKey(destination);
26
+ const previous = seen.get(key);
27
+ if (previous) {
28
+ throw new Error(`Batch outputs resolve to the same destination: ${previous} and ${destination}`);
29
+ }
30
+ seen.set(key, destination);
31
+ }
32
+ return destinations;
33
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { spawnSync } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import path from "node:path";
6
+ import YAML from "yaml";
7
+ import { resolveBatchOutputPaths } from "./batch-paths.js";
8
+ import { takePtyShot } from "./pty-shot.js";
9
+ import { closeSharedBrowser, takeTuiShot } from "./shot.js";
10
+ const localRequire = createRequire(import.meta.url);
11
+ function help() {
12
+ console.log(`tui-shot — deterministic PNG screenshots of terminal interfaces
13
+
14
+ Usage:
15
+ tui-shot install-browser [--with-deps]
16
+ tui-shot shot <fixture.tsx> -o <out.png> [options]
17
+ tui-shot pty <fixture.yaml|json> -o <out.png> [options]
18
+ tui-shot batch <manifest.yaml|json> [options]
19
+
20
+ Options:
21
+ -o, --out <path> Output PNG path
22
+ --cols <count> Override terminal columns
23
+ --rows <count> Override terminal rows
24
+ --scale <factor> Override PNG device scale factor
25
+ --out-dir <path> Override batch output directory
26
+ --headed Show Chromium while capturing
27
+ --with-deps Install Chromium system dependencies (Linux)
28
+ -h, --help Show this help
29
+ `);
30
+ }
31
+ function takeValue(args, flag) {
32
+ const value = args.shift();
33
+ if (!value || value.startsWith("-")) {
34
+ throw new Error(`${flag} requires a value`);
35
+ }
36
+ return value;
37
+ }
38
+ function parseArgs(argv) {
39
+ const args = [...argv];
40
+ const flags = {};
41
+ const positionals = [];
42
+ while (args.length) {
43
+ const value = args.shift();
44
+ if (value === "-h" || value === "--help")
45
+ flags.help = true;
46
+ else if (value === "--headed")
47
+ flags.headed = true;
48
+ else if (value === "--with-deps")
49
+ flags.withDeps = true;
50
+ else if (value === "-o" || value === "--out")
51
+ flags.out = takeValue(args, value);
52
+ else if (value === "--cols")
53
+ flags.cols = takeValue(args, value);
54
+ else if (value === "--rows")
55
+ flags.rows = takeValue(args, value);
56
+ else if (value === "--scale")
57
+ flags.scale = takeValue(args, value);
58
+ else if (value === "--out-dir")
59
+ flags.outDir = takeValue(args, value);
60
+ else if (value.startsWith("-"))
61
+ throw new Error(`Unknown flag: ${value}`);
62
+ else
63
+ positionals.push(value);
64
+ }
65
+ return { flags, positionals };
66
+ }
67
+ function numberFlag(flags, key, options) {
68
+ const raw = flags[key];
69
+ if (raw === undefined)
70
+ return undefined;
71
+ const value = Number(raw);
72
+ if (!Number.isFinite(value) ||
73
+ value <= 0 ||
74
+ value > options.maximum ||
75
+ (options.integer && !Number.isInteger(value))) {
76
+ const kind = options.integer ? "positive integer" : "positive number";
77
+ throw new Error(`--${key} must be a ${kind} no greater than ${options.maximum}`);
78
+ }
79
+ return value;
80
+ }
81
+ function captureOverrides(flags) {
82
+ return {
83
+ cols: numberFlag(flags, "cols", { integer: true, maximum: 1_000 }),
84
+ rows: numberFlag(flags, "rows", { integer: true, maximum: 1_000 }),
85
+ scale: numberFlag(flags, "scale", { maximum: 4 }),
86
+ };
87
+ }
88
+ function assertPngPath(outPath) {
89
+ if (path.extname(outPath).toLowerCase() !== ".png") {
90
+ throw new Error(`Output must use a .png extension: ${outPath}`);
91
+ }
92
+ }
93
+ function installBrowser(withDeps) {
94
+ const playwrightRoot = path.dirname(localRequire.resolve("playwright/package.json"));
95
+ const result = spawnSync(process.execPath, [
96
+ path.join(playwrightRoot, "cli.js"),
97
+ "install",
98
+ ...(withDeps ? ["--with-deps"] : []),
99
+ "chromium",
100
+ ], { stdio: "inherit" });
101
+ if (result.error) {
102
+ throw new Error(`Could not start Playwright installer: ${result.error.message}`);
103
+ }
104
+ if (result.status !== 0) {
105
+ throw new Error(`Playwright browser installation exited with status ${result.status}`);
106
+ }
107
+ }
108
+ async function shot(fixturePath, flags) {
109
+ const outPath = flags.out;
110
+ if (typeof outPath !== "string") {
111
+ throw new Error("shot requires -o <out.png>");
112
+ }
113
+ assertPngPath(outPath);
114
+ const written = await takeTuiShot({
115
+ fixturePath,
116
+ outPath,
117
+ headed: Boolean(flags.headed),
118
+ ...captureOverrides(flags),
119
+ });
120
+ console.log(`wrote ${written}`);
121
+ }
122
+ async function pty(fixturePath, flags) {
123
+ const outPath = flags.out;
124
+ if (typeof outPath !== "string") {
125
+ throw new Error("pty requires -o <out.png>");
126
+ }
127
+ assertPngPath(outPath);
128
+ const written = await takePtyShot({
129
+ fixturePath,
130
+ outPath,
131
+ headed: Boolean(flags.headed),
132
+ ...captureOverrides(flags),
133
+ });
134
+ console.log(`wrote ${written}`);
135
+ }
136
+ function parseManifest(absolute) {
137
+ const raw = fs.readFileSync(absolute, "utf8");
138
+ const value = absolute.endsWith(".json")
139
+ ? JSON.parse(raw)
140
+ : YAML.parse(raw);
141
+ if (!value || typeof value !== "object" || !Array.isArray(value.shots)) {
142
+ throw new Error(`No shots listed in ${absolute}`);
143
+ }
144
+ const manifest = value;
145
+ if (manifest.shots.length === 0 ||
146
+ manifest.shots.some((entry) => !entry ||
147
+ typeof entry.fixture !== "string" ||
148
+ !entry.fixture ||
149
+ typeof entry.out !== "string" ||
150
+ !entry.out)) {
151
+ throw new Error(`Every shot in ${absolute} needs fixture and out paths`);
152
+ }
153
+ return manifest;
154
+ }
155
+ async function batch(manifestPath, flags) {
156
+ const absolute = path.resolve(manifestPath);
157
+ const manifest = parseManifest(absolute);
158
+ const base = path.dirname(absolute);
159
+ const outDir = typeof flags.outDir === "string" ? path.resolve(flags.outDir) : null;
160
+ const outPaths = resolveBatchOutputPaths(manifest.shots, base, outDir);
161
+ for (const outPath of outPaths)
162
+ assertPngPath(outPath);
163
+ const overrides = captureOverrides(flags);
164
+ let completed = 0;
165
+ for (const [index, entry] of manifest.shots.entries()) {
166
+ const fixturePath = path.resolve(base, entry.fixture);
167
+ const outPath = outPaths[index];
168
+ process.stdout.write(`shot ${path.relative(process.cwd(), fixturePath)} … `);
169
+ await takeTuiShot({
170
+ fixturePath,
171
+ outPath,
172
+ headed: Boolean(flags.headed),
173
+ ...overrides,
174
+ });
175
+ console.log(`→ ${path.relative(process.cwd(), outPath)}`);
176
+ completed++;
177
+ }
178
+ console.log(`done: ${completed}/${manifest.shots.length} shots`);
179
+ }
180
+ async function main() {
181
+ const argv = process.argv.slice(2);
182
+ const command = argv[0];
183
+ if (!command || command === "help" || command === "-h" || command === "--help") {
184
+ help();
185
+ if (!command)
186
+ process.exitCode = 1;
187
+ return;
188
+ }
189
+ const { flags, positionals } = parseArgs(argv.slice(1));
190
+ if (flags.help) {
191
+ help();
192
+ return;
193
+ }
194
+ if (command === "install-browser") {
195
+ if (positionals.length) {
196
+ throw new Error("install-browser does not accept arguments");
197
+ }
198
+ installBrowser(Boolean(flags.withDeps));
199
+ }
200
+ else if (command === "shot") {
201
+ if (!positionals[0])
202
+ throw new Error("shot requires a fixture path");
203
+ await shot(positionals[0], flags);
204
+ }
205
+ else if (command === "pty") {
206
+ if (!positionals[0])
207
+ throw new Error("pty requires a fixture path");
208
+ await pty(positionals[0], flags);
209
+ }
210
+ else if (command === "batch") {
211
+ if (!positionals[0])
212
+ throw new Error("batch requires a manifest path");
213
+ await batch(positionals[0], flags);
214
+ }
215
+ else {
216
+ throw new Error(`Unknown command: ${command}`);
217
+ }
218
+ }
219
+ main()
220
+ .catch((error) => {
221
+ console.error(error instanceof Error ? error.message : error);
222
+ process.exitCode = 1;
223
+ })
224
+ .finally(closeSharedBrowser);
@@ -0,0 +1,3 @@
1
+ export { closeSharedBrowser, takeTuiShot } from "./shot.js";
2
+ export { takePtyShot } from "./pty-shot.js";
3
+ export type { BatchEntry, BatchManifest, PtyAction, PtyKey, PtyShotFixture, PtyShotRequest, TuiShotFixture, TuiShotRequest, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ export { closeSharedBrowser, takeTuiShot } from "./shot.js";
2
+ export { takePtyShot } from "./pty-shot.js";
@@ -0,0 +1,50 @@
1
+ import { spawn } from "node:child_process";
2
+ import fs from "node:fs";
3
+ const [, , token, statusPath, command, ...args] = process.argv;
4
+ if (!token || !statusPath || !command) {
5
+ process.stderr.write("Astroshot PTY status wrapper requires a token, status path, and command.\n");
6
+ process.exit(2);
7
+ }
8
+ const child = spawn(command, args, {
9
+ cwd: process.cwd(),
10
+ env: process.env,
11
+ stdio: "inherit",
12
+ windowsHide: true,
13
+ });
14
+ let reported = false;
15
+ // Terminal-generated signals are delivered to the PTY process group. Keep the
16
+ // bridge alive so the target's exit callback can report its authoritative code.
17
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
18
+ process.on(signal, () => { });
19
+ }
20
+ function reportExit(code) {
21
+ if (reported)
22
+ return;
23
+ reported = true;
24
+ const temporaryStatusPath = `${statusPath}.${process.pid}.tmp`;
25
+ try {
26
+ fs.writeFileSync(temporaryStatusPath, String(code), { mode: 0o600 });
27
+ fs.renameSync(temporaryStatusPath, statusPath);
28
+ }
29
+ catch (error) {
30
+ process.stderr.write(`Unable to write PTY program exit status: ${error instanceof Error ? error.message : String(error)}\n`);
31
+ }
32
+ const marker = `\x1b]777;astroshot-exit-${token}=${code}\x07`;
33
+ const writeMarker = () => {
34
+ process.stdout.write(marker, () => process.exit(0));
35
+ };
36
+ const markerDelay = Number(process.env.ASTROSHOT_TEST_DELAY_PTY_EXIT_MARKER_MS ?? 0);
37
+ if (Number.isFinite(markerDelay) && markerDelay > 0) {
38
+ setTimeout(writeMarker, markerDelay);
39
+ }
40
+ else {
41
+ writeMarker();
42
+ }
43
+ }
44
+ child.once("error", (error) => {
45
+ process.stderr.write(`Unable to launch PTY program: ${error.message}\n`);
46
+ reportExit(127);
47
+ });
48
+ child.once("exit", (code) => {
49
+ reportExit(code ?? 1);
50
+ });
@@ -0,0 +1,2 @@
1
+ import type { PtyShotRequest } from "./types.js";
2
+ export declare function takePtyShot(request: PtyShotRequest): Promise<string>;