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,346 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-only access to the Astroshots macOS app's real preferences.
|
|
3
|
+
*
|
|
4
|
+
* Two traps make the obvious implementations wrong:
|
|
5
|
+
*
|
|
6
|
+
* 1. Whole-domain plist → JSON conversion can never work. The domain contains
|
|
7
|
+
* AppKit's `NSOSPLastRootDirectory` open-panel bookmark (CFData), and JSON
|
|
8
|
+
* has no representation for it, so `plutil -convert json` aborts with
|
|
9
|
+
* "Invalid object in plist for JSON format" on effectively every real user's
|
|
10
|
+
* machine. Extract one key at a time instead.
|
|
11
|
+
* 2. `~/Library/Preferences/<domain>.plist` is a lazily flushed cache of
|
|
12
|
+
* cfprefsd state, so reading the file can report yesterday's configuration.
|
|
13
|
+
* Go through cfprefsd (`defaults export`) first and treat the file as a
|
|
14
|
+
* fallback only.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here writes: `astroshot doctor` must not mutate app state.
|
|
17
|
+
*/
|
|
18
|
+
import fs from "node:fs";
|
|
19
|
+
import os from "node:os";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
|
|
23
|
+
export const ASTROSHOTS_DOMAIN = "ai.archastro.Astroshots";
|
|
24
|
+
|
|
25
|
+
const MISSING_KEY_PATTERN =
|
|
26
|
+
/No value at that key path|invalid key path|Invalid object in plist/i;
|
|
27
|
+
|
|
28
|
+
// Resolve Apple's tools by absolute path. They always live in /usr/bin, and a
|
|
29
|
+
// minimal or empty PATH must never be mistaken for "this user has no watch
|
|
30
|
+
// roots" — that is the exact false negative doctor exists to eliminate.
|
|
31
|
+
const DEFAULTS_BIN = "/usr/bin/defaults";
|
|
32
|
+
const PLUTIL_BIN = "/usr/bin/plutil";
|
|
33
|
+
|
|
34
|
+
/** Whether the tools this module shells out to are actually present. */
|
|
35
|
+
export function preferenceToolsAvailable({ platform = process.platform } = {}) {
|
|
36
|
+
if (platform !== "darwin") return { available: false, missing: [] };
|
|
37
|
+
const missing = [DEFAULTS_BIN, PLUTIL_BIN].filter(
|
|
38
|
+
(binary) => !fs.existsSync(binary),
|
|
39
|
+
);
|
|
40
|
+
return { available: missing.length === 0, missing };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Snapshot the preference domain as an XML plist.
|
|
45
|
+
*
|
|
46
|
+
* `defaults export` goes through cfprefsd, so it observes what the app itself
|
|
47
|
+
* sees. The on-disk plist is only used when cfprefsd is unavailable.
|
|
48
|
+
*/
|
|
49
|
+
export function readPreferenceDomain(
|
|
50
|
+
domain = ASTROSHOTS_DOMAIN,
|
|
51
|
+
{ home = os.homedir(), platform = process.platform } = {},
|
|
52
|
+
) {
|
|
53
|
+
if (platform !== "darwin") {
|
|
54
|
+
return { available: false, source: null, reason: "not-macos" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const exported = spawnSync(DEFAULTS_BIN, ["export", domain, "-"], {
|
|
58
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
59
|
+
});
|
|
60
|
+
if (!exported.error && exported.status === 0 && exported.stdout?.length) {
|
|
61
|
+
return { available: true, source: "cfprefsd", plist: exported.stdout };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const plistPath = path.join(
|
|
65
|
+
home,
|
|
66
|
+
"Library",
|
|
67
|
+
"Preferences",
|
|
68
|
+
`${domain}.plist`,
|
|
69
|
+
);
|
|
70
|
+
try {
|
|
71
|
+
const bytes = fs.readFileSync(plistPath);
|
|
72
|
+
return {
|
|
73
|
+
available: true,
|
|
74
|
+
source: "plist-file",
|
|
75
|
+
plist: bytes,
|
|
76
|
+
plistPath,
|
|
77
|
+
staleRisk: true,
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
// `defaults` failing to launch is a tool failure, not evidence that the
|
|
81
|
+
// domain is absent.
|
|
82
|
+
if (exported.error) {
|
|
83
|
+
return {
|
|
84
|
+
available: false,
|
|
85
|
+
source: null,
|
|
86
|
+
reason: "tool-unavailable",
|
|
87
|
+
error: exported.error.message,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
available: false,
|
|
92
|
+
source: null,
|
|
93
|
+
reason:
|
|
94
|
+
exported.status === 1 && !exported.stdout?.length
|
|
95
|
+
? "domain-not-found"
|
|
96
|
+
: "unreadable",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract a single preference key from a plist snapshot.
|
|
103
|
+
*
|
|
104
|
+
* Three outcomes, deliberately distinct:
|
|
105
|
+
* - `{ present: true, raw }` the key exists
|
|
106
|
+
* - `{ present: false }` the key is genuinely absent
|
|
107
|
+
* - `{ present: false, failed: true }` the tool could not answer
|
|
108
|
+
*
|
|
109
|
+
* A missing key exits non-zero with the same "Invalid object" wording as a real
|
|
110
|
+
* failure, so absence is matched explicitly. Everything else — `plutil`
|
|
111
|
+
* missing, a spawn error, or an unrecognized non-zero exit — is a tool failure.
|
|
112
|
+
* Collapsing the third case into absence would make a correctly configured user
|
|
113
|
+
* read as "setup never completed".
|
|
114
|
+
*/
|
|
115
|
+
export function extractPreferenceKey(plist, key, format = "raw") {
|
|
116
|
+
const result = spawnSync(
|
|
117
|
+
PLUTIL_BIN,
|
|
118
|
+
["-extract", key, format, "-o", "-", "-"],
|
|
119
|
+
{ input: plist, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 },
|
|
120
|
+
);
|
|
121
|
+
if (result.error) {
|
|
122
|
+
return { present: false, failed: true, error: result.error.message };
|
|
123
|
+
}
|
|
124
|
+
if (result.status !== 0) {
|
|
125
|
+
if (MISSING_KEY_PATTERN.test(result.stderr ?? "")) {
|
|
126
|
+
return { present: false };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
present: false,
|
|
130
|
+
failed: true,
|
|
131
|
+
error: (result.stderr ?? "").trim() || `plutil exited ${result.status}`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return { present: true, raw: result.stdout };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function extractStringArray(plist, key) {
|
|
138
|
+
const extracted = extractPreferenceKey(plist, key, "json");
|
|
139
|
+
if (!extracted.present) {
|
|
140
|
+
return { present: false, failed: extracted.failed, error: extracted.error };
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const parsed = JSON.parse(extracted.raw);
|
|
144
|
+
if (!Array.isArray(parsed)) return { present: false };
|
|
145
|
+
return {
|
|
146
|
+
present: true,
|
|
147
|
+
value: parsed.filter((entry) => typeof entry === "string"),
|
|
148
|
+
};
|
|
149
|
+
} catch (error) {
|
|
150
|
+
// The key exists but did not decode: unreadable, not absent.
|
|
151
|
+
return {
|
|
152
|
+
present: false,
|
|
153
|
+
failed: true,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function extractString(plist, key) {
|
|
160
|
+
const extracted = extractPreferenceKey(plist, key, "raw");
|
|
161
|
+
if (!extracted.present) {
|
|
162
|
+
return { present: false, failed: extracted.failed, error: extracted.error };
|
|
163
|
+
}
|
|
164
|
+
const value = extracted.raw.replace(/\n$/, "");
|
|
165
|
+
return { present: true, value };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractBoolean(plist, key) {
|
|
169
|
+
const extracted = extractString(plist, key);
|
|
170
|
+
if (!extracted.present) {
|
|
171
|
+
return { present: false, failed: extracted.failed };
|
|
172
|
+
}
|
|
173
|
+
return { present: true, value: /^(true|1|yes)$/i.test(extracted.value) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Mirror of `Preferences.normalizeWatchRootPaths` in the Swift app: expand
|
|
178
|
+
* tildes, standardize, resolve symlinks, drop duplicates, and drop roots
|
|
179
|
+
* already covered recursively by an earlier root.
|
|
180
|
+
*/
|
|
181
|
+
export function normalizeWatchRootPaths(paths, { home = os.homedir() } = {}) {
|
|
182
|
+
const result = [];
|
|
183
|
+
for (const rawPath of paths) {
|
|
184
|
+
if (typeof rawPath !== "string" || rawPath.length === 0) continue;
|
|
185
|
+
const candidate = normalizePath(rawPath, { home });
|
|
186
|
+
if (result.some((root) => pathIsInside(root, candidate))) continue;
|
|
187
|
+
for (let index = result.length - 1; index >= 0; index -= 1) {
|
|
188
|
+
if (pathIsInside(candidate, result[index])) result.splice(index, 1);
|
|
189
|
+
}
|
|
190
|
+
result.push(candidate);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Expand `~`, make absolute, and resolve symlinks as far as the path exists. */
|
|
196
|
+
export function normalizePath(rawPath, { home = os.homedir() } = {}) {
|
|
197
|
+
let expanded = rawPath;
|
|
198
|
+
if (expanded === "~") expanded = home;
|
|
199
|
+
else if (expanded.startsWith("~/")) expanded = path.join(home, expanded.slice(2));
|
|
200
|
+
const absolute = path.resolve(expanded);
|
|
201
|
+
try {
|
|
202
|
+
return fs.realpathSync.native(absolute);
|
|
203
|
+
} catch {
|
|
204
|
+
// Resolve the deepest existing ancestor so a missing leaf still normalizes.
|
|
205
|
+
const parts = absolute.split(path.sep);
|
|
206
|
+
for (let depth = parts.length - 1; depth > 1; depth -= 1) {
|
|
207
|
+
const ancestor = parts.slice(0, depth).join(path.sep);
|
|
208
|
+
try {
|
|
209
|
+
const real = fs.realpathSync.native(ancestor);
|
|
210
|
+
return path.join(real, ...parts.slice(depth));
|
|
211
|
+
} catch {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return absolute;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Containment on path-component boundaries, so `/Users/x/proj-two` is not
|
|
221
|
+
* treated as living inside `/Users/x/proj`.
|
|
222
|
+
*/
|
|
223
|
+
export function pathIsInside(root, candidate) {
|
|
224
|
+
if (root === candidate) return true;
|
|
225
|
+
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
226
|
+
return candidate.startsWith(prefix);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The app's live watch configuration.
|
|
231
|
+
*
|
|
232
|
+
* `watchRoots` (string array) is authoritative; the legacy singular
|
|
233
|
+
* `watchRoot` is still honored when `watchRoots` was never written so upgrades
|
|
234
|
+
* keep the folder the user already chose.
|
|
235
|
+
*/
|
|
236
|
+
export function readWatchConfiguration({
|
|
237
|
+
domain = ASTROSHOTS_DOMAIN,
|
|
238
|
+
home = os.homedir(),
|
|
239
|
+
platform = process.platform,
|
|
240
|
+
// Seam for tests: lets a suite supply a plist snapshot (including a corrupt
|
|
241
|
+
// one) without breaking the host's /usr/bin.
|
|
242
|
+
readDomain = readPreferenceDomain,
|
|
243
|
+
} = {}) {
|
|
244
|
+
const snapshot = readDomain(domain, { home, platform });
|
|
245
|
+
if (!snapshot.available) {
|
|
246
|
+
return {
|
|
247
|
+
available: false,
|
|
248
|
+
reason: snapshot.reason,
|
|
249
|
+
source: null,
|
|
250
|
+
roots: [],
|
|
251
|
+
hasCompletedFirstRunSetup: false,
|
|
252
|
+
usedLegacyKey: false,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const modern = extractStringArray(snapshot.plist, "watchRoots");
|
|
257
|
+
const legacy = modern.present
|
|
258
|
+
? { present: false }
|
|
259
|
+
: extractString(snapshot.plist, "watchRoot");
|
|
260
|
+
const firstRun = extractBoolean(snapshot.plist, "hasCompletedFirstRunSetup");
|
|
261
|
+
|
|
262
|
+
// If the extraction tool could not answer, we know nothing about this user's
|
|
263
|
+
// setup. Reporting "no watch roots" here would tell a correctly configured
|
|
264
|
+
// user to redo first-run setup, so surface the unreadable state instead.
|
|
265
|
+
const failure = [modern, legacy, firstRun].find((result) => result.failed);
|
|
266
|
+
if (failure) {
|
|
267
|
+
return {
|
|
268
|
+
available: false,
|
|
269
|
+
reason: "tool-unavailable",
|
|
270
|
+
error: failure.error,
|
|
271
|
+
source: snapshot.source,
|
|
272
|
+
roots: [],
|
|
273
|
+
hasCompletedFirstRunSetup: false,
|
|
274
|
+
usedLegacyKey: false,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const stored = modern.present
|
|
279
|
+
? modern.value
|
|
280
|
+
: legacy.present && legacy.value
|
|
281
|
+
? [legacy.value]
|
|
282
|
+
: [];
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
available: true,
|
|
286
|
+
source: snapshot.source,
|
|
287
|
+
staleRisk: Boolean(snapshot.staleRisk),
|
|
288
|
+
plistPath: snapshot.plistPath,
|
|
289
|
+
roots: normalizeWatchRootPaths(stored, { home }),
|
|
290
|
+
storedRoots: stored,
|
|
291
|
+
usedLegacyKey: !modern.present && legacy.present,
|
|
292
|
+
hasCompletedFirstRunSetup: firstRun.present ? firstRun.value : false,
|
|
293
|
+
hasConfiguredWatchRoots: modern.present || legacy.present,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Classify a project directory against the app's watch roots.
|
|
299
|
+
*
|
|
300
|
+
* Each outcome needs a different remediation, so they stay distinct instead of
|
|
301
|
+
* collapsing into "not watched":
|
|
302
|
+
* - `setup-incomplete` first-run folder setup never finished
|
|
303
|
+
* - `outside-roots` setup finished, but this project is not covered
|
|
304
|
+
* - `inside-root` covered by `matchedRoot`
|
|
305
|
+
* - `unknown` the configuration could not be read at all — never
|
|
306
|
+
* claim anything about the user's setup here
|
|
307
|
+
* - `unsupported` not macOS
|
|
308
|
+
*/
|
|
309
|
+
export function evaluateWatchCoverage(
|
|
310
|
+
projectPath,
|
|
311
|
+
configuration,
|
|
312
|
+
{ home = os.homedir() } = {},
|
|
313
|
+
) {
|
|
314
|
+
if (!configuration.available) {
|
|
315
|
+
return {
|
|
316
|
+
state: configuration.reason === "not-macos" ? "unsupported" : "unknown",
|
|
317
|
+
reason: configuration.reason,
|
|
318
|
+
error: configuration.error,
|
|
319
|
+
roots: [],
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const normalizedProject = normalizePath(projectPath, { home });
|
|
323
|
+
const matchedRoot =
|
|
324
|
+
configuration.roots.find((root) => pathIsInside(root, normalizedProject)) ??
|
|
325
|
+
null;
|
|
326
|
+
if (matchedRoot) {
|
|
327
|
+
return {
|
|
328
|
+
state: "inside-root",
|
|
329
|
+
matchedRoot,
|
|
330
|
+
projectPath: normalizedProject,
|
|
331
|
+
roots: configuration.roots,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (!configuration.hasCompletedFirstRunSetup || configuration.roots.length === 0) {
|
|
335
|
+
return {
|
|
336
|
+
state: "setup-incomplete",
|
|
337
|
+
projectPath: normalizedProject,
|
|
338
|
+
roots: configuration.roots,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
return {
|
|
342
|
+
state: "outside-roots",
|
|
343
|
+
projectPath: normalizedProject,
|
|
344
|
+
roots: configuration.roots,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const REACT_TEMPLATE = `import type { ReactShotFixture } from "@archastro/astroshot/react";
|
|
5
|
+
|
|
6
|
+
function ExampleCard() {
|
|
7
|
+
return (
|
|
8
|
+
<article
|
|
9
|
+
data-astroshot
|
|
10
|
+
style={{
|
|
11
|
+
width: 420,
|
|
12
|
+
padding: 24,
|
|
13
|
+
borderRadius: 16,
|
|
14
|
+
background: "#f8fafc",
|
|
15
|
+
color: "#0f172a",
|
|
16
|
+
fontFamily: "system-ui, sans-serif",
|
|
17
|
+
boxShadow: "0 20px 50px rgba(15, 23, 42, 0.16)",
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<h1 style={{ margin: "0 0 8px", fontSize: 24 }}>React fixture</h1>
|
|
21
|
+
<p style={{ margin: 0 }}>Replace this component with the state you want to capture.</p>
|
|
22
|
+
</article>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
width: 720,
|
|
28
|
+
height: 420,
|
|
29
|
+
background: "#e2e8f0",
|
|
30
|
+
selector: "[data-astroshot]",
|
|
31
|
+
waitFor: "text=React fixture",
|
|
32
|
+
component: <ExampleCard />,
|
|
33
|
+
} satisfies ReactShotFixture;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const INK_TEMPLATE = `import React from "react";
|
|
37
|
+
import { Box, Text } from "ink";
|
|
38
|
+
import type { InkShotFixture } from "@archastro/astroshot/ink";
|
|
39
|
+
|
|
40
|
+
export default {
|
|
41
|
+
cols: 64,
|
|
42
|
+
rows: 12,
|
|
43
|
+
expectText: ["Ink fixture", "Ready to capture"],
|
|
44
|
+
component: (
|
|
45
|
+
<Box borderStyle="round" borderColor="cyan" paddingX={1} flexDirection="column">
|
|
46
|
+
<Text bold color="cyan">Ink fixture</Text>
|
|
47
|
+
<Text>Ready to capture</Text>
|
|
48
|
+
</Box>
|
|
49
|
+
),
|
|
50
|
+
} satisfies InkShotFixture;
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const PTY_TEMPLATE = `version: 1
|
|
54
|
+
command: ./target/debug/my-tui
|
|
55
|
+
args: []
|
|
56
|
+
cwd: .
|
|
57
|
+
cols: 100
|
|
58
|
+
rows: 30
|
|
59
|
+
timeoutMs: 15000
|
|
60
|
+
actions:
|
|
61
|
+
- waitFor: "Choose an option"
|
|
62
|
+
- key: down
|
|
63
|
+
- key: enter
|
|
64
|
+
- waitFor: "Ready"
|
|
65
|
+
expectText:
|
|
66
|
+
- "Ready"
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const PTY_JSON_TEMPLATE = `${JSON.stringify(
|
|
70
|
+
{
|
|
71
|
+
version: 1,
|
|
72
|
+
command: "./target/debug/my-tui",
|
|
73
|
+
args: [],
|
|
74
|
+
cwd: ".",
|
|
75
|
+
cols: 100,
|
|
76
|
+
rows: 30,
|
|
77
|
+
timeoutMs: 15000,
|
|
78
|
+
actions: [
|
|
79
|
+
{ waitFor: "Choose an option" },
|
|
80
|
+
{ key: "down" },
|
|
81
|
+
{ key: "enter" },
|
|
82
|
+
{ waitFor: "Ready" },
|
|
83
|
+
],
|
|
84
|
+
expectText: ["Ready"],
|
|
85
|
+
},
|
|
86
|
+
null,
|
|
87
|
+
2,
|
|
88
|
+
)}\n`;
|
|
89
|
+
|
|
90
|
+
const TEMPLATES = {
|
|
91
|
+
react: {
|
|
92
|
+
defaultPath: "react.shot.tsx",
|
|
93
|
+
label: "React",
|
|
94
|
+
source: REACT_TEMPLATE,
|
|
95
|
+
},
|
|
96
|
+
ink: {
|
|
97
|
+
defaultPath: "ink.shot.tsx",
|
|
98
|
+
label: "Ink",
|
|
99
|
+
source: INK_TEMPLATE,
|
|
100
|
+
},
|
|
101
|
+
pty: {
|
|
102
|
+
defaultPath: "pty.shot.yaml",
|
|
103
|
+
label: "PTY",
|
|
104
|
+
source: PTY_TEMPLATE,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function canonicalTemplateMode(mode) {
|
|
109
|
+
return mode === "tui" ? "ink" : mode;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function writeFixtureTemplate({
|
|
113
|
+
mode,
|
|
114
|
+
outputPath,
|
|
115
|
+
force = false,
|
|
116
|
+
cwd = process.cwd(),
|
|
117
|
+
}) {
|
|
118
|
+
const canonicalMode = canonicalTemplateMode(mode);
|
|
119
|
+
const template = TEMPLATES[canonicalMode];
|
|
120
|
+
if (!template) {
|
|
121
|
+
throw new Error("init requires one of: react, ink, pty");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requestedPath = outputPath ?? template.defaultPath;
|
|
125
|
+
const absolutePath = path.resolve(cwd, requestedPath);
|
|
126
|
+
const expectedExtension = canonicalMode === "pty" ? /\.(?:ya?ml|json)$/i : /\.tsx$/i;
|
|
127
|
+
if (!expectedExtension.test(absolutePath)) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
canonicalMode === "pty"
|
|
130
|
+
? "PTY fixture templates must use .yaml, .yml, or .json"
|
|
131
|
+
: `${template.label} fixture templates must use .tsx`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parentDirectory = path.dirname(absolutePath);
|
|
136
|
+
fs.mkdirSync(parentDirectory, { recursive: true });
|
|
137
|
+
const source =
|
|
138
|
+
canonicalMode === "pty" && path.extname(absolutePath).toLowerCase() === ".json"
|
|
139
|
+
? PTY_JSON_TEMPLATE
|
|
140
|
+
: template.source;
|
|
141
|
+
if (!force) {
|
|
142
|
+
try {
|
|
143
|
+
fs.writeFileSync(absolutePath, source, {
|
|
144
|
+
encoding: "utf8",
|
|
145
|
+
flag: "wx",
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (error?.code === "EEXIST") {
|
|
149
|
+
throw new Error(`Refusing to overwrite ${absolutePath}; pass --force to replace it`);
|
|
150
|
+
}
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
let existing;
|
|
155
|
+
try {
|
|
156
|
+
existing = fs.lstatSync(absolutePath);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if (error?.code !== "ENOENT") throw error;
|
|
159
|
+
}
|
|
160
|
+
if (existing?.isSymbolicLink()) {
|
|
161
|
+
throw new Error(`Refusing to replace symbolic link ${absolutePath}`);
|
|
162
|
+
}
|
|
163
|
+
if (existing?.isDirectory()) {
|
|
164
|
+
throw new Error(`Refusing to replace directory ${absolutePath}`);
|
|
165
|
+
}
|
|
166
|
+
const temporaryPath = path.join(
|
|
167
|
+
parentDirectory,
|
|
168
|
+
`.${path.basename(absolutePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
169
|
+
);
|
|
170
|
+
try {
|
|
171
|
+
fs.writeFileSync(temporaryPath, source, {
|
|
172
|
+
encoding: "utf8",
|
|
173
|
+
flag: "wx",
|
|
174
|
+
});
|
|
175
|
+
fs.renameSync(temporaryPath, absolutePath);
|
|
176
|
+
} finally {
|
|
177
|
+
fs.rmSync(temporaryPath, { force: true });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
absolutePath,
|
|
183
|
+
label: template.label,
|
|
184
|
+
mode: canonicalMode,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"generated_by": "npm run build:demo-fixtures --workspace @archastro/astroshot",
|
|
4
|
+
"viewport": "720x450",
|
|
5
|
+
"shots": [
|
|
6
|
+
{
|
|
7
|
+
"asset": "welcome.png",
|
|
8
|
+
"slug": "welcome",
|
|
9
|
+
"title": "Watch path works",
|
|
10
|
+
"description": "astroshot demo wrote this still into .astroshot/ with zero prerequisites."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"asset": "next-steps.png",
|
|
14
|
+
"slug": "next-steps",
|
|
15
|
+
"title": "Next steps",
|
|
16
|
+
"description": "The commands that capture real React, Ink, PTY, and movie states."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"asset": "journey.png",
|
|
20
|
+
"video": "journey.webm",
|
|
21
|
+
"slug": "journey",
|
|
22
|
+
"title": "Journey movie",
|
|
23
|
+
"description": "A poster PNG plus sibling WebM proves movie playback, duration, and chapters.",
|
|
24
|
+
"duration_ms": 4240,
|
|
25
|
+
"source": "frames",
|
|
26
|
+
"chapters": [
|
|
27
|
+
{
|
|
28
|
+
"slug": "recording",
|
|
29
|
+
"t_ms": 0
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"slug": "poster",
|
|
33
|
+
"t_ms": 2685
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"slug": "streaming",
|
|
37
|
+
"t_ms": 3180
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@archastro/tui-shot";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@archastro/astroshot",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "One CLI for deterministic React, terminal, and movie UI captures",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"react",
|
|
7
|
+
"ink",
|
|
8
|
+
"pty",
|
|
9
|
+
"ratatui",
|
|
10
|
+
"terminal",
|
|
11
|
+
"screenshot",
|
|
12
|
+
"movie",
|
|
13
|
+
"screencast",
|
|
14
|
+
"visual-testing"
|
|
15
|
+
],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ArchAstro/astroshots.git",
|
|
20
|
+
"directory": "packages/astroshot"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/ArchAstro/astroshots#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ArchAstro/astroshots/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"astroshot": "bin/astroshot.mjs"
|
|
29
|
+
},
|
|
30
|
+
"exports": {
|
|
31
|
+
"./react": {
|
|
32
|
+
"types": "./react.d.ts",
|
|
33
|
+
"import": "./react.js"
|
|
34
|
+
},
|
|
35
|
+
"./ink": {
|
|
36
|
+
"types": "./ink.d.ts",
|
|
37
|
+
"import": "./ink.js"
|
|
38
|
+
},
|
|
39
|
+
"./pty": {
|
|
40
|
+
"types": "./pty.d.ts",
|
|
41
|
+
"import": "./pty.js"
|
|
42
|
+
},
|
|
43
|
+
"./tui": {
|
|
44
|
+
"types": "./tui.d.ts",
|
|
45
|
+
"import": "./tui.js"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"bin",
|
|
50
|
+
"fixtures",
|
|
51
|
+
"react.d.ts",
|
|
52
|
+
"react.js",
|
|
53
|
+
"ink.d.ts",
|
|
54
|
+
"ink.js",
|
|
55
|
+
"pty.d.ts",
|
|
56
|
+
"pty.js",
|
|
57
|
+
"tui.d.ts",
|
|
58
|
+
"tui.js",
|
|
59
|
+
"README.md"
|
|
60
|
+
],
|
|
61
|
+
"scripts": {
|
|
62
|
+
"pretest": "npm run build --workspace @archastro/react-shot && npm run build --workspace @archastro/tui-shot && npm run build --workspace @archastro/movie-harness",
|
|
63
|
+
"test": "node --test test/*.test.mjs",
|
|
64
|
+
"build:demo-fixtures": "node scripts/build-demo-fixtures.mjs"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": ">=22.14.0"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public",
|
|
71
|
+
"provenance": true,
|
|
72
|
+
"registry": "https://registry.npmjs.org/"
|
|
73
|
+
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"@archastro/movie-harness": "0.2.1",
|
|
76
|
+
"@archastro/react-shot": "0.2.1",
|
|
77
|
+
"@archastro/tui-shot": "0.2.1"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { closeSharedBrowser, takePtyShot } from "@archastro/tui-shot";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@archastro/react-shot";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@archastro/react-shot";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@archastro/tui-shot";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@archastro/tui-shot";
|