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.
- package/README.md +87 -0
- package/bin/astroshot.mjs +71 -0
- package/node_modules/@archastro/astroshot/LICENSE +21 -0
- package/node_modules/@archastro/astroshot/README.md +96 -0
- package/node_modules/@archastro/astroshot/bin/astroshot.mjs +276 -0
- package/node_modules/@archastro/astroshot/bin/demo.mjs +275 -0
- package/node_modules/@archastro/astroshot/bin/doctor.mjs +492 -0
- package/node_modules/@archastro/astroshot/bin/mac-preferences.mjs +346 -0
- package/node_modules/@archastro/astroshot/bin/templates.mjs +186 -0
- package/node_modules/@archastro/astroshot/fixtures/demo/fixtures.json +42 -0
- package/node_modules/@archastro/astroshot/fixtures/demo/journey.png +0 -0
- package/node_modules/@archastro/astroshot/fixtures/demo/journey.webm +0 -0
- package/node_modules/@archastro/astroshot/fixtures/demo/next-steps.png +0 -0
- package/node_modules/@archastro/astroshot/fixtures/demo/welcome.png +0 -0
- package/node_modules/@archastro/astroshot/ink.d.ts +5 -0
- package/node_modules/@archastro/astroshot/ink.js +1 -0
- package/node_modules/@archastro/astroshot/package.json +79 -0
- package/node_modules/@archastro/astroshot/pty.d.ts +8 -0
- package/node_modules/@archastro/astroshot/pty.js +1 -0
- package/node_modules/@archastro/astroshot/react.d.ts +1 -0
- package/node_modules/@archastro/astroshot/react.js +1 -0
- package/node_modules/@archastro/astroshot/tui.d.ts +1 -0
- package/node_modules/@archastro/astroshot/tui.js +1 -0
- package/node_modules/@archastro/movie-harness/README.md +138 -0
- package/node_modules/@archastro/movie-harness/bin/astroshot-movie.mjs +5 -0
- package/node_modules/@archastro/movie-harness/dist/cli.d.ts +1 -0
- package/node_modules/@archastro/movie-harness/dist/cli.js +389 -0
- package/node_modules/@archastro/movie-harness/dist/encode.d.ts +14 -0
- package/node_modules/@archastro/movie-harness/dist/encode.js +184 -0
- package/node_modules/@archastro/movie-harness/dist/index.d.ts +14 -0
- package/node_modules/@archastro/movie-harness/dist/index.js +11 -0
- package/node_modules/@archastro/movie-harness/dist/paths.d.ts +9 -0
- package/node_modules/@archastro/movie-harness/dist/paths.js +57 -0
- package/node_modules/@archastro/movie-harness/dist/png.d.ts +8 -0
- package/node_modules/@archastro/movie-harness/dist/png.js +79 -0
- package/node_modules/@archastro/movie-harness/dist/session.d.ts +45 -0
- package/node_modules/@archastro/movie-harness/dist/session.js +170 -0
- package/node_modules/@archastro/movie-harness/dist/sink.d.ts +7 -0
- package/node_modules/@archastro/movie-harness/dist/sink.js +113 -0
- package/node_modules/@archastro/movie-harness/dist/source-help.d.ts +29 -0
- package/node_modules/@archastro/movie-harness/dist/source-help.js +222 -0
- package/node_modules/@archastro/movie-harness/dist/sources/browser.d.ts +7 -0
- package/node_modules/@archastro/movie-harness/dist/sources/browser.js +87 -0
- package/node_modules/@archastro/movie-harness/dist/sources/desktop-macos.d.ts +110 -0
- package/node_modules/@archastro/movie-harness/dist/sources/desktop-macos.js +505 -0
- package/node_modules/@archastro/movie-harness/dist/sources/frames-store.d.ts +19 -0
- package/node_modules/@archastro/movie-harness/dist/sources/frames-store.js +165 -0
- package/node_modules/@archastro/movie-harness/dist/sources/pty.d.ts +15 -0
- package/node_modules/@archastro/movie-harness/dist/sources/pty.js +330 -0
- package/node_modules/@archastro/movie-harness/dist/terminal-paint.d.ts +27 -0
- package/node_modules/@archastro/movie-harness/dist/terminal-paint.js +190 -0
- package/node_modules/@archastro/movie-harness/dist/types.d.ts +140 -0
- package/node_modules/@archastro/movie-harness/dist/types.js +1 -0
- package/node_modules/@archastro/movie-harness/native/macos/WindowTools.swift +164 -0
- package/node_modules/@archastro/movie-harness/package.json +67 -0
- package/node_modules/@archastro/react-shot/LICENSE +21 -0
- package/node_modules/@archastro/react-shot/README.md +181 -0
- package/node_modules/@archastro/react-shot/bin/react-shot.mjs +2 -0
- package/node_modules/@archastro/react-shot/dist/batch-paths.d.ts +4 -0
- package/node_modules/@archastro/react-shot/dist/batch-paths.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/batch-paths.js +34 -0
- package/node_modules/@archastro/react-shot/dist/batch-paths.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/cli.d.ts +3 -0
- package/node_modules/@archastro/react-shot/dist/cli.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/cli.js +217 -0
- package/node_modules/@archastro/react-shot/dist/cli.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/config.d.ts +5 -0
- package/node_modules/@archastro/react-shot/dist/config.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/config.js +75 -0
- package/node_modules/@archastro/react-shot/dist/config.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/create-server.d.ts +13 -0
- package/node_modules/@archastro/react-shot/dist/create-server.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/create-server.js +254 -0
- package/node_modules/@archastro/react-shot/dist/create-server.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/index.d.ts +4 -0
- package/node_modules/@archastro/react-shot/dist/index.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/index.js +3 -0
- package/node_modules/@archastro/react-shot/dist/index.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/meta.d.ts +9 -0
- package/node_modules/@archastro/react-shot/dist/meta.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/meta.js +23 -0
- package/node_modules/@archastro/react-shot/dist/meta.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/shot.d.ts +4 -0
- package/node_modules/@archastro/react-shot/dist/shot.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/shot.js +259 -0
- package/node_modules/@archastro/react-shot/dist/shot.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/stubs.d.ts +6 -0
- package/node_modules/@archastro/react-shot/dist/stubs.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/stubs.js +49 -0
- package/node_modules/@archastro/react-shot/dist/stubs.js.map +1 -0
- package/node_modules/@archastro/react-shot/dist/types.d.ts +79 -0
- package/node_modules/@archastro/react-shot/dist/types.d.ts.map +1 -0
- package/node_modules/@archastro/react-shot/dist/types.js +2 -0
- package/node_modules/@archastro/react-shot/dist/types.js.map +1 -0
- package/node_modules/@archastro/react-shot/host/index.html +22 -0
- package/node_modules/@archastro/react-shot/package.json +81 -0
- package/node_modules/@archastro/tui-shot/LICENSE +21 -0
- package/node_modules/@archastro/tui-shot/README.md +145 -0
- package/node_modules/@archastro/tui-shot/bin/tui-shot.mjs +23 -0
- package/node_modules/@archastro/tui-shot/dist/batch-paths.d.ts +3 -0
- package/node_modules/@archastro/tui-shot/dist/batch-paths.js +33 -0
- package/node_modules/@archastro/tui-shot/dist/cli.d.ts +2 -0
- package/node_modules/@archastro/tui-shot/dist/cli.js +224 -0
- package/node_modules/@archastro/tui-shot/dist/index.d.ts +3 -0
- package/node_modules/@archastro/tui-shot/dist/index.js +2 -0
- package/node_modules/@archastro/tui-shot/dist/pty-exit-wrapper.d.ts +1 -0
- package/node_modules/@archastro/tui-shot/dist/pty-exit-wrapper.js +50 -0
- package/node_modules/@archastro/tui-shot/dist/pty-shot.d.ts +2 -0
- package/node_modules/@archastro/tui-shot/dist/pty-shot.js +483 -0
- package/node_modules/@archastro/tui-shot/dist/render-ink.d.ts +10 -0
- package/node_modules/@archastro/tui-shot/dist/render-ink.js +87 -0
- package/node_modules/@archastro/tui-shot/dist/shot.d.ts +24 -0
- package/node_modules/@archastro/tui-shot/dist/shot.js +250 -0
- package/node_modules/@archastro/tui-shot/dist/terminal-html.d.ts +14 -0
- package/node_modules/@archastro/tui-shot/dist/terminal-html.js +144 -0
- package/node_modules/@archastro/tui-shot/dist/types.d.ts +81 -0
- package/node_modules/@archastro/tui-shot/dist/types.js +1 -0
- package/node_modules/@archastro/tui-shot/package.json +90 -0
- 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,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,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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|