storyforge 0.2.2 → 0.3.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 (2) hide show
  1. package/dist/index.js +87 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -355,14 +355,26 @@ async function devCommand(options) {
355
355
  res.end(JSON.stringify([...saved, ...project]));
356
356
  return;
357
357
  }
358
- const compGetMatch = pathname.match(/^\/api\/compositions\/([^/]+)$/);
358
+ const compGetMatch = pathname.match(/^\/api\/compositions\/([^/]+?)(\.json)?$/);
359
359
  if (req.method === "GET" && compGetMatch) {
360
360
  const name = decodeURIComponent(compGetMatch[1]).replace(/[^a-zA-Z0-9_-]/g, "");
361
+ const wantJson = !!compGetMatch[2] || url.searchParams.get("format") === "json";
361
362
  if (!name) {
362
363
  res.writeHead(400, CORS_HEADERS);
363
364
  res.end("Invalid name");
364
365
  return;
365
366
  }
367
+ if (wantJson) {
368
+ const jsonPath = path2.join(dir, "compositions", `${name}.json`);
369
+ if (fs2.existsSync(jsonPath)) {
370
+ res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json; charset=utf-8" });
371
+ res.end(fs2.readFileSync(jsonPath, "utf-8"));
372
+ return;
373
+ }
374
+ res.writeHead(404, CORS_HEADERS);
375
+ res.end("Not found");
376
+ return;
377
+ }
366
378
  const compositionsDir = path2.join(os2.homedir(), ".forge", "compositions", meta.projectId);
367
379
  const savedPath = path2.join(compositionsDir, `${name}.tsx`);
368
380
  if (fs2.existsSync(savedPath)) {
@@ -381,24 +393,57 @@ async function devCommand(options) {
381
393
  res.end("Not found");
382
394
  return;
383
395
  }
384
- const compPostMatch = pathname.match(/^\/api\/compositions\/([^/]+)$/);
396
+ const compPostMatch = pathname.match(/^\/api\/compositions\/([^/]+?)(\.json)?$/);
385
397
  if (req.method === "POST" && compPostMatch) {
386
398
  const name = compPostMatch[1].replace(/[^a-zA-Z0-9_-]/g, "");
399
+ const pathForcesJson = !!compPostMatch[2];
387
400
  if (!name) {
388
401
  res.writeHead(400, CORS_HEADERS);
389
402
  res.end("Invalid name");
390
403
  return;
391
404
  }
392
- const compositionsDir = path2.join(os2.homedir(), ".forge", "compositions", meta.projectId);
393
- fs2.mkdirSync(compositionsDir, { recursive: true });
394
- const filePath = path2.join(compositionsDir, `${name}.tsx`);
405
+ const reqContentType = (req.headers["content-type"] ?? "").toLowerCase();
406
+ const isJsonBody = pathForcesJson || reqContentType.includes("application/json");
395
407
  const chunks = [];
396
408
  req.on("data", (chunk) => chunks.push(chunk));
397
409
  req.on("end", () => {
398
410
  const body = Buffer.concat(chunks).toString("utf-8");
411
+ if (isJsonBody) {
412
+ let parsed;
413
+ try {
414
+ parsed = JSON.parse(body);
415
+ } catch (e) {
416
+ res.writeHead(400, { ...CORS_HEADERS, "Content-Type": "application/json" });
417
+ res.end(JSON.stringify({ error: `Invalid JSON: ${e.message}` }));
418
+ return;
419
+ }
420
+ const valid = (() => {
421
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return false;
422
+ const isCompositionShape = typeof parsed.kind === "string" && typeof parsed.aspect === "string" && Array.isArray(parsed.layers);
423
+ const isChunkShape = Array.isArray(parsed.compositions);
424
+ return isCompositionShape || isChunkShape;
425
+ })();
426
+ if (!valid) {
427
+ res.writeHead(400, { ...CORS_HEADERS, "Content-Type": "application/json" });
428
+ res.end(JSON.stringify({
429
+ error: "Unrecognized composition shape \u2014 expected { kind, aspect, layers[] } or { compositions[] }"
430
+ }));
431
+ return;
432
+ }
433
+ const jsonDir = path2.join(dir, "compositions");
434
+ fs2.mkdirSync(jsonDir, { recursive: true });
435
+ const filePath2 = path2.join(jsonDir, `${name}.json`);
436
+ fs2.writeFileSync(filePath2, JSON.stringify(parsed, null, 2), "utf-8");
437
+ res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
438
+ res.end(JSON.stringify({ ok: true, path: filePath2, kind: "json" }));
439
+ return;
440
+ }
441
+ const compositionsDir = path2.join(os2.homedir(), ".forge", "compositions", meta.projectId);
442
+ fs2.mkdirSync(compositionsDir, { recursive: true });
443
+ const filePath = path2.join(compositionsDir, `${name}.tsx`);
399
444
  fs2.writeFileSync(filePath, body, "utf-8");
400
445
  res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
401
- res.end(JSON.stringify({ ok: true, path: filePath }));
446
+ res.end(JSON.stringify({ ok: true, path: filePath, kind: "tsx" }));
402
447
  });
403
448
  req.on("error", () => {
404
449
  res.writeHead(500, CORS_HEADERS);
@@ -566,6 +611,42 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
566
611
  res.end(fs2.readFileSync(compPath));
567
612
  return;
568
613
  }
614
+ if (pathname === "/api/chunk-compositions/append-images" && req.method === "POST") {
615
+ const bodyChunks = [];
616
+ req.on("data", (c2) => bodyChunks.push(c2));
617
+ req.on("end", () => {
618
+ try {
619
+ const { assemblyId, paths } = JSON.parse(Buffer.concat(bodyChunks).toString("utf-8"));
620
+ const compPath = path2.join(dir, "chunk-compositions.json");
621
+ if (!fs2.existsSync(compPath)) {
622
+ res.writeHead(404, CORS_HEADERS);
623
+ res.end(JSON.stringify({ error: "chunk-compositions.json not found" }));
624
+ return;
625
+ }
626
+ const doc = JSON.parse(fs2.readFileSync(compPath, "utf-8"));
627
+ const scene = doc.scenes?.find((c2) => c2.assemblyId === assemblyId);
628
+ if (!scene) {
629
+ res.writeHead(404, CORS_HEADERS);
630
+ res.end(JSON.stringify({ error: `scene ${assemblyId} not found` }));
631
+ return;
632
+ }
633
+ const existing = Array.isArray(scene.images) ? scene.images : [];
634
+ const deduped = [...existing];
635
+ for (const p of paths) {
636
+ if (p && !deduped.includes(p)) deduped.push(p);
637
+ }
638
+ scene.images = deduped;
639
+ scene.imageCount = deduped.length;
640
+ fs2.writeFileSync(compPath, JSON.stringify(doc, null, 2), "utf-8");
641
+ res.writeHead(200, { ...CORS_HEADERS, "Content-Type": "application/json" });
642
+ res.end(JSON.stringify({ ok: true, images: deduped }));
643
+ } catch (e) {
644
+ res.writeHead(500, CORS_HEADERS);
645
+ res.end(JSON.stringify({ error: e.message }));
646
+ }
647
+ });
648
+ return;
649
+ }
569
650
  const listMatch = pathname.match(/^\/api\/list\/(.+)$/);
570
651
  if (listMatch) {
571
652
  const relPath = decodeURIComponent(listMatch[1]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyforge",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "StoryForge — local bridge for the Forge video production web app. Zero runtime dependencies.",
5
5
  "type": "module",
6
6
  "bin": {