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.
- package/dist/index.js +87 -6
- 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
|
|
393
|
-
|
|
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]);
|