astroshot 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +87 -0
  2. package/bin/astroshot.mjs +71 -0
  3. package/node_modules/@archastro/astroshot/LICENSE +21 -0
  4. package/node_modules/@archastro/astroshot/README.md +96 -0
  5. package/node_modules/@archastro/astroshot/bin/astroshot.mjs +276 -0
  6. package/node_modules/@archastro/astroshot/bin/demo.mjs +275 -0
  7. package/node_modules/@archastro/astroshot/bin/doctor.mjs +492 -0
  8. package/node_modules/@archastro/astroshot/bin/mac-preferences.mjs +346 -0
  9. package/node_modules/@archastro/astroshot/bin/templates.mjs +186 -0
  10. package/node_modules/@archastro/astroshot/fixtures/demo/fixtures.json +42 -0
  11. package/node_modules/@archastro/astroshot/fixtures/demo/journey.png +0 -0
  12. package/node_modules/@archastro/astroshot/fixtures/demo/journey.webm +0 -0
  13. package/node_modules/@archastro/astroshot/fixtures/demo/next-steps.png +0 -0
  14. package/node_modules/@archastro/astroshot/fixtures/demo/welcome.png +0 -0
  15. package/node_modules/@archastro/astroshot/ink.d.ts +5 -0
  16. package/node_modules/@archastro/astroshot/ink.js +1 -0
  17. package/node_modules/@archastro/astroshot/package.json +79 -0
  18. package/node_modules/@archastro/astroshot/pty.d.ts +8 -0
  19. package/node_modules/@archastro/astroshot/pty.js +1 -0
  20. package/node_modules/@archastro/astroshot/react.d.ts +1 -0
  21. package/node_modules/@archastro/astroshot/react.js +1 -0
  22. package/node_modules/@archastro/astroshot/tui.d.ts +1 -0
  23. package/node_modules/@archastro/astroshot/tui.js +1 -0
  24. package/node_modules/@archastro/movie-harness/README.md +138 -0
  25. package/node_modules/@archastro/movie-harness/bin/astroshot-movie.mjs +5 -0
  26. package/node_modules/@archastro/movie-harness/dist/cli.d.ts +1 -0
  27. package/node_modules/@archastro/movie-harness/dist/cli.js +389 -0
  28. package/node_modules/@archastro/movie-harness/dist/encode.d.ts +14 -0
  29. package/node_modules/@archastro/movie-harness/dist/encode.js +184 -0
  30. package/node_modules/@archastro/movie-harness/dist/index.d.ts +14 -0
  31. package/node_modules/@archastro/movie-harness/dist/index.js +11 -0
  32. package/node_modules/@archastro/movie-harness/dist/paths.d.ts +9 -0
  33. package/node_modules/@archastro/movie-harness/dist/paths.js +57 -0
  34. package/node_modules/@archastro/movie-harness/dist/png.d.ts +8 -0
  35. package/node_modules/@archastro/movie-harness/dist/png.js +79 -0
  36. package/node_modules/@archastro/movie-harness/dist/session.d.ts +45 -0
  37. package/node_modules/@archastro/movie-harness/dist/session.js +170 -0
  38. package/node_modules/@archastro/movie-harness/dist/sink.d.ts +7 -0
  39. package/node_modules/@archastro/movie-harness/dist/sink.js +113 -0
  40. package/node_modules/@archastro/movie-harness/dist/source-help.d.ts +29 -0
  41. package/node_modules/@archastro/movie-harness/dist/source-help.js +222 -0
  42. package/node_modules/@archastro/movie-harness/dist/sources/browser.d.ts +7 -0
  43. package/node_modules/@archastro/movie-harness/dist/sources/browser.js +87 -0
  44. package/node_modules/@archastro/movie-harness/dist/sources/desktop-macos.d.ts +110 -0
  45. package/node_modules/@archastro/movie-harness/dist/sources/desktop-macos.js +505 -0
  46. package/node_modules/@archastro/movie-harness/dist/sources/frames-store.d.ts +19 -0
  47. package/node_modules/@archastro/movie-harness/dist/sources/frames-store.js +165 -0
  48. package/node_modules/@archastro/movie-harness/dist/sources/pty.d.ts +15 -0
  49. package/node_modules/@archastro/movie-harness/dist/sources/pty.js +330 -0
  50. package/node_modules/@archastro/movie-harness/dist/terminal-paint.d.ts +27 -0
  51. package/node_modules/@archastro/movie-harness/dist/terminal-paint.js +190 -0
  52. package/node_modules/@archastro/movie-harness/dist/types.d.ts +140 -0
  53. package/node_modules/@archastro/movie-harness/dist/types.js +1 -0
  54. package/node_modules/@archastro/movie-harness/native/macos/WindowTools.swift +164 -0
  55. package/node_modules/@archastro/movie-harness/package.json +67 -0
  56. package/node_modules/@archastro/react-shot/LICENSE +21 -0
  57. package/node_modules/@archastro/react-shot/README.md +181 -0
  58. package/node_modules/@archastro/react-shot/bin/react-shot.mjs +2 -0
  59. package/node_modules/@archastro/react-shot/dist/batch-paths.d.ts +4 -0
  60. package/node_modules/@archastro/react-shot/dist/batch-paths.d.ts.map +1 -0
  61. package/node_modules/@archastro/react-shot/dist/batch-paths.js +34 -0
  62. package/node_modules/@archastro/react-shot/dist/batch-paths.js.map +1 -0
  63. package/node_modules/@archastro/react-shot/dist/cli.d.ts +3 -0
  64. package/node_modules/@archastro/react-shot/dist/cli.d.ts.map +1 -0
  65. package/node_modules/@archastro/react-shot/dist/cli.js +217 -0
  66. package/node_modules/@archastro/react-shot/dist/cli.js.map +1 -0
  67. package/node_modules/@archastro/react-shot/dist/config.d.ts +5 -0
  68. package/node_modules/@archastro/react-shot/dist/config.d.ts.map +1 -0
  69. package/node_modules/@archastro/react-shot/dist/config.js +75 -0
  70. package/node_modules/@archastro/react-shot/dist/config.js.map +1 -0
  71. package/node_modules/@archastro/react-shot/dist/create-server.d.ts +13 -0
  72. package/node_modules/@archastro/react-shot/dist/create-server.d.ts.map +1 -0
  73. package/node_modules/@archastro/react-shot/dist/create-server.js +254 -0
  74. package/node_modules/@archastro/react-shot/dist/create-server.js.map +1 -0
  75. package/node_modules/@archastro/react-shot/dist/index.d.ts +4 -0
  76. package/node_modules/@archastro/react-shot/dist/index.d.ts.map +1 -0
  77. package/node_modules/@archastro/react-shot/dist/index.js +3 -0
  78. package/node_modules/@archastro/react-shot/dist/index.js.map +1 -0
  79. package/node_modules/@archastro/react-shot/dist/meta.d.ts +9 -0
  80. package/node_modules/@archastro/react-shot/dist/meta.d.ts.map +1 -0
  81. package/node_modules/@archastro/react-shot/dist/meta.js +23 -0
  82. package/node_modules/@archastro/react-shot/dist/meta.js.map +1 -0
  83. package/node_modules/@archastro/react-shot/dist/shot.d.ts +4 -0
  84. package/node_modules/@archastro/react-shot/dist/shot.d.ts.map +1 -0
  85. package/node_modules/@archastro/react-shot/dist/shot.js +259 -0
  86. package/node_modules/@archastro/react-shot/dist/shot.js.map +1 -0
  87. package/node_modules/@archastro/react-shot/dist/stubs.d.ts +6 -0
  88. package/node_modules/@archastro/react-shot/dist/stubs.d.ts.map +1 -0
  89. package/node_modules/@archastro/react-shot/dist/stubs.js +49 -0
  90. package/node_modules/@archastro/react-shot/dist/stubs.js.map +1 -0
  91. package/node_modules/@archastro/react-shot/dist/types.d.ts +79 -0
  92. package/node_modules/@archastro/react-shot/dist/types.d.ts.map +1 -0
  93. package/node_modules/@archastro/react-shot/dist/types.js +2 -0
  94. package/node_modules/@archastro/react-shot/dist/types.js.map +1 -0
  95. package/node_modules/@archastro/react-shot/host/index.html +22 -0
  96. package/node_modules/@archastro/react-shot/package.json +81 -0
  97. package/node_modules/@archastro/tui-shot/LICENSE +21 -0
  98. package/node_modules/@archastro/tui-shot/README.md +145 -0
  99. package/node_modules/@archastro/tui-shot/bin/tui-shot.mjs +23 -0
  100. package/node_modules/@archastro/tui-shot/dist/batch-paths.d.ts +3 -0
  101. package/node_modules/@archastro/tui-shot/dist/batch-paths.js +33 -0
  102. package/node_modules/@archastro/tui-shot/dist/cli.d.ts +2 -0
  103. package/node_modules/@archastro/tui-shot/dist/cli.js +224 -0
  104. package/node_modules/@archastro/tui-shot/dist/index.d.ts +3 -0
  105. package/node_modules/@archastro/tui-shot/dist/index.js +2 -0
  106. package/node_modules/@archastro/tui-shot/dist/pty-exit-wrapper.d.ts +1 -0
  107. package/node_modules/@archastro/tui-shot/dist/pty-exit-wrapper.js +50 -0
  108. package/node_modules/@archastro/tui-shot/dist/pty-shot.d.ts +2 -0
  109. package/node_modules/@archastro/tui-shot/dist/pty-shot.js +483 -0
  110. package/node_modules/@archastro/tui-shot/dist/render-ink.d.ts +10 -0
  111. package/node_modules/@archastro/tui-shot/dist/render-ink.js +87 -0
  112. package/node_modules/@archastro/tui-shot/dist/shot.d.ts +24 -0
  113. package/node_modules/@archastro/tui-shot/dist/shot.js +250 -0
  114. package/node_modules/@archastro/tui-shot/dist/terminal-html.d.ts +14 -0
  115. package/node_modules/@archastro/tui-shot/dist/terminal-html.js +144 -0
  116. package/node_modules/@archastro/tui-shot/dist/types.d.ts +81 -0
  117. package/node_modules/@archastro/tui-shot/dist/types.js +1 -0
  118. package/node_modules/@archastro/tui-shot/package.json +90 -0
  119. package/package.json +68 -0
@@ -0,0 +1,138 @@
1
+ # `@archastro/movie-harness`
2
+
3
+ Universal **movie** capture harness for Astroshots. One session API, many
4
+ sources, one `.astroshot/` sink (poster PNG + video + manifest).
5
+
6
+ Also exposed as **`astroshot movie`** from `@archastro/astroshot` (same binary
7
+ agents already install for stills).
8
+
9
+ | Source | Status | Notes |
10
+ |--------|--------|-------|
11
+ | `frames` | **ready** | Push any PNG/JPEG sequence (multi-process CLI) |
12
+ | `browser` | **ready** | Playwright `recordVideo` (headless OK) |
13
+ | `pty` | **ready** | Truecolor SGR → xterm → Chromium samples |
14
+ | `pty-demo` | **ready** | Truecolor smoke without a real program |
15
+ | `desktop.window` | **ready** (macOS) | OS `screencapture -l` + shipped Swift window list |
16
+ | `desktop.display` | stub | Use `desktop.window` or `frames` for now |
17
+
18
+ ## Which source? (for agents)
19
+
20
+ ```bash
21
+ astroshot movie which-source "record a ratatui TUI with truecolor"
22
+ astroshot movie which-source "SwiftUI onboarding window"
23
+ astroshot movie help-sources
24
+ astroshot movie --help # full decision table
25
+ ```
26
+
27
+ Hard rules:
28
+
29
+ 1. **TUI/CLI color** → `pty` (never `desktop.window` on Terminal/iTerm/Ghostty)
30
+ 2. **Web** → `browser` (not a desktop grab of Chrome)
31
+ 3. **Native Mac app** → `desktop.window` (`--bundle-id` / `--window-id`)
32
+ 4. **Custom pixels** → `frames`
33
+
34
+ ## Why
35
+
36
+ Stills already stream through `.astroshot/`. Journeys need movies. Browser and
37
+ PTY are off-screen (headless Chromium). Native desktop is a separate adapter
38
+ behind the same session contract.
39
+
40
+ **PTY color is sacred:** `TERM=xterm-256color`, `COLORTERM=truecolor`, cell paint
41
+ via xterm (RGB / 256 / 16). Never screenshot Terminal.app.
42
+
43
+ ## CLI
44
+
45
+ ```bash
46
+ # From monorepo after build:
47
+ node packages/movie-harness/bin/astroshot-movie.mjs --help
48
+
49
+ # Synthetic frames demo → .astroshot/demo-journey/
50
+ astroshot-movie run --source frames --feature demo-journey --slug flow
51
+
52
+ # Browser journey
53
+ astroshot-movie run --source browser --feature web --slug home \
54
+ --url https://example.com --settle-ms 500
55
+
56
+ # Truecolor terminal smoke (no fixture program)
57
+ astroshot movie run --source pty-demo --feature tui --slug brand
58
+
59
+ # Real PTY fixture
60
+ astroshot movie run --source pty --feature tui --slug flow \
61
+ --fixture ./fixtures/demo.pty.yaml
62
+
63
+ # Native macOS window (uses /usr/sbin/screencapture — already on the Mac)
64
+ astroshot movie list-windows
65
+ astroshot movie run --source desktop.window --feature app --slug onboard \
66
+ --bundle-id com.example.App --duration-ms 4000 --fps 10
67
+
68
+ # Multi-process frames
69
+ astroshot movie start --feature custom --slug walk
70
+ astroshot movie push-frame --feature custom --file ./frame.png
71
+ astroshot movie mark --feature custom --slug step-2
72
+ astroshot movie stop --feature custom --status pass
73
+ ```
74
+
75
+ `desktop.window` needs **Screen Recording** permission for your terminal/IDE
76
+ (System Settings → Privacy & Security). Window listing uses Swift
77
+ (`native/macos/WindowTools.swift`) shipped in the package — no extra download.
78
+
79
+ ## Library
80
+
81
+ ```ts
82
+ import { MovieSession, encodeSolidPng, recordBrowserMovie } from "@archastro/movie-harness";
83
+
84
+ const session = MovieSession.create({
85
+ feature: "install-wizard",
86
+ slug: "journey",
87
+ source: "frames",
88
+ size: { width: 1280, height: 720 },
89
+ fps: 15,
90
+ });
91
+ session.pushFrame(encodeSolidPng(1280, 720, [124, 92, 255]));
92
+ session.mark("purple");
93
+ const artifact = await session.stop({ status: "pass" });
94
+ // artifact.posterPath, artifact.videoPath under .astroshot/
95
+ ```
96
+
97
+ ## Write contract
98
+
99
+ ```text
100
+ .astroshot/<feature>/
101
+ manifest.json
102
+ 0001-journey.png # poster (tray / overlay today)
103
+ 0001-journey.webm # movie
104
+ ```
105
+
106
+ Manifest shot fields (extension):
107
+
108
+ ```json
109
+ {
110
+ "kind": "movie",
111
+ "file": "0001-journey.png",
112
+ "video": "0001-journey.webm",
113
+ "duration_ms": 1200,
114
+ "source": "browser",
115
+ "chapters": [{ "slug": "mid", "t_ms": 400 }]
116
+ }
117
+ ```
118
+
119
+ Phase 0: Astroshots app needs no changes (poster is a normal image).
120
+ Phase 1: open `video` from detail when present.
121
+
122
+ ## Encode
123
+
124
+ 1. **ffmpeg** if on `PATH` (WebM VP9 or MP4 H.264)
125
+ 2. Else **Playwright** slideshow → `recordVideo` WebM (no extra native deps)
126
+
127
+ ## Desktop window (macOS)
128
+
129
+ Uses tooling already on the machine:
130
+
131
+ 1. **Window list** — shipped `WindowTools.swift` via `swift` (Xcode CLT)
132
+ 2. **Frames** — `/usr/sbin/screencapture -l <windowId>`
133
+ 3. **Encode** — same Playwright/ffmpeg path as other sources
134
+
135
+ ```bash
136
+ astroshot movie run --source desktop.window --feature app --slug flow \
137
+ --bundle-id ai.archastro.Astroshots --duration-ms 3000
138
+ ```
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../dist/cli.js";
3
+
4
+ const code = await runCli(process.argv.slice(2));
5
+ process.exit(code);
@@ -0,0 +1 @@
1
+ export declare function runCli(argv: string[]): Promise<number>;
@@ -0,0 +1,389 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { encodeSolidPng } from "./png.js";
4
+ import { resolveRoot } from "./paths.js";
5
+ import { finalizeManifest } from "./sink.js";
6
+ import { formatSourceCatalog, recommendSource, SOURCE_DECISION_TABLE, sourceHintForError, } from "./source-help.js";
7
+ import { recordBrowserMovie } from "./sources/browser.js";
8
+ import { assertDesktopToolchain, checkScreenRecordingAccess, desktopMatchFromFlags, listDesktopWindows, openScreenRecordingSettings, recordDesktopWindowMovie, } from "./sources/desktop-macos.js";
9
+ import { loadFrameSession, markFrameSession, pushFrameToSession, startFrameSession, stopFrameSession, } from "./sources/frames-store.js";
10
+ import { recordPtyMovie, recordTruecolorDemoMovie, } from "./sources/pty.js";
11
+ function usage() {
12
+ return `astroshot movie — universal movie harness → .astroshot/ poster + video
13
+
14
+ ${SOURCE_DECISION_TABLE}
15
+
16
+ Commands
17
+ --------
18
+ which-source [intent…] Recommend a --source for an agent/human intent
19
+ list-windows List macOS windows (id, bundle, title) for desktop.window
20
+ check-screen-access Detect Screen Recording TCC (optional --request prompt)
21
+ open-screen-settings Open System Settings → Screen Recording
22
+ start / push-frame / mark / stop
23
+ Multi-process frames session
24
+ run One-shot recorder (browser | pty | desktop.window | frames | …)
25
+ finalize Set manifest status pass|fail|idle
26
+
27
+ run --source …
28
+ --------------
29
+ browser --url URL | --script path.mjs [--headed] [--settle-ms N]
30
+ pty --fixture path.yaml|json # truecolor TUI — preferred for terminals
31
+ pty-demo [--color 7c5cff] # truecolor smoke, no program
32
+ desktop.window --bundle-id ID | --window-id N | --title-regex RE | --owner NAME | --pid N
33
+ [--duration-ms 3000] [--fps 10] [--cursor] [--allow-blank]
34
+ frames [--demo-frames N] # or use start/push-frame/stop
35
+
36
+ Common options
37
+ --------------
38
+ --feature NAME kebab-case feature dir under .astroshot/ (required for capture)
39
+ --slug SLUG kebab-case movie slug (required for capture)
40
+ --root DIR worktree root (default: git root or cwd)
41
+ --run-id ID stable run identity (share across movies in one journey)
42
+ --title TEXT human title
43
+ --description T what the movie proves
44
+ --size WxH encode size (desktop defaults to window pixels)
45
+ --fps N default 15 (desktop default 10)
46
+ --format webm|mp4
47
+ --status running|pass|fail|idle
48
+ --session ID frames session id (default: latest)
49
+ -h, --help
50
+
51
+ Agent notes
52
+ -----------
53
+ • Prefer the decision table over improvising OS screen recording for web/TUI.
54
+ • Always land poster+video under .astroshot/ so Astroshots streams posters.
55
+ • desktop.window uses macOS screencapture (already on the system) + Swift window list
56
+ shipped in this package — no extra binary download. Needs Screen Recording TCC.
57
+ • For full catalog: astroshot movie help-sources
58
+
59
+ Examples
60
+ --------
61
+ astroshot movie which-source "record a ratatui TUI with truecolor"
62
+ astroshot movie run --source browser --feature web --slug home --url https://example.com
63
+ astroshot movie run --source pty --feature tui --slug flow --fixture ./flow.pty.yaml
64
+ astroshot movie run --source desktop.window --feature app --slug onboard \\
65
+ --bundle-id com.example.App --duration-ms 4000
66
+ astroshot movie list-windows
67
+ `;
68
+ }
69
+ function parseArgs(argv) {
70
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") {
71
+ return { command: "help", args: {}, rest: [] };
72
+ }
73
+ const command = argv[0];
74
+ const args = {};
75
+ const rest = [];
76
+ for (let i = 1; i < argv.length; i++) {
77
+ const token = argv[i];
78
+ if (token === "-h" || token === "--help") {
79
+ args.help = true;
80
+ continue;
81
+ }
82
+ if (!token.startsWith("--")) {
83
+ rest.push(token);
84
+ continue;
85
+ }
86
+ const key = token.slice(2);
87
+ const next = argv[i + 1];
88
+ if (next === undefined || next.startsWith("--")) {
89
+ args[key] = true;
90
+ }
91
+ else {
92
+ args[key] = next;
93
+ i += 1;
94
+ }
95
+ }
96
+ return { command, args, rest };
97
+ }
98
+ function req(args, key) {
99
+ const value = args[key];
100
+ if (typeof value !== "string" || !value) {
101
+ throw new Error(`--${key} is required`);
102
+ }
103
+ return value;
104
+ }
105
+ function opt(args, key) {
106
+ const value = args[key];
107
+ return typeof value === "string" ? value : undefined;
108
+ }
109
+ function parseSize(value) {
110
+ if (!value)
111
+ return undefined;
112
+ const match = /^(\d+)x(\d+)$/.exec(value);
113
+ if (!match)
114
+ throw new Error(`--size must be WxH, got ${value}`);
115
+ return { width: Number(match[1]), height: Number(match[2]) };
116
+ }
117
+ function parseFormat(value) {
118
+ if (!value)
119
+ return undefined;
120
+ if (value !== "webm" && value !== "mp4") {
121
+ throw new Error(`--format must be webm|mp4`);
122
+ }
123
+ return value;
124
+ }
125
+ function parseFps(value) {
126
+ if (!value)
127
+ return undefined;
128
+ const n = Number(value);
129
+ if (!(n > 0 && n <= 60))
130
+ throw new Error(`--fps must be in (0, 60]`);
131
+ return n;
132
+ }
133
+ export async function runCli(argv) {
134
+ try {
135
+ const { command, args, rest } = parseArgs(argv);
136
+ if (command === "help" || args.help) {
137
+ process.stdout.write(`${usage()}\n`);
138
+ return 0;
139
+ }
140
+ switch (command) {
141
+ case "which-source":
142
+ return cmdWhichSource(rest, args);
143
+ case "help-sources":
144
+ process.stdout.write(`${formatSourceCatalog()}\n`);
145
+ return 0;
146
+ case "list-windows":
147
+ return cmdListWindows();
148
+ case "check-screen-access":
149
+ return cmdCheckScreenAccess(args);
150
+ case "open-screen-settings":
151
+ return cmdOpenScreenSettings();
152
+ case "start":
153
+ return cmdStart(args);
154
+ case "push-frame":
155
+ return cmdPushFrame(args);
156
+ case "mark":
157
+ return cmdMark(args);
158
+ case "stop":
159
+ return await cmdStop(args);
160
+ case "run":
161
+ return await cmdRun(args);
162
+ case "finalize":
163
+ return cmdFinalize(args);
164
+ default:
165
+ throw new Error(`unknown command: ${command}\n\n${usage()}`);
166
+ }
167
+ }
168
+ catch (error) {
169
+ const message = error instanceof Error ? error.message : String(error);
170
+ process.stderr.write(`error: ${message}\n`);
171
+ if (!/which-source|decision table|list-windows/i.test(message)) {
172
+ process.stderr.write(`hint: ${sourceHintForError()}\n`);
173
+ }
174
+ return 1;
175
+ }
176
+ }
177
+ function cmdWhichSource(rest, args) {
178
+ const intent = rest.join(" ") ||
179
+ opt(args, "intent") ||
180
+ opt(args, "for") ||
181
+ "";
182
+ if (!intent) {
183
+ process.stdout.write(`${SOURCE_DECISION_TABLE}\n\n` +
184
+ `Pass free text, e.g.:\n` +
185
+ ` astroshot movie which-source "native SwiftUI onboarding window"\n` +
186
+ ` astroshot movie which-source "ratatui truecolor dashboard"\n`);
187
+ return 0;
188
+ }
189
+ const advice = recommendSource(intent);
190
+ process.stdout.write(`${JSON.stringify({ intent, recommended: advice.source, reason: advice.reason }, null, 2)}\n`);
191
+ return 0;
192
+ }
193
+ function cmdListWindows() {
194
+ assertDesktopToolchain();
195
+ const windows = listDesktopWindows();
196
+ process.stdout.write(`${JSON.stringify(windows, null, 2)}\n`);
197
+ process.stderr.write(`# ${windows.length} windows — match with --window-id / --bundle-id / --title-regex / --owner / --pid\n`);
198
+ return 0;
199
+ }
200
+ function cmdCheckScreenAccess(args) {
201
+ assertDesktopToolchain();
202
+ const request = Boolean(args.request);
203
+ const report = checkScreenRecordingAccess({ request });
204
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
205
+ if (!report.granted) {
206
+ process.stderr.write(`\nScreen Recording: DENIED (enable "${report.enableApp}")\n` +
207
+ ` → ${report.settingsHint}\n` +
208
+ ` → or: astroshot movie open-screen-settings\n` +
209
+ ` → then quit & reopen ${report.enableApp}, re-run check-screen-access\n`);
210
+ if (args["open-settings"] || args.open) {
211
+ openScreenRecordingSettings();
212
+ }
213
+ return 2;
214
+ }
215
+ process.stderr.write(`# Screen Recording preflight: OK — still enable "${report.enableApp}" in Settings if captures fail\n`);
216
+ return 0;
217
+ }
218
+ function cmdOpenScreenSettings() {
219
+ assertDesktopToolchain();
220
+ const ok = openScreenRecordingSettings();
221
+ if (!ok) {
222
+ process.stderr.write("error: could not open System Settings; open Privacy & Security → Screen Recording manually\n");
223
+ return 1;
224
+ }
225
+ process.stderr.write("Opened System Settings (Screen Recording). Enable the app that launched this command, quit it fully, reopen, then:\n" +
226
+ " astroshot movie check-screen-access\n");
227
+ return 0;
228
+ }
229
+ function cmdStart(args) {
230
+ const state = startFrameSession({
231
+ feature: req(args, "feature"),
232
+ slug: req(args, "slug"),
233
+ root: opt(args, "root"),
234
+ runId: opt(args, "run-id"),
235
+ title: opt(args, "title"),
236
+ description: opt(args, "description"),
237
+ size: parseSize(opt(args, "size")),
238
+ fps: parseFps(opt(args, "fps")),
239
+ format: parseFormat(opt(args, "format")),
240
+ });
241
+ process.stdout.write(`${JSON.stringify({
242
+ id: state.id,
243
+ feature: state.feature,
244
+ slug: state.slug,
245
+ runId: state.runId,
246
+ frameDir: state.frameDir,
247
+ }, null, 2)}\n`);
248
+ return 0;
249
+ }
250
+ function cmdPushFrame(args) {
251
+ const state = loadFrameSession(opt(args, "root") ?? resolveRoot(), req(args, "feature"), opt(args, "session"));
252
+ const file = req(args, "file");
253
+ const next = pushFrameToSession(state, path.resolve(file));
254
+ process.stdout.write(`${JSON.stringify({ id: next.id, frameCount: next.frameCount }, null, 2)}\n`);
255
+ return 0;
256
+ }
257
+ function cmdMark(args) {
258
+ const state = loadFrameSession(opt(args, "root") ?? resolveRoot(), req(args, "feature"), opt(args, "session"));
259
+ const next = markFrameSession(state, req(args, "slug"), opt(args, "note"));
260
+ process.stdout.write(`${JSON.stringify({ id: next.id, chapters: next.chapters }, null, 2)}\n`);
261
+ return 0;
262
+ }
263
+ async function cmdStop(args) {
264
+ const state = loadFrameSession(opt(args, "root") ?? resolveRoot(), req(args, "feature"), opt(args, "session"));
265
+ const status = (opt(args, "status") ?? "running");
266
+ const artifact = await stopFrameSession(state, { status });
267
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
268
+ return 0;
269
+ }
270
+ async function cmdRun(args) {
271
+ const source = req(args, "source");
272
+ const feature = req(args, "feature");
273
+ const slug = req(args, "slug");
274
+ const common = {
275
+ feature,
276
+ slug,
277
+ root: opt(args, "root"),
278
+ runId: opt(args, "run-id"),
279
+ title: opt(args, "title"),
280
+ description: opt(args, "description"),
281
+ size: parseSize(opt(args, "size")),
282
+ fps: parseFps(opt(args, "fps")),
283
+ format: parseFormat(opt(args, "format")),
284
+ status: (opt(args, "status") ?? "running"),
285
+ };
286
+ if (source === "frames") {
287
+ const count = Number(opt(args, "demo-frames") ?? "8");
288
+ const size = common.size ?? { width: 320, height: 180 };
289
+ let state = startFrameSession({ ...common, size });
290
+ const tmpDir = path.join(state.frameDir, "..", "demo-src");
291
+ fs.mkdirSync(tmpDir, { recursive: true });
292
+ const colors = [
293
+ [124, 92, 255],
294
+ [90, 200, 250],
295
+ [80, 220, 160],
296
+ [240, 180, 110],
297
+ [240, 114, 122],
298
+ [232, 232, 242],
299
+ [90, 90, 122],
300
+ [9, 10, 18],
301
+ ];
302
+ for (let i = 0; i < count; i++) {
303
+ const rgb = colors[i % colors.length];
304
+ const png = encodeSolidPng(size.width, size.height, rgb);
305
+ const file = path.join(tmpDir, `f-${i}.png`);
306
+ fs.writeFileSync(file, png);
307
+ state = pushFrameToSession(state, file);
308
+ if (i === Math.floor(count / 2)) {
309
+ state = markFrameSession(state, "midpoint");
310
+ }
311
+ }
312
+ const artifact = await stopFrameSession(state, {
313
+ status: common.status,
314
+ });
315
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
316
+ return 0;
317
+ }
318
+ if (source === "browser") {
319
+ const artifact = await recordBrowserMovie({
320
+ ...common,
321
+ url: opt(args, "url"),
322
+ scriptPath: opt(args, "script"),
323
+ headed: Boolean(args.headed),
324
+ settleMs: opt(args, "settle-ms")
325
+ ? Number(opt(args, "settle-ms"))
326
+ : undefined,
327
+ });
328
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
329
+ return 0;
330
+ }
331
+ if (source === "pty") {
332
+ const artifact = await recordPtyMovie({
333
+ ...common,
334
+ fixturePath: req(args, "fixture"),
335
+ });
336
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
337
+ return 0;
338
+ }
339
+ if (source === "pty-demo") {
340
+ const artifact = await recordTruecolorDemoMovie({
341
+ ...common,
342
+ color: opt(args, "color"),
343
+ });
344
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
345
+ return 0;
346
+ }
347
+ if (source === "desktop.window") {
348
+ assertDesktopToolchain();
349
+ const match = desktopMatchFromFlags({
350
+ "window-id": opt(args, "window-id"),
351
+ "bundle-id": opt(args, "bundle-id"),
352
+ "title-regex": opt(args, "title-regex"),
353
+ owner: opt(args, "owner"),
354
+ pid: opt(args, "pid"),
355
+ pick: opt(args, "pick"),
356
+ });
357
+ const durationMs = opt(args, "duration-ms")
358
+ ? Number(opt(args, "duration-ms"))
359
+ : 3_000;
360
+ const artifact = await recordDesktopWindowMovie({
361
+ ...common,
362
+ match,
363
+ durationMs,
364
+ cursor: Boolean(args.cursor),
365
+ allowBlank: Boolean(args["allow-blank"]),
366
+ fps: common.fps ?? 10,
367
+ });
368
+ process.stdout.write(`${JSON.stringify(artifact, null, 2)}\n`);
369
+ return 0;
370
+ }
371
+ if (source === "desktop.display" || source === "desktop.region") {
372
+ throw new Error(`${source} is not implemented yet.\n` +
373
+ `Use --source desktop.window for a single app, or --source frames.\n` +
374
+ sourceHintForError("desktop.window"));
375
+ }
376
+ throw new Error(`unknown --source ${JSON.stringify(source)}\n${SOURCE_DECISION_TABLE}`);
377
+ }
378
+ function cmdFinalize(args) {
379
+ const root = resolveRoot(opt(args, "root"));
380
+ const feature = req(args, "feature");
381
+ const runId = req(args, "run-id");
382
+ const status = req(args, "status");
383
+ if (!["pass", "fail", "idle", "running"].includes(status)) {
384
+ throw new Error("--status must be pass|fail|idle|running");
385
+ }
386
+ finalizeManifest(root, feature, runId, status);
387
+ process.stdout.write(`${JSON.stringify({ feature, runId, status }, null, 2)}\n`);
388
+ return 0;
389
+ }
@@ -0,0 +1,14 @@
1
+ import type { EncodeFramesRequest } from "./types.js";
2
+ /**
3
+ * Encode a PNG sequence to WebM/MP4.
4
+ *
5
+ * Prefer ffmpeg when present (fast, high quality). Otherwise replay frames in
6
+ * headless Chromium with Playwright recordVideo — no native deps beyond the
7
+ * Chromium we already ship for stills.
8
+ */
9
+ export declare function encodeFrames(request: EncodeFramesRequest): Promise<{
10
+ videoPath: string;
11
+ durationMs: number;
12
+ }>;
13
+ /** Copy the last frame as the poster PNG. */
14
+ export declare function posterFromFrames(framePaths: string[], outPath: string): string;