overlay-factory-worker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +215 -0
  2. package/package.json +64 -0
  3. package/public/fonts/Handjet-variable.woff2 +0 -0
  4. package/remotion.config.ts +14 -0
  5. package/scripts/check-legibility.ts +284 -0
  6. package/scripts/check-safe-area.ts +148 -0
  7. package/scripts/custom-fonts.ts +114 -0
  8. package/scripts/export.sh +31 -0
  9. package/scripts/field-images.ts +93 -0
  10. package/scripts/ig-probe.ts +83 -0
  11. package/scripts/ig-sync.ts +204 -0
  12. package/scripts/ingest.sh +34 -0
  13. package/scripts/library.ts +143 -0
  14. package/scripts/look-card.ts +107 -0
  15. package/scripts/look-store.ts +176 -0
  16. package/scripts/make-card.ts +237 -0
  17. package/scripts/merge-index.ts +74 -0
  18. package/scripts/new-episode.ts +149 -0
  19. package/scripts/overlay-worker.ts +1119 -0
  20. package/scripts/place-overlay.ts +359 -0
  21. package/scripts/prep-card.ts +73 -0
  22. package/scripts/quality.ts +0 -0
  23. package/scripts/render-overlay.ts +150 -0
  24. package/scripts/report.ts +127 -0
  25. package/scripts/rerender-cards.ts +116 -0
  26. package/scripts/series.ts +816 -0
  27. package/scripts/set-difficulty.ts +62 -0
  28. package/scripts/state-dir.ts +102 -0
  29. package/scripts/stock.ts +254 -0
  30. package/scripts/verify.ts +149 -0
  31. package/scripts/wp-restock.ts +281 -0
  32. package/src/Root.tsx +112 -0
  33. package/src/index.css +1 -0
  34. package/src/index.ts +4 -0
  35. package/src/lab/FontLab.tsx +50 -0
  36. package/src/lab/FontSheet.tsx +188 -0
  37. package/src/lab/PillLab.tsx +121 -0
  38. package/src/overlay/Composition.tsx +297 -0
  39. package/src/overlay/DifficultyMeter.tsx +86 -0
  40. package/src/overlay/PixelText.tsx +134 -0
  41. package/src/overlay/Title.tsx +75 -0
  42. package/src/overlay/brandFonts.ts +58 -0
  43. package/src/overlay/cardLayout.ts +94 -0
  44. package/src/overlay/fonts.ts +19 -0
  45. package/src/overlay/look.ts +155 -0
  46. package/src/overlay/safeArea.ts +89 -0
  47. package/src/overlay/types.ts +134 -0
  48. package/src/series/what-prints/CodeCard.tsx +107 -0
  49. package/src/series/what-prints/Composition.tsx +106 -0
  50. package/src/series/what-prints/codeCardTypes.ts +105 -0
  51. package/src/series/what-prints/types.ts +22 -0
  52. package/tsconfig.json +17 -0
  53. package/worker/cli.mjs +151 -0
  54. package/worker/service.mjs +404 -0
@@ -0,0 +1,404 @@
1
+ // Background-service install/uninstall for the Overlay Factory worker.
2
+ //
3
+ // Modelled on goosetools-worker's service.js so all four workers behave the
4
+ // same way: the token lands in ~/.goosetools/env (shared — one connected
5
+ // computer, one token), a stable copy of this package is installed under
6
+ // ~/.goosetools/overlay-app, and a launchd LaunchAgent runs it whenever the
7
+ // machine is on.
8
+ //
9
+ // Two deliberate differences from the other workers:
10
+ //
11
+ // 1. macOS only. The composite pass encodes with hevc_videotoolbox, which
12
+ // is an Apple framework — there is no Windows path to fall back to yet.
13
+ // 2. `uninstall` does NOT remove ~/.goosetools. The carousel worker can:
14
+ // everything it keeps there is re-derivable from the server. This one
15
+ // keeps the puzzle library, the learned looks, and the used-card record
16
+ // in ~/.goosetools/overlay, and none of that exists anywhere else.
17
+
18
+ import { execSync } from "node:child_process";
19
+ import {
20
+ chmodSync,
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ readFileSync,
25
+ rmSync,
26
+ statSync,
27
+ writeFileSync,
28
+ } from "node:fs";
29
+ import { homedir } from "node:os";
30
+ import { dirname, join } from "node:path";
31
+
32
+ export const GOOSE_DIR = join(homedir(), ".goosetools");
33
+ const ENV_FILE = join(GOOSE_DIR, "env");
34
+ const APP_PREFIX = join(GOOSE_DIR, "overlay-app");
35
+ const STATE_DIR = join(GOOSE_DIR, "overlay");
36
+ const PKG = "overlay-factory-worker";
37
+ const INSTALLED_CLI = join(APP_PREFIX, "node_modules", PKG, "worker", "cli.mjs");
38
+
39
+ const MAC_LABEL = "com.goosetools.overlay";
40
+ const MAC_PLIST = join(homedir(), "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
41
+ const LOG = join(GOOSE_DIR, "overlay-worker.log");
42
+ const ERR_LOG = join(GOOSE_DIR, "overlay-worker.err.log");
43
+
44
+ function sh(cmd, opts = {}) {
45
+ return execSync(cmd, { stdio: ["ignore", "pipe", "pipe"], ...opts })
46
+ .toString()
47
+ .trim();
48
+ }
49
+
50
+ function quietSh(cmd) {
51
+ try {
52
+ sh(cmd);
53
+ return true;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ function isEmptyDir(dir) {
60
+ try {
61
+ return readdirSync(dir).length === 0;
62
+ } catch {
63
+ return true;
64
+ }
65
+ }
66
+
67
+ export function hasFfmpeg() {
68
+ return quietSh("ffmpeg -version") && quietSh("ffprobe -version");
69
+ }
70
+
71
+ function npmGlobalBin() {
72
+ try {
73
+ return join(sh("npm prefix -g", { shell: true }), "bin");
74
+ } catch {
75
+ return "";
76
+ }
77
+ }
78
+
79
+ // launchd starts an agent with an almost empty environment. The render shells
80
+ // out to ffmpeg, npx and claude, so PATH is spelled out here — inheriting the
81
+ // interactive shell's PATH is exactly what does not happen, and it's the usual
82
+ // way one of these agents ends up running but failing every job.
83
+ function servicePath() {
84
+ const parts = [
85
+ dirname(process.execPath),
86
+ npmGlobalBin(),
87
+ "/opt/homebrew/bin",
88
+ "/usr/local/bin",
89
+ `${process.env.HOME ?? ""}/.local/bin`,
90
+ "/usr/bin",
91
+ "/bin",
92
+ "/usr/sbin",
93
+ "/sbin",
94
+ ].filter(Boolean);
95
+ return [...new Set(parts)].join(":");
96
+ }
97
+
98
+ function readEnvFile() {
99
+ if (!existsSync(ENV_FILE)) return {};
100
+ return Object.fromEntries(
101
+ readFileSync(ENV_FILE, "utf8")
102
+ .split("\n")
103
+ .filter((l) => l.includes("=") && !l.trim().startsWith("#"))
104
+ .map((l) => [
105
+ l.slice(0, l.indexOf("=")).trim(),
106
+ l.slice(l.indexOf("=") + 1).trim(),
107
+ ]),
108
+ );
109
+ }
110
+
111
+ // One token file for every worker on this machine. Whichever installer runs
112
+ // last writes it, and they all write the same two keys — so `goosetools
113
+ // install --token gt_…` leaves every worker pointing at the same credential.
114
+ function writeEnvFile({ url, token }) {
115
+ mkdirSync(GOOSE_DIR, { recursive: true });
116
+ writeFileSync(ENV_FILE, `GOOSETOOLS_URL=${url}\nWORKER_TOKEN=${token}\n`);
117
+ chmodSync(ENV_FILE, 0o600);
118
+ }
119
+
120
+ function installStableCopy(root) {
121
+ console.log("Setting up the background copy (this can take a few minutes)…");
122
+ mkdirSync(APP_PREFIX, { recursive: true });
123
+ // From a git checkout (development), install the checkout itself; normal
124
+ // npx users get the published package.
125
+ const source = existsSync(join(root, ".git")) ? `"${root}"` : `${PKG}@latest`;
126
+ execSync(
127
+ `npm install --no-fund --no-audit --loglevel=error --prefix "${APP_PREFIX}" ${source}`,
128
+ { stdio: ["ignore", "inherit", "inherit"], shell: true },
129
+ );
130
+ if (!existsSync(INSTALLED_CLI)) {
131
+ throw new Error("background copy did not install where expected");
132
+ }
133
+ }
134
+
135
+ // ── macOS (launchd) ─────────────────────────────────────────────────────────
136
+
137
+ function macPlist() {
138
+ const esc = (s) =>
139
+ s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
140
+ return `<?xml version="1.0" encoding="UTF-8"?>
141
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
142
+ <plist version="1.0">
143
+ <dict>
144
+ <key>Label</key>
145
+ <string>${MAC_LABEL}</string>
146
+ <key>ProgramArguments</key>
147
+ <array>
148
+ <string>${esc(process.execPath)}</string>
149
+ <string>${esc(INSTALLED_CLI)}</string>
150
+ <string>run</string>
151
+ </array>
152
+ <key>WorkingDirectory</key>
153
+ <string>${esc(join(APP_PREFIX, "node_modules", PKG))}</string>
154
+ <key>RunAtLoad</key>
155
+ <true/>
156
+ <key>KeepAlive</key>
157
+ <true/>
158
+ <key>ThrottleInterval</key>
159
+ <integer>30</integer>
160
+ <key>StandardOutPath</key>
161
+ <string>${esc(LOG)}</string>
162
+ <key>StandardErrorPath</key>
163
+ <string>${esc(ERR_LOG)}</string>
164
+ <key>EnvironmentVariables</key>
165
+ <dict>
166
+ <key>PATH</key>
167
+ <string>${esc(servicePath())}</string>
168
+ <key>HOME</key>
169
+ <string>${esc(homedir())}</string>
170
+ </dict>
171
+ </dict>
172
+ </plist>
173
+ `;
174
+ }
175
+
176
+ function macInstall() {
177
+ mkdirSync(dirname(MAC_PLIST), { recursive: true });
178
+ writeFileSync(MAC_PLIST, macPlist());
179
+ const uid = process.getuid();
180
+ quietSh(`launchctl bootout gui/${uid}/${MAC_LABEL}`);
181
+ if (!quietSh(`launchctl bootstrap gui/${uid} "${MAC_PLIST}"`)) {
182
+ quietSh(`launchctl unload "${MAC_PLIST}"`);
183
+ execSync(`launchctl load -w "${MAC_PLIST}"`, { stdio: "ignore" });
184
+ }
185
+ }
186
+
187
+ function macUninstall() {
188
+ quietSh(`launchctl bootout gui/${process.getuid()}/${MAC_LABEL}`);
189
+ quietSh(`launchctl unload "${MAC_PLIST}"`);
190
+ rmSync(MAC_PLIST, { force: true });
191
+ }
192
+
193
+ function requireMac() {
194
+ if (process.platform === "darwin") return;
195
+ console.error(
196
+ "\nThe Overlay Factory worker runs on macOS only — the render encodes with\n" +
197
+ "hevc_videotoolbox, an Apple framework. The other Goose Tools workers\n" +
198
+ "(Carousel, Caption, Brand) work here; only reels need a Mac.\n",
199
+ );
200
+ process.exit(1);
201
+ }
202
+
203
+ // ── Public commands ─────────────────────────────────────────────────────────
204
+
205
+ export function install({ url, token, root }) {
206
+ requireMac();
207
+ if (!token) {
208
+ console.error(
209
+ `Missing worker token. Run: npx ${PKG} install --url ${url} --token gt_…`,
210
+ );
211
+ process.exit(1);
212
+ }
213
+ writeEnvFile({ url, token });
214
+ mkdirSync(STATE_DIR, { recursive: true });
215
+ installStableCopy(root);
216
+ macInstall();
217
+ console.log(
218
+ "\n✓ Overlay Factory connected. This computer renders reels in the\n" +
219
+ " background whenever it's on, even after a restart.\n\n" +
220
+ ` Logs: ${LOG}\n` +
221
+ ` Your work: ${STATE_DIR} (puzzles, looks, cards — kept across updates)\n` +
222
+ ` Turn off: npx --yes ${PKG} uninstall\n`,
223
+ );
224
+ // Anyone who ran this worker from a reel-factory checkout has a puzzle
225
+ // library and learned looks sitting in that repo. The packaged worker reads
226
+ // the state dir instead, so say where to bring them across rather than
227
+ // letting it look like the work vanished.
228
+ if (isEmptyDir(STATE_DIR)) {
229
+ console.log(
230
+ " Ran this from a reel-factory checkout before? Bring your work over:\n\n" +
231
+ ` cp -R ~/code/reel-factory/{episodes,puzzles,series} ${STATE_DIR}/\n` +
232
+ ` cp -R ~/code/reel-factory/public/assets ${STATE_DIR}/assets\n`,
233
+ );
234
+ }
235
+ if (!hasFfmpeg()) {
236
+ console.log(
237
+ " One thing left — reels need ffmpeg, which isn't installed yet:\n\n" +
238
+ " brew install ffmpeg\n\n" +
239
+ " Everything else on this computer works without it. Renders will\n" +
240
+ " fail with that same message until it's there.\n",
241
+ );
242
+ }
243
+ }
244
+
245
+ export function uninstall() {
246
+ macUninstall();
247
+ rmSync(APP_PREFIX, { recursive: true, force: true });
248
+ console.log(
249
+ "✓ Overlay Factory worker removed — this computer no longer renders reels.\n" +
250
+ ` Kept: ${STATE_DIR} (puzzles, looks, used-card record). Delete it by hand\n` +
251
+ " if you want it gone; nothing else has a copy.",
252
+ );
253
+ }
254
+
255
+ // The daemon runs the COPY under ~/.goosetools/overlay-app, not wherever this
256
+ // was run from — so new code only reaches it by reinstalling that copy.
257
+ // Re-registering the service restarts it, which is what actually loads the new
258
+ // code (Node reads every module once, at startup).
259
+ export function update({ root }) {
260
+ requireMac();
261
+ const saved = readEnvFile();
262
+ if (!saved.WORKER_TOKEN) {
263
+ console.error(
264
+ "No saved worker token — this computer hasn't been connected yet.\n" +
265
+ "Get your connect command at https://goosetools.com/dashboard/setup",
266
+ );
267
+ process.exit(1);
268
+ }
269
+ const before = installedVersion();
270
+ installStableCopy(root);
271
+ macInstall();
272
+ const after = installedVersion();
273
+ console.log(
274
+ after && before && after !== before
275
+ ? `\n✓ Updated ${before} → ${after} and restarted. Nothing else to do.\n`
276
+ : `\n✓ Worker reinstalled (version ${after ?? "unknown"}) and restarted.\n`,
277
+ );
278
+ }
279
+
280
+ // ── status ──────────────────────────────────────────────────────────────────
281
+
282
+ function installedVersion() {
283
+ try {
284
+ const pkg = join(APP_PREFIX, "node_modules", PKG, "package.json");
285
+ return JSON.parse(readFileSync(pkg, "utf8")).version ?? null;
286
+ } catch {
287
+ return null;
288
+ }
289
+ }
290
+
291
+ function latestVersion() {
292
+ try {
293
+ return sh(`npm view ${PKG} version`, { shell: true, timeout: 15000 });
294
+ } catch {
295
+ return null; // offline, or npm unreachable — not worth failing on
296
+ }
297
+ }
298
+
299
+ function compareVersions(a, b) {
300
+ const parts = (v) =>
301
+ String(v)
302
+ .split(".")
303
+ .map((n) => Number.parseInt(n, 10) || 0);
304
+ const [pa, pb] = [parts(a), parts(b)];
305
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
306
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) < (pb[i] ?? 0) ? -1 : 1;
307
+ }
308
+ return 0;
309
+ }
310
+
311
+ // launchctl prints "<pid>\t<last exit>\t<label>"; a "-" pid means registered
312
+ // but not currently running.
313
+ function servicePid() {
314
+ try {
315
+ const line = sh(`launchctl list | grep ${MAC_LABEL}`, { shell: true });
316
+ const pid = line.split(/\s+/)[0];
317
+ return pid === "-" ? null : pid;
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+
323
+ function tail(file, n) {
324
+ try {
325
+ return readFileSync(file, "utf8").trimEnd().split("\n").slice(-n).join("\n");
326
+ } catch {
327
+ return "";
328
+ }
329
+ }
330
+
331
+ // How long ago a log was last written. Without this, days-old errors read as
332
+ // a live outage — the single most misleading thing in a log tail.
333
+ function lastWritten(file) {
334
+ try {
335
+ const mins = Math.round((Date.now() - statSync(file).mtimeMs) / 60000);
336
+ if (mins < 1) return "just now";
337
+ if (mins < 60) return `${mins} min ago`;
338
+ const hours = Math.round(mins / 60);
339
+ if (hours < 24) return `${hours} hr ago`;
340
+ return `${Math.round(hours / 24)} days ago`;
341
+ } catch {
342
+ return "unknown";
343
+ }
344
+ }
345
+
346
+ export function status() {
347
+ const ok = (b) => (b ? "✓" : "✗");
348
+ const env = readEnvFile();
349
+ const installed = installedVersion();
350
+ const latest = latestVersion();
351
+ const pid = servicePid();
352
+ const url = env.GOOSETOOLS_URL ?? "https://goosetools.com";
353
+ const ffmpeg = hasFfmpeg();
354
+
355
+ console.log("\nOverlay Factory worker — status\n");
356
+ console.log(` ${ok(installed)} Installed ${installed ?? "not installed"}`);
357
+ const cmp = installed && latest ? compareVersions(installed, latest) : 0;
358
+ const stale = cmp < 0;
359
+ if (latest) {
360
+ const note = stale
361
+ ? " ← update available"
362
+ : cmp > 0
363
+ ? " (you're ahead — local dev build)"
364
+ : "";
365
+ console.log(` ${ok(!stale)} Latest on npm ${latest}${note}`);
366
+ } else {
367
+ console.log(" · Latest on npm couldn't check (offline?)");
368
+ }
369
+ console.log(` ${ok(pid)} Running ${pid ? `yes (pid ${pid})` : "no"}`);
370
+ console.log(
371
+ ` ${ok(env.WORKER_TOKEN)} Token saved ${
372
+ env.WORKER_TOKEN ? "yes" : "no — reconnect at goosetools.com/dashboard/setup"
373
+ }`,
374
+ );
375
+ console.log(
376
+ ` ${ok(ffmpeg)} ffmpeg ${
377
+ ffmpeg ? "found" : "missing — renders fail without it: brew install ffmpeg"
378
+ }`,
379
+ );
380
+ console.log(` · Server ${url}`);
381
+ console.log(` · Your work ${STATE_DIR}`);
382
+ console.log(` · Logs ${LOG}`);
383
+
384
+ const errors = tail(ERR_LOG, 15);
385
+ if (errors) {
386
+ console.log(
387
+ `\nErrors (last written ${lastWritten(ERR_LOG)}):\n${errors.replace(/^/gm, " ")}`,
388
+ );
389
+ }
390
+ const log = tail(LOG, 15);
391
+ if (log) {
392
+ console.log(
393
+ `\nActivity (last written ${lastWritten(LOG)}):\n${log.replace(/^/gm, " ")}`,
394
+ );
395
+ }
396
+
397
+ if (stale) {
398
+ console.log(`\nTo update: npx --yes ${PKG}@latest update\n`);
399
+ } else if (!pid && installed) {
400
+ console.log(`\nNot running. Restart it with: npx --yes ${PKG}@latest update\n`);
401
+ } else {
402
+ console.log("");
403
+ }
404
+ }