goosetools-worker 0.2.5 → 0.2.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goosetools-worker",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "The Goose Tools carousel worker — your computer drafts and renders carousel slides for goosetools.com using your own Claude account.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/render.js CHANGED
@@ -5,6 +5,7 @@ import yaml from "js-yaml";
5
5
  import { chromium } from "playwright";
6
6
  import { FONT_SPECS, customFontFaces, sanitizeFonts } from "./fonts.js";
7
7
  import { assertFfmpeg, composite, posterFrame } from "./video.js";
8
+ import { styleDirFor } from "./state.js";
8
9
 
9
10
  const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
10
11
 
@@ -39,7 +40,8 @@ const spec = yaml.load(readFileSync(join(dir, "slides.yml"), "utf8"));
39
40
 
40
41
  // resolve active style: slides.yml `style:` > brand.yml defaultStyle
41
42
  const styleName = spec.style ?? brandBase.defaultStyle;
42
- const styleDir = join(ROOT, "styles", styleName);
43
+ // Built-ins ship in the package; everything else is per-machine (state.js).
44
+ const styleDir = styleDirFor(styleName);
43
45
  if (!existsSync(styleDir)) {
44
46
  console.error(`Unknown style "${styleName}" — expected ${styleDir}`);
45
47
  process.exit(1);
package/src/state.js ADDED
@@ -0,0 +1,55 @@
1
+ // Where the Carousel Maker's per-machine work lives.
2
+ //
3
+ // Generated styles and rendered decks used to be written inside the package
4
+ // folder (styles/<slug>, carousels/web-*). Installed from npm, that folder is
5
+ // ~/.goosetools/app/node_modules/goosetools-worker, which `update` deletes and
6
+ // reinstalls — so every update silently deleted every style the user had
7
+ // generated (it happened on 2026-09-26). They now live in ~/.goosetools/carousel,
8
+ // which nothing reinstalls. The built-in styles and the demo deck still ship
9
+ // in the package, because they ARE the package.
10
+
11
+ import { cpSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
17
+
18
+ /** Styles that ship in the package (see package.json `files`). */
19
+ export const SHIPPED_STYLES = ["graph-paper", "polka-dot"];
20
+
21
+ export const STATE_DIR = process.env.GOOSETOOLS_CAROUSEL_DIR ?? join(homedir(), ".goosetools", "carousel");
22
+ export const STYLES_DIR = join(STATE_DIR, "styles");
23
+ /** Per-job work folders: the deck, its photos, its rendered slides. */
24
+ export const WORK_DIR = join(STATE_DIR, "carousels");
25
+
26
+ /** Where a style's folder is: in the package if it ships, otherwise in state. */
27
+ export function styleDirFor(slug) {
28
+ return SHIPPED_STYLES.includes(slug) ? join(ROOT, "styles", slug) : join(STYLES_DIR, slug);
29
+ }
30
+
31
+ /**
32
+ * Move any style that isn't a built-in out of the package folder.
33
+ *
34
+ * Only a checkout still has them there (an npm install lost them when it was
35
+ * replaced), but a checkout is exactly where a style made during development
36
+ * sits — so this runs on every start and is a no-op once done. A style
37
+ * already in state wins; the package copy is then just removed.
38
+ */
39
+ export function migrateStylesOutOfPackage() {
40
+ const pkgStyles = join(ROOT, "styles");
41
+ if (!existsSync(pkgStyles)) return 0;
42
+ mkdirSync(STYLES_DIR, { recursive: true });
43
+ let moved = 0;
44
+ for (const d of readdirSync(pkgStyles, { withFileTypes: true })) {
45
+ if (!d.isDirectory() || SHIPPED_STYLES.includes(d.name)) continue;
46
+ const from = join(pkgStyles, d.name);
47
+ const to = join(STYLES_DIR, d.name);
48
+ if (!existsSync(to)) {
49
+ cpSync(from, to, { recursive: true });
50
+ moved++;
51
+ }
52
+ rmSync(from, { recursive: true, force: true });
53
+ }
54
+ return moved;
55
+ }
package/worker/index.js CHANGED
@@ -13,6 +13,7 @@ import { promisify } from "node:util";
13
13
  import yaml from "js-yaml";
14
14
  import { upload } from "./upload.js";
15
15
  import { skillSection } from "./skill.js";
16
+ import { migrateStylesOutOfPackage, STYLES_DIR, styleDirFor, WORK_DIR } from "../src/state.js";
16
17
  import { extFromUrl } from "./style.js";
17
18
  import { FONT_SPECS, sanitizeFonts } from "../src/fonts.js";
18
19
  import { assertFfmpeg, isVideoPath, posterFrame } from "../src/video.js";
@@ -120,7 +121,7 @@ const MAX_PROMPT_REFS = 6;
120
121
  async function ensureStyleRefs(styleSlug, styleRefUrls) {
121
122
  const urls = (styleRefUrls ?? []).slice(0, 20);
122
123
  if (urls.length === 0) return [];
123
- const refsDir = join(ROOT, "styles", styleSlug, "refs");
124
+ const refsDir = join(styleDirFor(styleSlug), "refs");
124
125
  const manifestPath = join(refsDir, "manifest.json");
125
126
 
126
127
  const cachedPaths = () => {
@@ -485,10 +486,10 @@ async function draftSlides(job, styleDir, media, refPaths, brand) {
485
486
  async function runJob(job) {
486
487
  console.log(`▶ job ${job.id}: "${(job.prompt ?? "").slice(0, 60)}" [${job.styleSlug}]`);
487
488
 
488
- const styleDir = join(ROOT, "styles", job.styleSlug);
489
+ const styleDir = styleDirFor(job.styleSlug);
489
490
  if (!existsSync(styleDir)) throw new Error(`Style "${job.styleSlug}" isn't installed on this computer yet`);
490
491
 
491
- const dir = join(ROOT, "carousels", `web-${job.id.slice(0, 8)}`);
492
+ const dir = join(WORK_DIR, `web-${job.id.slice(0, 8)}`);
492
493
  mkdirSync(join(dir, "photos"), { recursive: true });
493
494
  mkdirSync(join(dir, "videos"), { recursive: true });
494
495
  // Older servers don't send these — default to applying the brand, matching
@@ -699,6 +700,14 @@ Do not rewrite any slide's text and do not switch templates.`;
699
700
 
700
701
  // ── Main loop ────────────────────────────────────────────────────────────────
701
702
  console.log(`Goose Tools worker connected to ${BASE_URL} — waiting for jobs (Ctrl+C to stop)`);
703
+ // Styles made from a checkout sat inside the package folder; move them to
704
+ // ~/.goosetools/carousel so an update can't delete them (src/state.js).
705
+ try {
706
+ const moved = migrateStylesOutOfPackage();
707
+ if (moved) console.log(`Moved ${moved} style(s) to ${STYLES_DIR}.`);
708
+ } catch (e) {
709
+ console.error(`Couldn't move styles out of the package: ${e.message}`);
710
+ }
702
711
  let firstPoll = true;
703
712
 
704
713
  while (true) {
package/worker/style.js CHANGED
@@ -17,6 +17,7 @@ import { extname, join } from "node:path";
17
17
  import yaml from "js-yaml";
18
18
  import { upload } from "./upload.js";
19
19
  import { skillSection } from "./skill.js";
20
+ import { styleDirFor, WORK_DIR } from "../src/state.js";
20
21
 
21
22
  const RESERVED_SLUGS = new Set(["graph-paper", "polka-dot"]);
22
23
  const REQUIRED_TEMPLATES = ["cover", "text", "photo", "cta"];
@@ -137,13 +138,13 @@ export async function runStyleJob(job, { api, execFileAsync, ROOT, TOKEN, BASE_U
137
138
  console.log(`▶ style ${job.id}: "${name}" (${refUrls.length} refs) [${slug}]`);
138
139
 
139
140
  if (RESERVED_SLUGS.has(slug)) throw new Error(`"${slug}" is a built-in style`);
140
- const styleDir = join(ROOT, "styles", slug);
141
+ const styleDir = styleDirFor(slug);
141
142
  const marker = join(styleDir, ".generated");
142
143
  if (existsSync(styleDir) && !existsSync(marker)) {
143
- throw new Error(`A hand-made style already exists at styles/${slug} on this computer`);
144
+ throw new Error(`A hand-made style already exists at ${styleDir} on this computer`);
144
145
  }
145
146
 
146
- const workDir = join(ROOT, "carousels", `style-${job.id.slice(0, 8)}`);
147
+ const workDir = join(WORK_DIR, `style-${job.id.slice(0, 8)}`);
147
148
  const refsDir = join(workDir, "refs");
148
149
  mkdirSync(refsDir, { recursive: true });
149
150
 
@@ -157,7 +158,7 @@ export async function runStyleJob(job, { api, execFileAsync, ROOT, TOKEN, BASE_U
157
158
  refPaths.push(p);
158
159
  }
159
160
 
160
- const exemplarDir = join(ROOT, "styles", "graph-paper");
161
+ const exemplarDir = styleDirFor("graph-paper");
161
162
  const exemplar = {
162
163
  styleYml: readFileSync(join(exemplarDir, "style.yml"), "utf8"),
163
164
  cover: readFileSync(join(exemplarDir, "templates", "cover.html"), "utf8"),
@@ -207,7 +208,7 @@ export async function runStyleJob(job, { api, execFileAsync, ROOT, TOKEN, BASE_U
207
208
  join(styleDir, "refs", "manifest.json"),
208
209
  JSON.stringify({ urls: refUrls.slice(0, 20) }, null, 2),
209
210
  );
210
- console.log(` styles/${slug}/ written (${Object.keys(files).length} files + ${refPaths.length} refs)`);
211
+ console.log(` ${styleDir} written (${Object.keys(files).length} files + ${refPaths.length} refs)`);
211
212
 
212
213
  // Preview: render the new style's cover + text slide with the user's brand.
213
214
  const previewDir = join(workDir, "preview");