@archastro/tui-shot 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 +21 -0
- package/README.md +147 -0
- package/bin/tui-shot.mjs +23 -0
- package/dist/batch-paths.d.ts +3 -0
- package/dist/batch-paths.js +33 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +224 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/pty-exit-wrapper.d.ts +1 -0
- package/dist/pty-exit-wrapper.js +50 -0
- package/dist/pty-shot.d.ts +2 -0
- package/dist/pty-shot.js +483 -0
- package/dist/render-ink.d.ts +10 -0
- package/dist/render-ink.js +87 -0
- package/dist/shot.d.ts +24 -0
- package/dist/shot.js +250 -0
- package/dist/terminal-html.d.ts +14 -0
- package/dist/terminal-html.js +144 -0
- package/dist/types.d.ts +81 -0
- package/dist/types.js +1 -0
- package/package.json +90 -0
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,147 @@
|
|
|
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 --@archastro:registry=https://registry.npmjs.org @archastro/astroshot install-browser
|
|
20
|
+
npx --@archastro:registry=https://registry.npmjs.org @archastro/astroshot init ink
|
|
21
|
+
npx --@archastro:registry=https://registry.npmjs.org @archastro/astroshot init pty
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then capture either boundary:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx --@archastro:registry=https://registry.npmjs.org \
|
|
28
|
+
@archastro/astroshot ink ./ink.shot.tsx -o ./ink.png
|
|
29
|
+
npx --@archastro:registry=https://registry.npmjs.org \
|
|
30
|
+
@archastro/astroshot pty ./pty.shot.yaml -o ./ratatui.png
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
On Linux hosts missing Chromium's system libraries, install the browser with
|
|
34
|
+
`install-browser --with-deps`.
|
|
35
|
+
|
|
36
|
+
## Ink fixture contract
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
import { Box, Text } from "ink";
|
|
40
|
+
import React from "react";
|
|
41
|
+
import type { InkShotFixture } from "@archastro/astroshot/ink";
|
|
42
|
+
|
|
43
|
+
export default {
|
|
44
|
+
cols: 80,
|
|
45
|
+
rows: 12,
|
|
46
|
+
background: "#090a12",
|
|
47
|
+
scale: 2,
|
|
48
|
+
expectText: ["Ready to deploy"],
|
|
49
|
+
component: (
|
|
50
|
+
<Box borderStyle="round">
|
|
51
|
+
<Text color="#b9a8ff">Ready to deploy</Text>
|
|
52
|
+
</Box>
|
|
53
|
+
),
|
|
54
|
+
} satisfies InkShotFixture;
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`expectText` makes capture fail unless every listed string exists in the final
|
|
58
|
+
terminal screen. `--cols`, `--rows`, and `--scale` override fixture values.
|
|
59
|
+
Ink capture also supports `batch <manifest.yaml|json>`; manifest paths resolve
|
|
60
|
+
relative to the manifest.
|
|
61
|
+
|
|
62
|
+
## PTY fixture contract
|
|
63
|
+
|
|
64
|
+
PTY fixtures work with any executable that behaves like a terminal program:
|
|
65
|
+
|
|
66
|
+
```yaml
|
|
67
|
+
version: 1
|
|
68
|
+
command: ./target/debug/my-ratatui-app
|
|
69
|
+
args: []
|
|
70
|
+
cwd: .
|
|
71
|
+
cols: 100
|
|
72
|
+
rows: 30
|
|
73
|
+
timeoutMs: 15000
|
|
74
|
+
settleMs: 100
|
|
75
|
+
actions:
|
|
76
|
+
- waitFor: Choose an option
|
|
77
|
+
- key: down
|
|
78
|
+
- key: enter
|
|
79
|
+
- waitFor: Ready
|
|
80
|
+
expectText:
|
|
81
|
+
- Ready
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The child receives `TERM=xterm-256color`, a fixed terminal grid, and the
|
|
85
|
+
fixture environment. `command` is spawned directly—not through a shell—and
|
|
86
|
+
`cwd` resolves relative to the fixture. On Windows, use an `.exe`; `.cmd` and
|
|
87
|
+
`.bat` scripts are rejected because they require a shell. Supported actions
|
|
88
|
+
are:
|
|
89
|
+
|
|
90
|
+
- `waitFor` with an optional per-action `timeoutMs`;
|
|
91
|
+
- `waitForExit: true` with an optional `timeoutMs` when the documented program
|
|
92
|
+
is expected to terminate before capture;
|
|
93
|
+
- `key`: `enter`, arrows, `tab`, `escape`, `backspace`, `space`, `ctrl-c`, or
|
|
94
|
+
`ctrl-d`;
|
|
95
|
+
- `text` for literal input;
|
|
96
|
+
- `pauseMs` for a bounded delay.
|
|
97
|
+
|
|
98
|
+
Actions run in order. The final xterm screen must contain every `expectText`
|
|
99
|
+
value before Chromium captures it. Astroshot terminates a still-running child
|
|
100
|
+
after the screenshot.
|
|
101
|
+
|
|
102
|
+
A child that exits nonzero before capture fails by default, even when expected
|
|
103
|
+
text rendered. Set `allowNonZeroExit: true` only to document an intentional
|
|
104
|
+
failure state. Use a final `waitForExit: true` action when process completion is
|
|
105
|
+
part of the documented state; this waits for the authoritative child status
|
|
106
|
+
instead of relying on a fixed settle delay.
|
|
107
|
+
|
|
108
|
+
The renderer targets text and ANSI/VT terminal interfaces. Mouse events,
|
|
109
|
+
mid-journey resize actions, and terminal graphics protocols such as Sixel or
|
|
110
|
+
Kitty images are not currently modeled.
|
|
111
|
+
|
|
112
|
+
`node-pty` is an optional, lazily loaded native dependency so React and Ink
|
|
113
|
+
capture still work on an unsupported PTY host. This release pins the exact
|
|
114
|
+
official `1.2.0-beta.14` build; PTY capture reports an actionable error if its
|
|
115
|
+
native addon is unavailable.
|
|
116
|
+
|
|
117
|
+
## Reproducibility and trust
|
|
118
|
+
|
|
119
|
+
Playwright is pinned so each release selects one Chromium version. PNG bytes
|
|
120
|
+
can still differ across operating systems because the default stage uses host
|
|
121
|
+
fonts. Set `fontFamily` and use a consistent OS/container for pixel baselines.
|
|
122
|
+
|
|
123
|
+
Ink fixture modules and PTY commands are executable code with the current
|
|
124
|
+
user's permissions. Only capture trusted repositories. A PTY is an I/O
|
|
125
|
+
boundary, not a security sandbox; use a container for untrusted programs.
|
|
126
|
+
|
|
127
|
+
## Programmatic API
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import {
|
|
131
|
+
closeSharedBrowser,
|
|
132
|
+
takePtyShot,
|
|
133
|
+
takeTuiShot,
|
|
134
|
+
} from "@archastro/tui-shot";
|
|
135
|
+
|
|
136
|
+
await takeTuiShot({
|
|
137
|
+
fixturePath: "./screenshots/welcome.tsx",
|
|
138
|
+
outPath: "./screenshots/welcome.png",
|
|
139
|
+
});
|
|
140
|
+
await takePtyShot({
|
|
141
|
+
fixturePath: "./screenshots/ratatui.yaml",
|
|
142
|
+
outPath: "./screenshots/ratatui.png",
|
|
143
|
+
});
|
|
144
|
+
await closeSharedBrowser();
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Ink fixtures support Ink 7 and React 19 as peer dependencies.
|
package/bin/tui-shot.mjs
ADDED
|
@@ -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
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -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);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -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
|
+
});
|