astroshot 0.2.1 → 0.2.2
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/node_modules/@archastro/astroshot/README.md +4 -0
- package/node_modules/@archastro/astroshot/bin/astroshot.mjs +56 -1
- package/node_modules/@archastro/astroshot/bin/demo.mjs +72 -16
- package/node_modules/@archastro/astroshot/package.json +5 -4
- package/node_modules/@archastro/astroshot-review/LICENSE +21 -0
- package/node_modules/@archastro/astroshot-review/README.md +19 -0
- package/node_modules/@archastro/astroshot-review/bin/astroshot-review.mjs +17 -0
- package/node_modules/@archastro/astroshot-review/dist/cli.d.ts +12 -0
- package/node_modules/@archastro/astroshot-review/dist/cli.js +240 -0
- package/node_modules/@archastro/astroshot-review/dist/data/friction.d.ts +22 -0
- package/node_modules/@archastro/astroshot-review/dist/data/friction.js +278 -0
- package/node_modules/@archastro/astroshot-review/dist/data/hash-cache.d.ts +17 -0
- package/node_modules/@archastro/astroshot-review/dist/data/hash-cache.js +51 -0
- package/node_modules/@archastro/astroshot-review/dist/data/index-cache.d.ts +22 -0
- package/node_modules/@archastro/astroshot-review/dist/data/index-cache.js +64 -0
- package/node_modules/@archastro/astroshot-review/dist/data/manifest.d.ts +41 -0
- package/node_modules/@archastro/astroshot-review/dist/data/manifest.js +105 -0
- package/node_modules/@archastro/astroshot-review/dist/data/model.d.ts +96 -0
- package/node_modules/@archastro/astroshot-review/dist/data/model.js +1 -0
- package/node_modules/@archastro/astroshot-review/dist/data/paths.d.ts +27 -0
- package/node_modules/@archastro/astroshot-review/dist/data/paths.js +98 -0
- package/node_modules/@archastro/astroshot-review/dist/data/review-store.d.ts +67 -0
- package/node_modules/@archastro/astroshot-review/dist/data/review-store.js +237 -0
- package/node_modules/@archastro/astroshot-review/dist/data/scan.d.ts +32 -0
- package/node_modules/@archastro/astroshot-review/dist/data/scan.js +227 -0
- package/node_modules/@archastro/astroshot-review/dist/data/store.d.ts +92 -0
- package/node_modules/@archastro/astroshot-review/dist/data/store.js +408 -0
- package/node_modules/@archastro/astroshot-review/dist/data/watcher.d.ts +37 -0
- package/node_modules/@archastro/astroshot-review/dist/data/watcher.js +126 -0
- package/node_modules/@archastro/astroshot-review/dist/images/halfblocks.d.ts +10 -0
- package/node_modules/@archastro/astroshot-review/dist/images/halfblocks.js +21 -0
- package/node_modules/@archastro/astroshot-review/dist/images/png.d.ts +14 -0
- package/node_modules/@archastro/astroshot-review/dist/images/png.js +45 -0
- package/node_modules/@archastro/astroshot-review/dist/images/scale.d.ts +31 -0
- package/node_modules/@archastro/astroshot-review/dist/images/scale.js +102 -0
- package/node_modules/@archastro/astroshot-review/dist/images/service.d.ts +54 -0
- package/node_modules/@archastro/astroshot-review/dist/images/service.js +163 -0
- package/node_modules/@archastro/astroshot-review/dist/images/worker.d.ts +21 -0
- package/node_modules/@archastro/astroshot-review/dist/images/worker.js +30 -0
- package/node_modules/@archastro/astroshot-review/dist/index.d.ts +12 -0
- package/node_modules/@archastro/astroshot-review/dist/index.js +9 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/graphics-stdout.d.ts +9 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/graphics-stdout.js +36 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/herdr.d.ts +61 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/herdr.js +327 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/image-layer.d.ts +122 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/image-layer.js +471 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/kitty.d.ts +74 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/kitty.js +112 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/probe.d.ts +49 -0
- package/node_modules/@archastro/astroshot-review/dist/terminal/probe.js +206 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/app.d.ts +6 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/app.js +695 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/chrome.d.ts +53 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/chrome.js +69 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/context.d.ts +16 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/context.js +8 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/detail.d.ts +33 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/detail.js +39 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/friction.d.ts +42 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/friction.js +84 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/help.d.ts +4 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/help.js +61 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/hooks.d.ts +9 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/hooks.js +32 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/movie-player.d.ts +29 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/movie-player.js +119 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/picture.d.ts +19 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/picture.js +100 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/selectors.d.ts +26 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/selectors.js +69 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/settings.d.ts +5 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/settings.js +17 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/stream.d.ts +38 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/stream.js +118 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/system.d.ts +4 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/system.js +34 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/takeover.d.ts +26 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/takeover.js +29 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/text-input.d.ts +8 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/text-input.js +74 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/theme.d.ts +21 -0
- package/node_modules/@archastro/astroshot-review/dist/ui/theme.js +63 -0
- package/node_modules/@archastro/astroshot-review/dist/video/ffmpeg.d.ts +54 -0
- package/node_modules/@archastro/astroshot-review/dist/video/ffmpeg.js +206 -0
- package/node_modules/@archastro/astroshot-review/package.json +71 -0
- package/node_modules/@archastro/movie-harness/package.json +1 -1
- package/node_modules/@archastro/react-shot/package.json +4 -4
- package/node_modules/@archastro/tui-shot/README.md +9 -0
- package/node_modules/@archastro/tui-shot/dist/index.d.ts +2 -0
- package/node_modules/@archastro/tui-shot/dist/index.js +1 -0
- package/node_modules/@archastro/tui-shot/dist/kitty-graphics.d.ts +45 -0
- package/node_modules/@archastro/tui-shot/dist/kitty-graphics.js +279 -0
- package/node_modules/@archastro/tui-shot/dist/pty-shot.js +25 -3
- package/node_modules/@archastro/tui-shot/dist/shot.d.ts +2 -0
- package/node_modules/@archastro/tui-shot/dist/shot.js +3 -1
- package/node_modules/@archastro/tui-shot/dist/types.d.ts +6 -0
- package/node_modules/@archastro/tui-shot/package.json +1 -1
- package/package.json +10 -5
|
@@ -13,6 +13,10 @@ npx astroshot demo
|
|
|
13
13
|
# Check Node, watched folders, app install/run state, Chromium, and macOS
|
|
14
14
|
# Screen Recording. Each failure prints the exact fix command. Read-only.
|
|
15
15
|
npx astroshot doctor
|
|
16
|
+
|
|
17
|
+
# Review the stream in your terminal (Ghostty, kitty, or WezTerm draw the
|
|
18
|
+
# pictures; ffmpeg plays movies). Same review.json as the macOS app.
|
|
19
|
+
npx astroshot review
|
|
16
20
|
```
|
|
17
21
|
|
|
18
22
|
`demo` accepts `--feature <name>`, `--root <dir>`, and `--json`. `doctor`
|
|
@@ -8,12 +8,14 @@ import path from "node:path";
|
|
|
8
8
|
import { writeFixtureTemplate } from "./templates.mjs";
|
|
9
9
|
import { demoHelp, runDemo } from "./demo.mjs";
|
|
10
10
|
import { doctorHelp, runDoctor } from "./doctor.mjs";
|
|
11
|
+
import { readWatchConfiguration } from "./mac-preferences.mjs";
|
|
11
12
|
|
|
12
13
|
function help() {
|
|
13
14
|
console.log(`astroshot — one CLI for React, Ink, PTY stills, and movies
|
|
14
15
|
|
|
15
16
|
Usage:
|
|
16
|
-
astroshot
|
|
17
|
+
astroshot review [<dir>...] [--root <dir>] [--no-graphics]
|
|
18
|
+
astroshot demo [--feature <name>] [--root <dir>] [--dry-run] [--clean]
|
|
17
19
|
astroshot doctor [--root <dir>] [--json]
|
|
18
20
|
astroshot init react [fixture.tsx] [--force]
|
|
19
21
|
astroshot init ink [fixture.tsx] [--force]
|
|
@@ -28,6 +30,7 @@ Usage:
|
|
|
28
30
|
astroshot install-browser [--with-deps]
|
|
29
31
|
|
|
30
32
|
Start here:
|
|
33
|
+
review Review the .astroshot/ stream in your terminal (Kitty graphics)
|
|
31
34
|
demo Write a complete .astroshot/ example set (no prerequisites)
|
|
32
35
|
doctor Check Node, watched folders, app, Chromium, permissions
|
|
33
36
|
|
|
@@ -104,6 +107,11 @@ Options:
|
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
function engineBin(mode) {
|
|
110
|
+
if (mode === "review") {
|
|
111
|
+
const entry = fileURLToPath(import.meta.resolve("@archastro/astroshot-review"));
|
|
112
|
+
const packageRoot = path.dirname(path.dirname(entry));
|
|
113
|
+
return path.join(packageRoot, "bin", "astroshot-review.mjs");
|
|
114
|
+
}
|
|
107
115
|
if (mode === "movie") {
|
|
108
116
|
const entry = fileURLToPath(import.meta.resolve("@archastro/movie-harness"));
|
|
109
117
|
// package exports "." → dist/index.js → package root is two levels up from dist
|
|
@@ -189,6 +197,49 @@ function runInit(arguments_) {
|
|
|
189
197
|
console.log(`Created ${result.label} fixture: ${result.absolutePath}`);
|
|
190
198
|
}
|
|
191
199
|
|
|
200
|
+
/**
|
|
201
|
+
* `astroshot review`: the terminal tray. Without explicit roots it watches
|
|
202
|
+
* the same folders as the Astroshots app so both surfaces show one stream.
|
|
203
|
+
*/
|
|
204
|
+
function runReview(arguments_) {
|
|
205
|
+
// `astroshot review help` mirrors the other modes; anything else is
|
|
206
|
+
// forwarded verbatim so a folder literally named "help" still works.
|
|
207
|
+
const wantsHelp =
|
|
208
|
+
arguments_[0] === "help" ||
|
|
209
|
+
arguments_.includes("-h") ||
|
|
210
|
+
arguments_.includes("--help");
|
|
211
|
+
const forwarded = wantsHelp ? ["--help"] : [...arguments_];
|
|
212
|
+
const hasRoots =
|
|
213
|
+
forwarded.includes("--root") ||
|
|
214
|
+
forwarded.some((value) => !value.startsWith("-"));
|
|
215
|
+
if (!hasRoots && !wantsHelp) {
|
|
216
|
+
const configuration = readWatchConfiguration();
|
|
217
|
+
if (configuration.available && configuration.roots.length > 0) {
|
|
218
|
+
for (const root of configuration.roots) forwarded.push("--root", root);
|
|
219
|
+
forwarded.push("--roots-source", "app");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
let engine;
|
|
223
|
+
try {
|
|
224
|
+
engine = engineBin("review");
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error(
|
|
227
|
+
`astroshot could not find the review tray engine (@archastro/astroshot-review): ${error instanceof Error ? error.message : error}`,
|
|
228
|
+
);
|
|
229
|
+
console.error("Reinstall @archastro/astroshot (or the unscoped astroshot package) and retry.");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const result = spawnSync(process.execPath, [engine, ...forwarded], {
|
|
233
|
+
stdio: "inherit",
|
|
234
|
+
});
|
|
235
|
+
if (result.error) {
|
|
236
|
+
console.error(`astroshot could not start the review tray: ${result.error.message}`);
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
if (result.signal) process.kill(process.pid, result.signal);
|
|
240
|
+
process.exit(result.status ?? 1);
|
|
241
|
+
}
|
|
242
|
+
|
|
192
243
|
const [command, ...arguments_] = process.argv.slice(2);
|
|
193
244
|
|
|
194
245
|
if (command === "-v" || command === "--version") {
|
|
@@ -254,6 +305,10 @@ if (command === "react" || command === "ink" || command === "tui" || command ===
|
|
|
254
305
|
runEngine(canonicalMode, arguments_);
|
|
255
306
|
}
|
|
256
307
|
|
|
308
|
+
if (command === "review" || command === "tray") {
|
|
309
|
+
runReview(arguments_);
|
|
310
|
+
}
|
|
311
|
+
|
|
257
312
|
if (command === "movie") {
|
|
258
313
|
// Always forward to movie-harness (including --help / which-source).
|
|
259
314
|
const result = spawnSync(
|
|
@@ -30,6 +30,8 @@ Options:
|
|
|
30
30
|
--feature <name> Feature directory under .astroshot/ (default: ${DEFAULT_DEMO_FEATURE})
|
|
31
31
|
--root <dir> Worktree root (default: git root, else cwd)
|
|
32
32
|
--json Print the written paths as JSON
|
|
33
|
+
--dry-run Print planned paths without writing
|
|
34
|
+
--clean Accepted; does not delete yet
|
|
33
35
|
-h, --help Show this help
|
|
34
36
|
|
|
35
37
|
Writes two stills, one movie poster + video pair, and manifest.json using
|
|
@@ -112,14 +114,24 @@ export function loadDemoFixtures(fixturesDirectory = FIXTURES_DIR) {
|
|
|
112
114
|
return index;
|
|
113
115
|
}
|
|
114
116
|
|
|
117
|
+
function publicDemoResult(plan) {
|
|
118
|
+
return {
|
|
119
|
+
root: plan.root,
|
|
120
|
+
feature: plan.feature,
|
|
121
|
+
featureDirectory: plan.featureDirectory,
|
|
122
|
+
manifestPath: plan.manifestPath,
|
|
123
|
+
runId: plan.runId,
|
|
124
|
+
shots: plan.shots,
|
|
125
|
+
files: plan.files,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
/**
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* earlier numbered frames stay on disk as prior-run evidence. That matches the
|
|
120
|
-
* documented lifecycle used by astroshot-capture and the movie harness.
|
|
130
|
+
* Read-only plan of the next demo write: same root/feature resolve, kebab-case
|
|
131
|
+
* check, fixtures, next sequence, and file names (including manifest.json).
|
|
132
|
+
* Does not mkdir or write.
|
|
121
133
|
*/
|
|
122
|
-
export function
|
|
134
|
+
export function planDemo({
|
|
123
135
|
root,
|
|
124
136
|
feature = DEFAULT_DEMO_FEATURE,
|
|
125
137
|
fixturesDirectory = FIXTURES_DIR,
|
|
@@ -130,7 +142,6 @@ export function writeDemo({
|
|
|
130
142
|
const featureDirectory = path.join(resolvedRoot, ".astroshot", feature);
|
|
131
143
|
const index = loadDemoFixtures(fixturesDirectory);
|
|
132
144
|
|
|
133
|
-
fs.mkdirSync(featureDirectory, { recursive: true });
|
|
134
145
|
const startSequence = nextSequence(featureDirectory);
|
|
135
146
|
const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
|
|
136
147
|
// The first sequence of this run keeps the id unique when two runs land in
|
|
@@ -140,11 +151,16 @@ export function writeDemo({
|
|
|
140
151
|
|
|
141
152
|
const shots = [];
|
|
142
153
|
const files = [];
|
|
154
|
+
const copies = [];
|
|
143
155
|
index.shots.forEach((fixture, offset) => {
|
|
144
156
|
const sequence = String(startSequence + offset).padStart(4, "0");
|
|
145
157
|
const posterName = `${sequence}-${fixture.slug}.png`;
|
|
146
|
-
|
|
147
|
-
|
|
158
|
+
const posterPath = path.join(featureDirectory, posterName);
|
|
159
|
+
copies.push({
|
|
160
|
+
source: path.join(fixturesDirectory, fixture.asset),
|
|
161
|
+
destination: posterPath,
|
|
162
|
+
});
|
|
163
|
+
files.push(posterPath);
|
|
148
164
|
|
|
149
165
|
const shot = {
|
|
150
166
|
id: sequence,
|
|
@@ -159,8 +175,12 @@ export function writeDemo({
|
|
|
159
175
|
if (fixture.video) {
|
|
160
176
|
const videoExtension = path.extname(fixture.video) || ".webm";
|
|
161
177
|
const videoName = `${sequence}-${fixture.slug}${videoExtension}`;
|
|
162
|
-
|
|
163
|
-
|
|
178
|
+
const videoPath = path.join(featureDirectory, videoName);
|
|
179
|
+
copies.push({
|
|
180
|
+
source: path.join(fixturesDirectory, fixture.video),
|
|
181
|
+
destination: videoPath,
|
|
182
|
+
});
|
|
183
|
+
files.push(videoPath);
|
|
164
184
|
shot.kind = "movie";
|
|
165
185
|
shot.video = videoName;
|
|
166
186
|
shot.duration_ms = fixture.duration_ms;
|
|
@@ -181,7 +201,6 @@ export function writeDemo({
|
|
|
181
201
|
"Synthetic proof set written by `astroshot demo` — stills plus one journey movie.",
|
|
182
202
|
shots,
|
|
183
203
|
};
|
|
184
|
-
writeAtomic(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
185
204
|
files.push(manifestPath);
|
|
186
205
|
|
|
187
206
|
return {
|
|
@@ -189,12 +208,31 @@ export function writeDemo({
|
|
|
189
208
|
feature,
|
|
190
209
|
featureDirectory,
|
|
191
210
|
manifestPath,
|
|
211
|
+
manifest,
|
|
192
212
|
runId,
|
|
193
213
|
shots,
|
|
194
214
|
files,
|
|
215
|
+
copies,
|
|
195
216
|
};
|
|
196
217
|
}
|
|
197
218
|
|
|
219
|
+
/**
|
|
220
|
+
* Write the demo set and return the paths, without printing anything.
|
|
221
|
+
*
|
|
222
|
+
* Each invocation is its own run: a new `run_id` with a fresh shot list, while
|
|
223
|
+
* earlier numbered frames stay on disk as prior-run evidence. That matches the
|
|
224
|
+
* documented lifecycle used by astroshot-capture and the movie harness.
|
|
225
|
+
*/
|
|
226
|
+
export function writeDemo(options = {}) {
|
|
227
|
+
const plan = planDemo(options);
|
|
228
|
+
fs.mkdirSync(plan.featureDirectory, { recursive: true });
|
|
229
|
+
for (const { source, destination } of plan.copies) {
|
|
230
|
+
copyAtomic(source, destination);
|
|
231
|
+
}
|
|
232
|
+
writeAtomic(plan.manifestPath, `${JSON.stringify(plan.manifest, null, 2)}\n`);
|
|
233
|
+
return publicDemoResult(plan);
|
|
234
|
+
}
|
|
235
|
+
|
|
198
236
|
function coverageAdvice(root) {
|
|
199
237
|
const configuration = readWatchConfiguration();
|
|
200
238
|
const coverage = evaluateWatchCoverage(root, configuration);
|
|
@@ -231,7 +269,13 @@ function coverageAdvice(root) {
|
|
|
231
269
|
}
|
|
232
270
|
|
|
233
271
|
export function runDemo(argv, { log = console.log } = {}) {
|
|
234
|
-
const options = {
|
|
272
|
+
const options = {
|
|
273
|
+
feature: DEFAULT_DEMO_FEATURE,
|
|
274
|
+
root: undefined,
|
|
275
|
+
json: false,
|
|
276
|
+
dryRun: false,
|
|
277
|
+
clean: false,
|
|
278
|
+
};
|
|
235
279
|
for (let index = 0; index < argv.length; index += 1) {
|
|
236
280
|
const token = argv[index];
|
|
237
281
|
if (token === "-h" || token === "--help" || token === "help") {
|
|
@@ -242,6 +286,14 @@ export function runDemo(argv, { log = console.log } = {}) {
|
|
|
242
286
|
options.json = true;
|
|
243
287
|
continue;
|
|
244
288
|
}
|
|
289
|
+
if (token === "--dry-run") {
|
|
290
|
+
options.dryRun = true;
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (token === "--clean") {
|
|
294
|
+
options.clean = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
245
297
|
if (token === "--feature" || token === "--root") {
|
|
246
298
|
const value = argv[index + 1];
|
|
247
299
|
if (!value || value.startsWith("-")) {
|
|
@@ -254,18 +306,22 @@ export function runDemo(argv, { log = console.log } = {}) {
|
|
|
254
306
|
throw new Error(`Unknown demo argument: ${token}`);
|
|
255
307
|
}
|
|
256
308
|
|
|
257
|
-
const result =
|
|
309
|
+
const result = options.dryRun
|
|
310
|
+
? publicDemoResult(planDemo(options))
|
|
311
|
+
: writeDemo(options);
|
|
258
312
|
if (options.json) {
|
|
259
313
|
log(JSON.stringify({ ...result, advice: coverageAdvice(result.root) }, null, 2));
|
|
260
314
|
return 0;
|
|
261
315
|
}
|
|
262
316
|
|
|
263
|
-
const movies = result.shots.filter((shot) => shot.kind === "movie").length;
|
|
264
|
-
const stills = result.shots.length - movies;
|
|
265
317
|
log(`astroshot demo → ${result.featureDirectory}`);
|
|
266
318
|
for (const file of result.files) {
|
|
267
319
|
log(` ${path.relative(result.root, file)}`);
|
|
268
320
|
}
|
|
321
|
+
if (options.dryRun) return 0;
|
|
322
|
+
|
|
323
|
+
const movies = result.shots.filter((shot) => shot.kind === "movie").length;
|
|
324
|
+
const stills = result.shots.length - movies;
|
|
269
325
|
log("");
|
|
270
326
|
log(
|
|
271
327
|
`Wrote ${stills} still${stills === 1 ? "" : "s"}, ${movies} movie${movies === 1 ? "" : "s"} (poster + video), and manifest.json.`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@archastro/astroshot",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "One CLI for deterministic React, terminal, and movie UI captures",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -72,8 +72,9 @@
|
|
|
72
72
|
"registry": "https://registry.npmjs.org/"
|
|
73
73
|
},
|
|
74
74
|
"dependencies": {
|
|
75
|
-
"@archastro/
|
|
76
|
-
"@archastro/
|
|
77
|
-
"@archastro/
|
|
75
|
+
"@archastro/astroshot-review": "0.2.2",
|
|
76
|
+
"@archastro/movie-harness": "0.2.2",
|
|
77
|
+
"@archastro/react-shot": "0.2.2",
|
|
78
|
+
"@archastro/tui-shot": "0.2.2"
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -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.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @archastro/astroshot-review
|
|
2
|
+
|
|
3
|
+
The Astroshots review tray in your terminal. Streams `.astroshot/` captures
|
|
4
|
+
across worktrees newest-first, draws them with the Kitty graphics protocol,
|
|
5
|
+
plays movies through ffmpeg, and writes the same `review.json` Seen state and
|
|
6
|
+
feedback as the macOS app.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npx astroshot review # via the unified CLI (recommended)
|
|
10
|
+
npx astroshot-review ~/projects # this package's own bin
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- Terminals: Ghostty, kitty, WezTerm (anything with the Kitty graphics protocol).
|
|
14
|
+
Others get labeled placeholders.
|
|
15
|
+
- Movies need `ffmpeg` on `PATH`.
|
|
16
|
+
- Keys: `↑↓` move · `⏎` open · `f` full screen · `s` seen · `c` feedback ·
|
|
17
|
+
`u` history · `m` movies · `1`/`2` tabs · `,` settings · `?` help · `q` quit.
|
|
18
|
+
|
|
19
|
+
Full guide: [docs/review-tui.md](https://github.com/ArchAstro/astroshots/blob/main/docs/review-tui.md).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { main } from "../dist/cli.js";
|
|
3
|
+
|
|
4
|
+
main(process.argv.slice(2)).then(
|
|
5
|
+
(code) => {
|
|
6
|
+
process.exitCode = code;
|
|
7
|
+
// main() has torn the tray down, so the event loop should drain and the
|
|
8
|
+
// process exit on its own. If a stray handle is still referenced (a
|
|
9
|
+
// socket, a pipe), don't sit invisibly in the shell forever: exit. The
|
|
10
|
+
// timer is unref'd, so it never keeps the loop alive itself.
|
|
11
|
+
setTimeout(() => process.exit(code), 1000).unref();
|
|
12
|
+
},
|
|
13
|
+
(error) => {
|
|
14
|
+
console.error(error instanceof Error ? error.message : error);
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
},
|
|
17
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function reviewHelp(): string;
|
|
2
|
+
export interface ParsedArgs {
|
|
3
|
+
roots: string[];
|
|
4
|
+
graphics: boolean;
|
|
5
|
+
watch: boolean;
|
|
6
|
+
index: boolean;
|
|
7
|
+
help: boolean;
|
|
8
|
+
version: boolean;
|
|
9
|
+
rootsSource: "app" | "cli" | "cwd";
|
|
10
|
+
}
|
|
11
|
+
export declare function parseArgs(argv: string[]): ParsedArgs;
|
|
12
|
+
export declare function main(argv: string[]): Promise<number>;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { createElement } from "react";
|
|
5
|
+
import { ReviewStore } from "./data/store.js";
|
|
6
|
+
import { ImageService } from "./images/service.js";
|
|
7
|
+
import { createGraphicsStdout } from "./terminal/graphics-stdout.js";
|
|
8
|
+
import { HerdrSink, discoverHerdr, herdrAddress, probeHerdrSet } from "./terminal/herdr.js";
|
|
9
|
+
import { ImageLayer } from "./terminal/image-layer.js";
|
|
10
|
+
import { probeTerminal } from "./terminal/probe.js";
|
|
11
|
+
import { App } from "./ui/app.js";
|
|
12
|
+
import { ServicesContext } from "./ui/context.js";
|
|
13
|
+
import { detectFfmpeg } from "./video/ffmpeg.js";
|
|
14
|
+
export function reviewHelp() {
|
|
15
|
+
return `astroshot review — the Astroshots tray in your terminal
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
astroshot review [<dir>...] [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--root <dir> Folder to watch for .astroshot/ trees (repeatable)
|
|
22
|
+
--no-graphics Skip terminal pictures (text only)
|
|
23
|
+
--no-watch Do not follow filesystem changes
|
|
24
|
+
--no-index Ignore the on-disk index (full scan every start)
|
|
25
|
+
-h, --help Show this help
|
|
26
|
+
|
|
27
|
+
Without roots, \`astroshot review\` uses the folders the Astroshots app
|
|
28
|
+
watches (macOS) and otherwise the current directory. Images render pixel-
|
|
29
|
+
perfect in Kitty-protocol terminals (Ghostty, kitty, WezTerm) and inside
|
|
30
|
+
herdr (enable [experimental] kitty_graphics and reattach the client once);
|
|
31
|
+
elsewhere — mosh, tmux, plain terminals — they render as truecolor
|
|
32
|
+
half-block text. Movie playback needs ffmpeg on PATH.
|
|
33
|
+
|
|
34
|
+
Keys: ↑↓ move · ⏎ open · f full screen · s seen · c feedback · u history ·
|
|
35
|
+
m movies · 1/2 tabs · , settings · ? help · q quit`;
|
|
36
|
+
}
|
|
37
|
+
export function parseArgs(argv) {
|
|
38
|
+
const parsed = { roots: [], graphics: true, watch: true, index: true, help: false, version: false, rootsSource: "cli" };
|
|
39
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
40
|
+
const argument = argv[index];
|
|
41
|
+
switch (argument) {
|
|
42
|
+
case "-h":
|
|
43
|
+
case "--help":
|
|
44
|
+
parsed.help = true;
|
|
45
|
+
break;
|
|
46
|
+
case "-v":
|
|
47
|
+
case "--version":
|
|
48
|
+
parsed.version = true;
|
|
49
|
+
break;
|
|
50
|
+
case "--no-graphics":
|
|
51
|
+
parsed.graphics = false;
|
|
52
|
+
break;
|
|
53
|
+
case "--no-watch":
|
|
54
|
+
parsed.watch = false;
|
|
55
|
+
break;
|
|
56
|
+
case "--no-index":
|
|
57
|
+
parsed.index = false;
|
|
58
|
+
break;
|
|
59
|
+
case "--root": {
|
|
60
|
+
const value = argv[index + 1];
|
|
61
|
+
if (!value)
|
|
62
|
+
throw new Error("--root requires a directory");
|
|
63
|
+
parsed.roots.push(value);
|
|
64
|
+
index += 1;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
case "--roots-source": {
|
|
68
|
+
const value = argv[index + 1];
|
|
69
|
+
if (value !== "app" && value !== "cli" && value !== "cwd")
|
|
70
|
+
throw new Error("--roots-source must be app, cli, or cwd");
|
|
71
|
+
parsed.rootsSource = value;
|
|
72
|
+
index += 1;
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
if (argument.startsWith("-"))
|
|
77
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
78
|
+
parsed.roots.push(argument);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (parsed.roots.length === 0) {
|
|
82
|
+
parsed.roots = [process.cwd()];
|
|
83
|
+
parsed.rootsSource = "cwd";
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
function readVersion() {
|
|
88
|
+
try {
|
|
89
|
+
const packageJson = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
90
|
+
return packageJson.version ?? "0.0.0";
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return "0.0.0";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export async function main(argv) {
|
|
97
|
+
let args;
|
|
98
|
+
try {
|
|
99
|
+
args = parseArgs(argv);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
103
|
+
console.error("");
|
|
104
|
+
console.error(reviewHelp());
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
if (args.help) {
|
|
108
|
+
console.log(reviewHelp());
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
if (args.version) {
|
|
112
|
+
console.log(readVersion());
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
116
|
+
console.error("astroshot review needs an interactive terminal (stdin and stdout must be a TTY).");
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
const roots = args.roots.map((root) => path.resolve(root)).filter((root) => {
|
|
120
|
+
try {
|
|
121
|
+
return fs.statSync(root).isDirectory();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
console.error(`Ignoring missing folder: ${root}`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
const debugLog = process.env.ASTROSHOT_REVIEW_DEBUG
|
|
129
|
+
? (message) => fs.appendFileSync("astroshot-review.log", `${new Date().toISOString()} ${message}\n`)
|
|
130
|
+
: null;
|
|
131
|
+
let capabilities = await probeTerminal({ stdin: process.stdin, stdout: process.stdout }, { env: args.graphics ? process.env : { ...process.env, ASTROSHOT_REVIEW_GRAPHICS: "none" } });
|
|
132
|
+
// Inside herdr, raw Kitty escapes are dropped; render through its socket
|
|
133
|
+
// graphics API instead (pixel-perfect), falling back to half-blocks with an
|
|
134
|
+
// actionable reason when herdr can't yet report the host cell size.
|
|
135
|
+
let herdrSink;
|
|
136
|
+
const herdr = args.graphics ? herdrAddress() : null;
|
|
137
|
+
if (herdr) {
|
|
138
|
+
const discovery = await discoverHerdr(herdr, {
|
|
139
|
+
onWaiting: () => debugLog?.("herdr: waiting for host cell size"),
|
|
140
|
+
});
|
|
141
|
+
if (discovery.ok && discovery.cellWidth && discovery.cellHeight) {
|
|
142
|
+
const setSupport = await probeHerdrSet(herdr);
|
|
143
|
+
if (setSupport.ok) {
|
|
144
|
+
herdrSink = new HerdrSink(herdr, (error) => debugLog?.(`herdr: ${error.message}`));
|
|
145
|
+
capabilities = {
|
|
146
|
+
...capabilities,
|
|
147
|
+
graphics: "herdr",
|
|
148
|
+
cellWidth: discovery.cellWidth,
|
|
149
|
+
cellHeight: discovery.cellHeight,
|
|
150
|
+
cellSource: "query",
|
|
151
|
+
reason: undefined,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// herdr can size images but not place per-image layers; half-blocks it is.
|
|
156
|
+
capabilities = { ...capabilities, reason: setSupport.reason ?? "herdr pane.graphics.set unavailable" };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
capabilities = { ...capabilities, reason: discovery.reason ?? capabilities.reason };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
debugLog?.(`capabilities ${JSON.stringify(capabilities)}`);
|
|
164
|
+
const ffmpeg = detectFfmpeg();
|
|
165
|
+
const service = new ImageService();
|
|
166
|
+
const layer = new ImageLayer({
|
|
167
|
+
capabilities,
|
|
168
|
+
service,
|
|
169
|
+
herdr: herdrSink,
|
|
170
|
+
write: (data) => {
|
|
171
|
+
process.stdout.write(data);
|
|
172
|
+
},
|
|
173
|
+
onError: (src, error) => debugLog?.(`image ${src}: ${error.message}`),
|
|
174
|
+
onDebug: debugLog ? (message) => debugLog(`layer ${message}`) : undefined,
|
|
175
|
+
});
|
|
176
|
+
const store = new ReviewStore({
|
|
177
|
+
roots,
|
|
178
|
+
watch: args.watch,
|
|
179
|
+
useIndex: args.index,
|
|
180
|
+
onLog: (message) => debugLog?.(`store ${message}`),
|
|
181
|
+
});
|
|
182
|
+
void store.start();
|
|
183
|
+
const stdout = createGraphicsStdout(process.stdout, layer);
|
|
184
|
+
const services = { store, layer, service, capabilities, ffmpeg, rootsSource: args.rootsSource, version: readVersion() };
|
|
185
|
+
let cleared = false;
|
|
186
|
+
const clearPictures = () => {
|
|
187
|
+
if (cleared)
|
|
188
|
+
return;
|
|
189
|
+
cleared = true;
|
|
190
|
+
const output = layer.clear();
|
|
191
|
+
if (output)
|
|
192
|
+
process.stdout.write(output);
|
|
193
|
+
herdrSink?.dispose();
|
|
194
|
+
};
|
|
195
|
+
const instance = render(createElement(ServicesContext.Provider, { value: services }, createElement(App, { onQuit: clearPictures })), {
|
|
196
|
+
stdout,
|
|
197
|
+
stdin: process.stdin,
|
|
198
|
+
alternateScreen: true,
|
|
199
|
+
exitOnCtrlC: true,
|
|
200
|
+
patchConsole: true,
|
|
201
|
+
// Ink treats CI=true as "not interactive": no alternate screen, no key
|
|
202
|
+
// handling, frames appended instead of redrawn. The tray already refuses
|
|
203
|
+
// to run without a TTY on both ends, so a TTY is the only signal that
|
|
204
|
+
// matters — CI-flavoured shells and CI pty harnesses get the real UI.
|
|
205
|
+
interactive: true,
|
|
206
|
+
});
|
|
207
|
+
// `kill <pid>` or a closing terminal must not leave pictures or the
|
|
208
|
+
// alternate screen behind; Ink only handles Ctrl+C itself.
|
|
209
|
+
const onSignal = (signal) => {
|
|
210
|
+
clearPictures();
|
|
211
|
+
try {
|
|
212
|
+
process.stdout.write("\x1b[?1049l\x1b[?25h");
|
|
213
|
+
if (process.stdin.isTTY)
|
|
214
|
+
process.stdin.setRawMode(false);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// The terminal may already be gone.
|
|
218
|
+
}
|
|
219
|
+
instance.unmount();
|
|
220
|
+
process.exit(signal === "SIGHUP" ? 129 : 143);
|
|
221
|
+
};
|
|
222
|
+
process.once("SIGTERM", onSignal);
|
|
223
|
+
process.once("SIGHUP", onSignal);
|
|
224
|
+
process.once("SIGQUIT", onSignal);
|
|
225
|
+
process.once("SIGINT", onSignal);
|
|
226
|
+
// Last-ditch: closing the herdr sockets makes herdr drop the layers even if
|
|
227
|
+
// we never got to run a signal handler.
|
|
228
|
+
process.once("exit", () => herdrSink?.dispose());
|
|
229
|
+
try {
|
|
230
|
+
await instance.waitUntilExit();
|
|
231
|
+
}
|
|
232
|
+
finally {
|
|
233
|
+
process.off("SIGTERM", onSignal);
|
|
234
|
+
process.off("SIGHUP", onSignal);
|
|
235
|
+
clearPictures();
|
|
236
|
+
await store.dispose();
|
|
237
|
+
await service.dispose();
|
|
238
|
+
}
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { HashCache } from "./hash-cache.js";
|
|
2
|
+
import type { FrictionLog, FrictionRun, FrictionStep } from "./model.js";
|
|
3
|
+
export declare const PROMPT_FILE = "prompt.md";
|
|
4
|
+
export declare const META_FILE = "meta.json";
|
|
5
|
+
export declare const RUNS_DIR = "runs";
|
|
6
|
+
export declare const LOG_FILE = "log.jsonl";
|
|
7
|
+
/** Parse JSONL text into ordered steps; screenshots are resolved against `runDir`. */
|
|
8
|
+
export declare function parseJsonl(text: string, runDir: string): Promise<FrictionStep[]>;
|
|
9
|
+
export declare function loadRun(runDir: string, runId: string, hashes: HashCache): Promise<FrictionRun | null>;
|
|
10
|
+
export declare function loadRuns(logDir: string, hashes: HashCache): Promise<FrictionRun[]>;
|
|
11
|
+
export declare function loadFrictionLog(logDir: string, context: {
|
|
12
|
+
worktreePath: string;
|
|
13
|
+
worktree: string;
|
|
14
|
+
}, hashes: HashCache): Promise<FrictionLog | null>;
|
|
15
|
+
export declare function loadFrictionLogs(frictionDir: string, context: {
|
|
16
|
+
worktreePath: string;
|
|
17
|
+
worktree: string;
|
|
18
|
+
}, hashes: HashCache): Promise<FrictionLog[]>;
|
|
19
|
+
/** `MMM d · HH:mm` in local time for `yyyyMMddTHHmmssZ(-N)` run ids. */
|
|
20
|
+
export declare function runDisplayTitle(runId: string): string;
|
|
21
|
+
export declare function stepCountLabel(count: number): string;
|
|
22
|
+
export declare function frictionStatusLabel(status: string | null): string | null;
|