@archastro/astroshot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ArchAstro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # `@archastro/astroshot`
2
+
3
+ One command for deterministic React, Ink, and arbitrary terminal screenshots.
4
+
5
+ ```bash
6
+ npx --@archastro:registry=https://registry.npmjs.org \
7
+ @archastro/astroshot init react
8
+ npx --@archastro:registry=https://registry.npmjs.org \
9
+ @archastro/astroshot init ink
10
+ npx --@archastro:registry=https://registry.npmjs.org \
11
+ @archastro/astroshot init pty
12
+
13
+ npx --@archastro:registry=https://registry.npmjs.org \
14
+ @archastro/astroshot react ./react.shot.tsx -o ./react.png
15
+ npx --@archastro:registry=https://registry.npmjs.org \
16
+ @archastro/astroshot ink ./ink.shot.tsx -o ./ink.png
17
+ npx --@archastro:registry=https://registry.npmjs.org \
18
+ @archastro/astroshot pty ./pty.shot.yaml -o ./terminal.png
19
+ ```
20
+
21
+ Use `react` for isolated browser components, `ink` for in-process Ink fixture
22
+ trees, and `pty` for executable terminal applications such as Ratatui, Bubble
23
+ Tea, Textual, and curses programs. `tui` remains an alias for `ink` for
24
+ compatibility.
25
+
26
+ Ink fixtures require project-local peers:
27
+
28
+ ```bash
29
+ npm install --save-dev ink@^7.1 react@^19
30
+ ```
31
+
32
+ Install the shared Chromium runtime once:
33
+
34
+ ```bash
35
+ npx --@archastro:registry=https://registry.npmjs.org \
36
+ @archastro/astroshot install-browser
37
+ ```
38
+
39
+ Use `react batch <manifest>` or `ink batch <manifest>` for maintained fixture
40
+ sets. Run `<mode> --help` for mode-specific options.
41
+
42
+ Fixture types are available from the unified package:
43
+
44
+ ```tsx
45
+ import type { ReactShotFixture } from "@archastro/astroshot/react";
46
+ import type { InkShotFixture } from "@archastro/astroshot/ink";
47
+ ```
48
+
49
+ PTY YAML and JSON files launch `command` directly, without a shell. Their
50
+ `actions` can wait for visible text, send named keys or literal text, and
51
+ pause for bounded durations. Paths in `cwd` are relative to the fixture.
52
+ On Windows, point `command` at an `.exe`; `.cmd` and `.bat` files are rejected
53
+ because running them would introduce an implicit shell.
54
+
55
+ Fixtures, imported modules, and PTY commands execute with the current user's
56
+ permissions. Review untrusted files before capture and never include
57
+ credentials or private customer data in fixtures or generated images.
@@ -0,0 +1,215 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawnSync } from "node:child_process";
4
+ import fs from "node:fs";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+
8
+ import { writeFixtureTemplate } from "./templates.mjs";
9
+
10
+ function help() {
11
+ console.log(`astroshot — one CLI for React, Ink, and PTY screenshots
12
+
13
+ Usage:
14
+ astroshot init react [fixture.tsx] [--force]
15
+ astroshot init ink [fixture.tsx] [--force]
16
+ astroshot init pty [fixture.yaml] [--force]
17
+ astroshot react <fixture.tsx> -o <out.png> [options]
18
+ astroshot react shot <fixture.tsx> -o <out.png> [options]
19
+ astroshot react batch <manifest.yaml|json> [options]
20
+ astroshot ink <fixture.tsx> -o <out.png> [options]
21
+ astroshot ink batch <manifest.yaml|json> [options]
22
+ astroshot pty <fixture.yaml|json> -o <out.png> [options]
23
+ astroshot install-browser [--with-deps]
24
+
25
+ Commands:
26
+ react Capture an isolated React component
27
+ ink Capture an Ink component fixture
28
+ pty Capture any executable in a pseudoterminal
29
+ init Generate a React, Ink, or PTY fixture template
30
+ install-browser Install the shared Chromium runtime
31
+
32
+ Compatibility: "astroshot tui" remains an alias for "astroshot ink".
33
+ Run "astroshot <mode> --help" for mode options.`);
34
+ }
35
+
36
+ function modeHelp(mode) {
37
+ if (mode === "react") {
38
+ console.log(`astroshot react — deterministic React component screenshots
39
+
40
+ Usage:
41
+ astroshot init react [fixture.tsx] [--force]
42
+ astroshot react <fixture.tsx> -o <out.png> [options]
43
+ astroshot react batch <manifest.yaml|json> [options]
44
+
45
+ Options:
46
+ -o, --out <path> Output PNG path
47
+ --root <dir> Package root for imports and aliases
48
+ --config <path> Fixture configuration path
49
+ --width <px> Override viewport width
50
+ --height <px> Override viewport height
51
+ --headed Show Chromium for debugging
52
+ -h, --help Show this help`);
53
+ return;
54
+ }
55
+
56
+ if (mode === "ink") {
57
+ console.log(`astroshot ink — deterministic Ink component screenshots
58
+
59
+ Usage:
60
+ astroshot init ink [fixture.tsx] [--force]
61
+ astroshot ink <fixture.tsx> -o <out.png> [options]
62
+ astroshot ink batch <manifest.yaml|json> [options]
63
+
64
+ Options:
65
+ -o, --out <path> Output PNG path
66
+ --cols <count> Override terminal columns
67
+ --rows <count> Override terminal rows
68
+ --scale <factor> Override PNG device scale factor
69
+ --out-dir <path> Override batch output directory
70
+ --headed Show Chromium for debugging
71
+ -h, --help Show this help`);
72
+ return;
73
+ }
74
+
75
+ console.log(`astroshot pty — screenshots of arbitrary terminal programs
76
+
77
+ Usage:
78
+ astroshot init pty [fixture.yaml] [--force]
79
+ astroshot pty <fixture.yaml|json> -o <out.png> [options]
80
+
81
+ Options:
82
+ -o, --out <path> Output PNG path
83
+ --cols <count> Override terminal columns
84
+ --rows <count> Override terminal rows
85
+ --scale <factor> Override PNG device scale factor
86
+ --headed Show Chromium for debugging
87
+ -h, --help Show this help`);
88
+ }
89
+
90
+ function engineBin(mode) {
91
+ const packageName =
92
+ mode === "react" ? "@archastro/react-shot" : "@archastro/tui-shot";
93
+ const executable = mode === "react" ? "react-shot.mjs" : "tui-shot.mjs";
94
+ const entry = fileURLToPath(import.meta.resolve(packageName));
95
+ const packageRoot = path.dirname(path.dirname(entry));
96
+ return path.join(packageRoot, "bin", executable);
97
+ }
98
+
99
+ function runEngine(mode, arguments_) {
100
+ const engineMode = mode === "react" ? "react" : "tui";
101
+ const engineArguments =
102
+ mode === "pty" ? ["pty", ...arguments_] : arguments_;
103
+ const normalized =
104
+ mode !== "pty" &&
105
+ engineArguments[0] &&
106
+ /\.[cm]?tsx?$/.test(engineArguments[0])
107
+ ? ["shot", ...engineArguments]
108
+ : engineArguments;
109
+ const result = spawnSync(
110
+ process.execPath,
111
+ [engineBin(engineMode), ...normalized],
112
+ { stdio: "inherit" },
113
+ );
114
+ if (result.error) {
115
+ console.error(`astroshot could not start ${mode} capture: ${result.error.message}`);
116
+ process.exit(1);
117
+ }
118
+ if (result.signal) process.kill(process.pid, result.signal);
119
+ process.exit(result.status ?? 1);
120
+ }
121
+
122
+ function initHelp() {
123
+ console.log(`astroshot init — generate a fixture template
124
+
125
+ Usage:
126
+ astroshot init react [fixture.tsx] [--force]
127
+ astroshot init ink [fixture.tsx] [--force]
128
+ astroshot init pty [fixture.yaml] [--force]
129
+
130
+ Defaults:
131
+ react.shot.tsx
132
+ ink.shot.tsx
133
+ pty.shot.yaml
134
+
135
+ Existing files are never replaced unless --force is passed.`);
136
+ }
137
+
138
+ function runInit(arguments_) {
139
+ if (
140
+ arguments_.length === 0 ||
141
+ arguments_[0] === "help" ||
142
+ arguments_[0] === "-h" ||
143
+ arguments_[0] === "--help"
144
+ ) {
145
+ initHelp();
146
+ process.exit(arguments_.length === 0 ? 1 : 0);
147
+ }
148
+
149
+ const mode = arguments_[0];
150
+ const rest = arguments_.slice(1);
151
+ const force = rest.includes("--force") || rest.includes("-f");
152
+ const unknownFlags = rest.filter(
153
+ (value) => value.startsWith("-") && value !== "--force" && value !== "-f",
154
+ );
155
+ if (unknownFlags.length) {
156
+ throw new Error(`Unknown init flag: ${unknownFlags[0]}`);
157
+ }
158
+ const positionals = rest.filter((value) => !value.startsWith("-"));
159
+ if (positionals.length > 1) {
160
+ throw new Error("init accepts at most one output path");
161
+ }
162
+ const result = writeFixtureTemplate({
163
+ mode,
164
+ outputPath: positionals[0],
165
+ force,
166
+ });
167
+ console.log(`Created ${result.label} fixture: ${result.absolutePath}`);
168
+ }
169
+
170
+ const [command, ...arguments_] = process.argv.slice(2);
171
+
172
+ if (command === "-v" || command === "--version") {
173
+ const packageJSON = JSON.parse(
174
+ fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
175
+ );
176
+ console.log(packageJSON.version);
177
+ process.exit(0);
178
+ }
179
+
180
+ if (!command || command === "help" || command === "-h" || command === "--help") {
181
+ help();
182
+ process.exit(command ? 0 : 1);
183
+ }
184
+
185
+ if (command === "install-browser") {
186
+ runEngine("react", ["install-browser", ...arguments_]);
187
+ }
188
+
189
+ if (command === "init") {
190
+ try {
191
+ runInit(arguments_);
192
+ } catch (error) {
193
+ console.error(error instanceof Error ? error.message : error);
194
+ process.exit(1);
195
+ }
196
+ process.exit(0);
197
+ }
198
+
199
+ if (command === "react" || command === "ink" || command === "tui" || command === "pty") {
200
+ const canonicalMode = command === "tui" ? "ink" : command;
201
+ if (
202
+ arguments_.length === 0 ||
203
+ arguments_[0] === "help" ||
204
+ arguments_[0] === "-h" ||
205
+ arguments_[0] === "--help"
206
+ ) {
207
+ modeHelp(canonicalMode);
208
+ process.exit(arguments_.length === 0 ? 1 : 0);
209
+ }
210
+ runEngine(canonicalMode, arguments_);
211
+ }
212
+
213
+ console.error(`Unknown command: ${command}`);
214
+ help();
215
+ process.exit(1);
@@ -0,0 +1,186 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const REACT_TEMPLATE = `import type { ReactShotFixture } from "@archastro/astroshot/react";
5
+
6
+ function ExampleCard() {
7
+ return (
8
+ <article
9
+ data-astroshot
10
+ style={{
11
+ width: 420,
12
+ padding: 24,
13
+ borderRadius: 16,
14
+ background: "#f8fafc",
15
+ color: "#0f172a",
16
+ fontFamily: "system-ui, sans-serif",
17
+ boxShadow: "0 20px 50px rgba(15, 23, 42, 0.16)",
18
+ }}
19
+ >
20
+ <h1 style={{ margin: "0 0 8px", fontSize: 24 }}>React fixture</h1>
21
+ <p style={{ margin: 0 }}>Replace this component with the state you want to capture.</p>
22
+ </article>
23
+ );
24
+ }
25
+
26
+ export default {
27
+ width: 720,
28
+ height: 420,
29
+ background: "#e2e8f0",
30
+ selector: "[data-astroshot]",
31
+ waitFor: "text=React fixture",
32
+ component: <ExampleCard />,
33
+ } satisfies ReactShotFixture;
34
+ `;
35
+
36
+ const INK_TEMPLATE = `import React from "react";
37
+ import { Box, Text } from "ink";
38
+ import type { InkShotFixture } from "@archastro/astroshot/ink";
39
+
40
+ export default {
41
+ cols: 64,
42
+ rows: 12,
43
+ expectText: ["Ink fixture", "Ready to capture"],
44
+ component: (
45
+ <Box borderStyle="round" borderColor="cyan" paddingX={1} flexDirection="column">
46
+ <Text bold color="cyan">Ink fixture</Text>
47
+ <Text>Ready to capture</Text>
48
+ </Box>
49
+ ),
50
+ } satisfies InkShotFixture;
51
+ `;
52
+
53
+ const PTY_TEMPLATE = `version: 1
54
+ command: ./target/debug/my-tui
55
+ args: []
56
+ cwd: .
57
+ cols: 100
58
+ rows: 30
59
+ timeoutMs: 15000
60
+ actions:
61
+ - waitFor: "Choose an option"
62
+ - key: down
63
+ - key: enter
64
+ - waitFor: "Ready"
65
+ expectText:
66
+ - "Ready"
67
+ `;
68
+
69
+ const PTY_JSON_TEMPLATE = `${JSON.stringify(
70
+ {
71
+ version: 1,
72
+ command: "./target/debug/my-tui",
73
+ args: [],
74
+ cwd: ".",
75
+ cols: 100,
76
+ rows: 30,
77
+ timeoutMs: 15000,
78
+ actions: [
79
+ { waitFor: "Choose an option" },
80
+ { key: "down" },
81
+ { key: "enter" },
82
+ { waitFor: "Ready" },
83
+ ],
84
+ expectText: ["Ready"],
85
+ },
86
+ null,
87
+ 2,
88
+ )}\n`;
89
+
90
+ const TEMPLATES = {
91
+ react: {
92
+ defaultPath: "react.shot.tsx",
93
+ label: "React",
94
+ source: REACT_TEMPLATE,
95
+ },
96
+ ink: {
97
+ defaultPath: "ink.shot.tsx",
98
+ label: "Ink",
99
+ source: INK_TEMPLATE,
100
+ },
101
+ pty: {
102
+ defaultPath: "pty.shot.yaml",
103
+ label: "PTY",
104
+ source: PTY_TEMPLATE,
105
+ },
106
+ };
107
+
108
+ export function canonicalTemplateMode(mode) {
109
+ return mode === "tui" ? "ink" : mode;
110
+ }
111
+
112
+ export function writeFixtureTemplate({
113
+ mode,
114
+ outputPath,
115
+ force = false,
116
+ cwd = process.cwd(),
117
+ }) {
118
+ const canonicalMode = canonicalTemplateMode(mode);
119
+ const template = TEMPLATES[canonicalMode];
120
+ if (!template) {
121
+ throw new Error("init requires one of: react, ink, pty");
122
+ }
123
+
124
+ const requestedPath = outputPath ?? template.defaultPath;
125
+ const absolutePath = path.resolve(cwd, requestedPath);
126
+ const expectedExtension = canonicalMode === "pty" ? /\.(?:ya?ml|json)$/i : /\.tsx$/i;
127
+ if (!expectedExtension.test(absolutePath)) {
128
+ throw new Error(
129
+ canonicalMode === "pty"
130
+ ? "PTY fixture templates must use .yaml, .yml, or .json"
131
+ : `${template.label} fixture templates must use .tsx`,
132
+ );
133
+ }
134
+
135
+ const parentDirectory = path.dirname(absolutePath);
136
+ fs.mkdirSync(parentDirectory, { recursive: true });
137
+ const source =
138
+ canonicalMode === "pty" && path.extname(absolutePath).toLowerCase() === ".json"
139
+ ? PTY_JSON_TEMPLATE
140
+ : template.source;
141
+ if (!force) {
142
+ try {
143
+ fs.writeFileSync(absolutePath, source, {
144
+ encoding: "utf8",
145
+ flag: "wx",
146
+ });
147
+ } catch (error) {
148
+ if (error?.code === "EEXIST") {
149
+ throw new Error(`Refusing to overwrite ${absolutePath}; pass --force to replace it`);
150
+ }
151
+ throw error;
152
+ }
153
+ } else {
154
+ let existing;
155
+ try {
156
+ existing = fs.lstatSync(absolutePath);
157
+ } catch (error) {
158
+ if (error?.code !== "ENOENT") throw error;
159
+ }
160
+ if (existing?.isSymbolicLink()) {
161
+ throw new Error(`Refusing to replace symbolic link ${absolutePath}`);
162
+ }
163
+ if (existing?.isDirectory()) {
164
+ throw new Error(`Refusing to replace directory ${absolutePath}`);
165
+ }
166
+ const temporaryPath = path.join(
167
+ parentDirectory,
168
+ `.${path.basename(absolutePath)}.${process.pid}.${Date.now()}.tmp`,
169
+ );
170
+ try {
171
+ fs.writeFileSync(temporaryPath, source, {
172
+ encoding: "utf8",
173
+ flag: "wx",
174
+ });
175
+ fs.renameSync(temporaryPath, absolutePath);
176
+ } finally {
177
+ fs.rmSync(temporaryPath, { force: true });
178
+ }
179
+ }
180
+
181
+ return {
182
+ absolutePath,
183
+ label: template.label,
184
+ mode: canonicalMode,
185
+ };
186
+ }
package/ink.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "@archastro/tui-shot";
2
+ export type {
3
+ TuiShotFixture as InkShotFixture,
4
+ TuiShotRequest as InkShotRequest,
5
+ } from "@archastro/tui-shot";
package/ink.js ADDED
@@ -0,0 +1 @@
1
+ export * from "@archastro/tui-shot";
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@archastro/astroshot",
3
+ "version": "0.1.0",
4
+ "description": "One CLI for deterministic React and terminal UI screenshots",
5
+ "keywords": [
6
+ "react",
7
+ "ink",
8
+ "pty",
9
+ "ratatui",
10
+ "terminal",
11
+ "screenshot",
12
+ "visual-testing"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/ArchAstro/astroshots.git",
18
+ "directory": "packages/astroshot"
19
+ },
20
+ "homepage": "https://github.com/ArchAstro/astroshots#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/ArchAstro/astroshots/issues"
23
+ },
24
+ "type": "module",
25
+ "bin": {
26
+ "astroshot": "bin/astroshot.mjs"
27
+ },
28
+ "exports": {
29
+ "./react": {
30
+ "types": "./react.d.ts",
31
+ "import": "./react.js"
32
+ },
33
+ "./ink": {
34
+ "types": "./ink.d.ts",
35
+ "import": "./ink.js"
36
+ },
37
+ "./pty": {
38
+ "types": "./pty.d.ts",
39
+ "import": "./pty.js"
40
+ },
41
+ "./tui": {
42
+ "types": "./tui.d.ts",
43
+ "import": "./tui.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "bin",
48
+ "react.d.ts",
49
+ "react.js",
50
+ "ink.d.ts",
51
+ "ink.js",
52
+ "pty.d.ts",
53
+ "pty.js",
54
+ "tui.d.ts",
55
+ "tui.js",
56
+ "README.md"
57
+ ],
58
+ "scripts": {
59
+ "pretest": "npm run build --workspace @archastro/react-shot && npm run build --workspace @archastro/tui-shot",
60
+ "test": "node --test test/*.test.mjs"
61
+ },
62
+ "engines": {
63
+ "node": ">=22.14.0"
64
+ },
65
+ "publishConfig": {
66
+ "access": "public",
67
+ "provenance": true,
68
+ "registry": "https://registry.npmjs.org/"
69
+ },
70
+ "dependencies": {
71
+ "@archastro/react-shot": "0.1.0",
72
+ "@archastro/tui-shot": "0.1.0"
73
+ }
74
+ }
package/pty.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export {
2
+ closeSharedBrowser,
3
+ takePtyShot,
4
+ type PtyAction,
5
+ type PtyKey,
6
+ type PtyShotFixture,
7
+ type PtyShotRequest,
8
+ } from "@archastro/tui-shot";
package/pty.js ADDED
@@ -0,0 +1 @@
1
+ export { closeSharedBrowser, takePtyShot } from "@archastro/tui-shot";
package/react.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "@archastro/react-shot";
package/react.js ADDED
@@ -0,0 +1 @@
1
+ export * from "@archastro/react-shot";
package/tui.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "@archastro/tui-shot";
package/tui.js ADDED
@@ -0,0 +1 @@
1
+ export * from "@archastro/tui-shot";