storyforge 0.9.0 → 0.10.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.
|
@@ -7330,11 +7330,18 @@ var BridgePoller = class {
|
|
|
7330
7330
|
}
|
|
7331
7331
|
}
|
|
7332
7332
|
/**
|
|
7333
|
-
*
|
|
7334
|
-
*
|
|
7335
|
-
*
|
|
7336
|
-
*
|
|
7337
|
-
*
|
|
7333
|
+
* Upload a rendered clip via signed-URL flow:
|
|
7334
|
+
* 1. POST /api/cli-bridge/upload-url → { signedUrl, storjKey, version }
|
|
7335
|
+
* 2. PUT file directly to Storj via signedUrl (bypasses Vercel —
|
|
7336
|
+
* lifts the body-size ceiling, MP4s of any size go through)
|
|
7337
|
+
* 3. POST /api/cli-bridge/upload-finalize → inserts clips row,
|
|
7338
|
+
* regenerates manifest, returns { clipId }
|
|
7339
|
+
*
|
|
7340
|
+
* The legacy multipart path at /api/cli-bridge/upload-clip is still
|
|
7341
|
+
* deployed for back-compat with storyforge < 0.10.0 in the wild,
|
|
7342
|
+
* but new clients prefer this. Best-effort failure semantics —
|
|
7343
|
+
* an upload error is logged + reported per-chunk but doesn't take
|
|
7344
|
+
* down the whole render batch.
|
|
7338
7345
|
*/
|
|
7339
7346
|
async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
|
|
7340
7347
|
try {
|
|
@@ -7343,30 +7350,63 @@ var BridgePoller = class {
|
|
|
7343
7350
|
if (!stat.isFile() || stat.size === 0) {
|
|
7344
7351
|
return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
|
|
7345
7352
|
}
|
|
7346
|
-
const buf = await fs16.readFile(result.outputPath);
|
|
7347
7353
|
const durationFrames = Math.max(1, Math.round(result.durationSec * 30));
|
|
7348
|
-
const
|
|
7349
|
-
form.append("bridgeJobId", bridgeJobId);
|
|
7350
|
-
form.append("chunkId", result.chunkId);
|
|
7351
|
-
form.append("engine", result.engine);
|
|
7352
|
-
form.append("aspect", spec.aspect);
|
|
7353
|
-
form.append("durationSec", String(result.durationSec));
|
|
7354
|
-
form.append("durationFrames", String(durationFrames));
|
|
7355
|
-
form.append("file", new Blob([new Uint8Array(buf)], { type: "video/mp4" }), `${result.chunkId}.mp4`);
|
|
7356
|
-
const resp = await fetch(`${this.baseUrl}/api/cli-bridge/upload-clip`, {
|
|
7354
|
+
const mintResp = await fetch(`${this.baseUrl}/api/cli-bridge/upload-url`, {
|
|
7357
7355
|
method: "POST",
|
|
7358
|
-
headers: {
|
|
7359
|
-
|
|
7360
|
-
|
|
7356
|
+
headers: {
|
|
7357
|
+
Authorization: `Bearer ${this.token}`,
|
|
7358
|
+
"Content-Type": "application/json"
|
|
7359
|
+
},
|
|
7360
|
+
body: JSON.stringify({
|
|
7361
|
+
bridgeJobId,
|
|
7362
|
+
chunkId: result.chunkId,
|
|
7363
|
+
aspect: spec.aspect,
|
|
7364
|
+
contentType: "video/mp4"
|
|
7365
|
+
}),
|
|
7366
|
+
signal: AbortSignal.timeout(6e4)
|
|
7361
7367
|
});
|
|
7362
|
-
const
|
|
7363
|
-
if (!
|
|
7364
|
-
return { ok: false, error: `HTTP ${
|
|
7368
|
+
const mintText = await mintResp.text().catch(() => "");
|
|
7369
|
+
if (!mintResp.ok) {
|
|
7370
|
+
return { ok: false, error: `mint HTTP ${mintResp.status}: ${mintText.slice(0, 300)}` };
|
|
7365
7371
|
}
|
|
7366
|
-
const
|
|
7367
|
-
|
|
7372
|
+
const mint = JSON.parse(mintText);
|
|
7373
|
+
const buf = await fs16.readFile(result.outputPath);
|
|
7374
|
+
const putResp = await fetch(mint.signedUrl, {
|
|
7375
|
+
method: "PUT",
|
|
7376
|
+
headers: { "Content-Type": "video/mp4" },
|
|
7377
|
+
body: new Uint8Array(buf),
|
|
7378
|
+
signal: AbortSignal.timeout(15 * 6e4)
|
|
7379
|
+
});
|
|
7380
|
+
if (!putResp.ok) {
|
|
7381
|
+
const putText = await putResp.text().catch(() => "");
|
|
7382
|
+
return { ok: false, error: `storj PUT HTTP ${putResp.status}: ${putText.slice(0, 200)}` };
|
|
7383
|
+
}
|
|
7384
|
+
const finResp = await fetch(`${this.baseUrl}/api/cli-bridge/upload-finalize`, {
|
|
7385
|
+
method: "POST",
|
|
7386
|
+
headers: {
|
|
7387
|
+
Authorization: `Bearer ${this.token}`,
|
|
7388
|
+
"Content-Type": "application/json"
|
|
7389
|
+
},
|
|
7390
|
+
body: JSON.stringify({
|
|
7391
|
+
bridgeJobId,
|
|
7392
|
+
chunkId: result.chunkId,
|
|
7393
|
+
aspect: spec.aspect,
|
|
7394
|
+
version: mint.version,
|
|
7395
|
+
storjKey: mint.storjKey,
|
|
7396
|
+
engine: result.engine,
|
|
7397
|
+
durationSec: result.durationSec,
|
|
7398
|
+
durationFrames,
|
|
7399
|
+
size: stat.size
|
|
7400
|
+
}),
|
|
7401
|
+
signal: AbortSignal.timeout(6e4)
|
|
7402
|
+
});
|
|
7403
|
+
const finText = await finResp.text().catch(() => "");
|
|
7404
|
+
if (!finResp.ok) {
|
|
7405
|
+
return { ok: false, storjKey: mint.storjKey, error: `finalize HTTP ${finResp.status}: ${finText.slice(0, 300)}` };
|
|
7406
|
+
}
|
|
7407
|
+
log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${mint.storjKey} (v${mint.version}, ${(stat.size / 1024 / 1024).toFixed(1)}M)`);
|
|
7368
7408
|
void projectId;
|
|
7369
|
-
return { ok: true, storjKey:
|
|
7409
|
+
return { ok: true, storjKey: mint.storjKey };
|
|
7370
7410
|
} catch (err) {
|
|
7371
7411
|
return { ok: false, error: err.message };
|
|
7372
7412
|
}
|
package/dist/index.js
CHANGED
|
@@ -547,7 +547,7 @@ var exec = promisify(execCb);
|
|
|
547
547
|
var PORT = 4444;
|
|
548
548
|
function runCliPipingStdin(cmd, args, stdinData, opts = {}) {
|
|
549
549
|
const maxBytes = (opts.maxBufferMB ?? 16) * 1024 * 1024;
|
|
550
|
-
return new Promise((
|
|
550
|
+
return new Promise((resolve6, reject) => {
|
|
551
551
|
const proc = spawn(cmd, args, { stdio: ["pipe", "pipe", "pipe"] });
|
|
552
552
|
let stdout = "";
|
|
553
553
|
let stderr = "";
|
|
@@ -580,7 +580,7 @@ function runCliPipingStdin(cmd, args, stdinData, opts = {}) {
|
|
|
580
580
|
});
|
|
581
581
|
proc.on("close", (code) => {
|
|
582
582
|
if (timer) clearTimeout(timer);
|
|
583
|
-
|
|
583
|
+
resolve6({ stdout, stderr, code });
|
|
584
584
|
});
|
|
585
585
|
proc.stdin.end(stdinData);
|
|
586
586
|
});
|
|
@@ -1501,9 +1501,9 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1501
1501
|
}
|
|
1502
1502
|
const boundary = boundaryM[1];
|
|
1503
1503
|
const chunks = [];
|
|
1504
|
-
await new Promise((
|
|
1504
|
+
await new Promise((resolve6, reject) => {
|
|
1505
1505
|
req.on("data", (c) => chunks.push(Buffer.from(c)));
|
|
1506
|
-
req.on("end",
|
|
1506
|
+
req.on("end", resolve6);
|
|
1507
1507
|
req.on("error", reject);
|
|
1508
1508
|
});
|
|
1509
1509
|
const body = Buffer.concat(chunks);
|
|
@@ -1615,7 +1615,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1615
1615
|
return "0.0.0";
|
|
1616
1616
|
})();
|
|
1617
1617
|
void (async () => {
|
|
1618
|
-
const { BridgePoller } = await import("./bridge-poller-
|
|
1618
|
+
const { BridgePoller } = await import("./bridge-poller-G6LO7JFZ.js");
|
|
1619
1619
|
const poller = new BridgePoller({ baseUrl: bridgeUrl, token: bridgeToken, clientVersion: `storyforge ${pkgVersion}` });
|
|
1620
1620
|
poller.start();
|
|
1621
1621
|
})();
|
|
@@ -1640,10 +1640,10 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1640
1640
|
import * as readline from "readline";
|
|
1641
1641
|
function prompt(question) {
|
|
1642
1642
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1643
|
-
return new Promise((
|
|
1643
|
+
return new Promise((resolve6) => {
|
|
1644
1644
|
rl.question(question, (answer) => {
|
|
1645
1645
|
rl.close();
|
|
1646
|
-
|
|
1646
|
+
resolve6(answer.trim());
|
|
1647
1647
|
});
|
|
1648
1648
|
});
|
|
1649
1649
|
}
|
|
@@ -1765,7 +1765,7 @@ async function checkPython() {
|
|
|
1765
1765
|
};
|
|
1766
1766
|
}
|
|
1767
1767
|
async function checkManim() {
|
|
1768
|
-
const probe = await new Promise((
|
|
1768
|
+
const probe = await new Promise((resolve6) => {
|
|
1769
1769
|
const p = spawn2("python3", ["-m", "manim", "--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1770
1770
|
let stdout = "";
|
|
1771
1771
|
let stderr = "";
|
|
@@ -1775,18 +1775,18 @@ async function checkManim() {
|
|
|
1775
1775
|
p.stderr.on("data", (d) => {
|
|
1776
1776
|
stderr += d.toString("utf8");
|
|
1777
1777
|
});
|
|
1778
|
-
p.on("error", () =>
|
|
1778
|
+
p.on("error", () => resolve6({ installed: false, version: null }));
|
|
1779
1779
|
p.on("close", (code) => {
|
|
1780
1780
|
if (code === 0) {
|
|
1781
1781
|
const m = (stdout + stderr).match(/Manim Community v(\S+)/);
|
|
1782
|
-
|
|
1782
|
+
resolve6({ installed: true, version: m?.[1] ?? null });
|
|
1783
1783
|
} else {
|
|
1784
|
-
|
|
1784
|
+
resolve6({ installed: false, version: null });
|
|
1785
1785
|
}
|
|
1786
1786
|
});
|
|
1787
1787
|
setTimeout(() => {
|
|
1788
1788
|
p.kill();
|
|
1789
|
-
|
|
1789
|
+
resolve6({ installed: false, version: null });
|
|
1790
1790
|
}, 5e3);
|
|
1791
1791
|
});
|
|
1792
1792
|
return {
|
|
@@ -1915,11 +1915,11 @@ async function ask(rl, question) {
|
|
|
1915
1915
|
return ans === "" || ans === "y" || ans === "yes";
|
|
1916
1916
|
}
|
|
1917
1917
|
function runStep(argv) {
|
|
1918
|
-
return new Promise((
|
|
1918
|
+
return new Promise((resolve6) => {
|
|
1919
1919
|
const [cmd, ...args] = argv;
|
|
1920
1920
|
const proc = spawn3(cmd, args, { stdio: ["inherit", "inherit", "inherit"] });
|
|
1921
|
-
proc.on("error", (err) =>
|
|
1922
|
-
proc.on("close", (code) =>
|
|
1921
|
+
proc.on("error", (err) => resolve6({ ok: false, tail: err.message }));
|
|
1922
|
+
proc.on("close", (code) => resolve6({ ok: code === 0 }));
|
|
1923
1923
|
});
|
|
1924
1924
|
}
|
|
1925
1925
|
async function installRenderersCommand(opts = {}) {
|
|
@@ -2191,7 +2191,7 @@ async function ffmpegConcat(inputs, outPath) {
|
|
|
2191
2191
|
const listBody = inputs.map((p) => `file '${p.replace(/'/g, "'\\''")}'`).join("\n") + "\n";
|
|
2192
2192
|
fs5.writeFileSync(listPath, listBody);
|
|
2193
2193
|
fs5.mkdirSync(path5.dirname(outPath), { recursive: true });
|
|
2194
|
-
await new Promise((
|
|
2194
|
+
await new Promise((resolve6, reject) => {
|
|
2195
2195
|
const proc = spawn4("ffmpeg", [
|
|
2196
2196
|
"-y",
|
|
2197
2197
|
"-f",
|
|
@@ -2212,7 +2212,7 @@ async function ffmpegConcat(inputs, outPath) {
|
|
|
2212
2212
|
fs5.rmSync(tmpDir, { recursive: true, force: true });
|
|
2213
2213
|
} catch {
|
|
2214
2214
|
}
|
|
2215
|
-
if (code === 0)
|
|
2215
|
+
if (code === 0) resolve6();
|
|
2216
2216
|
else reject(new Error(`ffmpeg exited ${code}`));
|
|
2217
2217
|
});
|
|
2218
2218
|
});
|
|
@@ -2293,23 +2293,58 @@ async function uploadClip(slug, diff, baseUrl, token) {
|
|
|
2293
2293
|
const stat = await fs6.promises.stat(absPath);
|
|
2294
2294
|
const durationSec = Math.max(1, Math.round(stat.size / 25e4));
|
|
2295
2295
|
const durationFrames = durationSec * 30;
|
|
2296
|
-
const
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2296
|
+
const aspect = artifact.aspect ?? "16:9";
|
|
2297
|
+
const mint = await mintUploadUrl(slug, baseUrl, token, {
|
|
2298
|
+
kind: "clips",
|
|
2299
|
+
chunkId: artifact.chunkId,
|
|
2300
|
+
aspect,
|
|
2301
|
+
contentType: "video/mp4"
|
|
2302
|
+
});
|
|
2303
|
+
await putToStorj(mint.signedUrl, buf, "video/mp4");
|
|
2304
|
+
const fin = await postFinalize(slug, baseUrl, token, {
|
|
2305
|
+
kind: "clips",
|
|
2306
|
+
storjKey: mint.storjKey,
|
|
2307
|
+
chunkId: artifact.chunkId,
|
|
2308
|
+
aspect,
|
|
2309
|
+
version: mint.version,
|
|
2310
|
+
engine: "remotion",
|
|
2311
|
+
durationSec,
|
|
2312
|
+
durationFrames
|
|
2313
|
+
});
|
|
2314
|
+
return { storjKey: mint.storjKey, clipId: fin.clipId, version: mint.version };
|
|
2315
|
+
}
|
|
2316
|
+
async function mintUploadUrl(slug, baseUrl, token, args) {
|
|
2317
|
+
const resp = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}/upload-url`, {
|
|
2304
2318
|
method: "POST",
|
|
2305
|
-
headers: { Authorization: `Bearer ${token}
|
|
2306
|
-
body:
|
|
2307
|
-
signal: AbortSignal.timeout(
|
|
2319
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
2320
|
+
body: JSON.stringify(args),
|
|
2321
|
+
signal: AbortSignal.timeout(6e4)
|
|
2308
2322
|
});
|
|
2309
2323
|
const text = await resp.text().catch(() => "");
|
|
2324
|
+
if (!resp.ok) throw new Error(`mint-url HTTP ${resp.status}: ${text.slice(0, 300)}`);
|
|
2325
|
+
return JSON.parse(text);
|
|
2326
|
+
}
|
|
2327
|
+
async function putToStorj(signedUrl, body, contentType) {
|
|
2328
|
+
const resp = await fetch(signedUrl, {
|
|
2329
|
+
method: "PUT",
|
|
2330
|
+
headers: { "Content-Type": contentType },
|
|
2331
|
+
body: new Uint8Array(body),
|
|
2332
|
+
signal: AbortSignal.timeout(15 * 6e4)
|
|
2333
|
+
});
|
|
2310
2334
|
if (!resp.ok) {
|
|
2311
|
-
|
|
2335
|
+
const text = await resp.text().catch(() => "");
|
|
2336
|
+
throw new Error(`storj PUT HTTP ${resp.status}: ${text.slice(0, 200)}`);
|
|
2312
2337
|
}
|
|
2338
|
+
}
|
|
2339
|
+
async function postFinalize(slug, baseUrl, token, args) {
|
|
2340
|
+
const resp = await fetch(`${baseUrl}/api/projects/${encodeURIComponent(slug)}/upload-finalize`, {
|
|
2341
|
+
method: "POST",
|
|
2342
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
|
2343
|
+
body: JSON.stringify(args),
|
|
2344
|
+
signal: AbortSignal.timeout(6e4)
|
|
2345
|
+
});
|
|
2346
|
+
const text = await resp.text().catch(() => "");
|
|
2347
|
+
if (!resp.ok) throw new Error(`finalize HTTP ${resp.status}: ${text.slice(0, 300)}`);
|
|
2313
2348
|
return JSON.parse(text);
|
|
2314
2349
|
}
|
|
2315
2350
|
function resolveBridgeToken(explicit) {
|
|
@@ -2326,13 +2361,82 @@ function humanBytes(n) {
|
|
|
2326
2361
|
}
|
|
2327
2362
|
async function confirm(prompt2) {
|
|
2328
2363
|
const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
|
|
2329
|
-
const answer = await new Promise((
|
|
2364
|
+
const answer = await new Promise((resolve6) => rl.question(prompt2, resolve6));
|
|
2330
2365
|
rl.close();
|
|
2331
2366
|
return /^y(es)?$/i.test(answer.trim());
|
|
2332
2367
|
}
|
|
2333
2368
|
|
|
2369
|
+
// src/commands/push-hook.ts
|
|
2370
|
+
import * as fs7 from "fs";
|
|
2371
|
+
import * as path7 from "path";
|
|
2372
|
+
var ALLOWED_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov"]);
|
|
2373
|
+
async function pushHookCommand(opts) {
|
|
2374
|
+
const token = resolveBridgeToken2(opts.token);
|
|
2375
|
+
if (!token) {
|
|
2376
|
+
throw new Error(
|
|
2377
|
+
"push-hook requires a bridge token. Run `storyforge login`, set BRIDGE_TOKEN, or pass --token <bearer>."
|
|
2378
|
+
);
|
|
2379
|
+
}
|
|
2380
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
2381
|
+
if (opts.files.length === 0) throw new Error("no files to upload");
|
|
2382
|
+
log.info(`push-hook \xB7 slug=${opts.slug}`);
|
|
2383
|
+
log.info(`api \xB7 ${baseUrl}`);
|
|
2384
|
+
log.info(`files \xB7 ${opts.files.length}`);
|
|
2385
|
+
let succeeded = 0;
|
|
2386
|
+
let failed = 0;
|
|
2387
|
+
for (const filePath of opts.files) {
|
|
2388
|
+
const absPath = path7.resolve(filePath);
|
|
2389
|
+
if (!fs7.existsSync(absPath)) {
|
|
2390
|
+
log.warn(` \u2717 ${filePath} \u2014 not found`);
|
|
2391
|
+
failed += 1;
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
const ext = path7.extname(absPath).toLowerCase();
|
|
2395
|
+
if (!ALLOWED_EXTS.has(ext)) {
|
|
2396
|
+
log.warn(` \u2717 ${filePath} \u2014 unsupported extension ${ext}`);
|
|
2397
|
+
failed += 1;
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
const filename = path7.basename(absPath);
|
|
2401
|
+
const stat = fs7.statSync(absPath);
|
|
2402
|
+
const sizeMB = (stat.size / 1024 / 1024).toFixed(1);
|
|
2403
|
+
try {
|
|
2404
|
+
const buf = fs7.readFileSync(absPath);
|
|
2405
|
+
const contentType = ext === ".webm" ? "video/webm" : ext === ".mov" ? "video/quicktime" : "video/mp4";
|
|
2406
|
+
const mint = await mintUploadUrl(opts.slug, baseUrl, token, {
|
|
2407
|
+
kind: "hooks",
|
|
2408
|
+
filename,
|
|
2409
|
+
contentType
|
|
2410
|
+
});
|
|
2411
|
+
log.info(` \u2191 ${filename} (${sizeMB}M) \u279C ${mint.storjKey}`);
|
|
2412
|
+
await putToStorj(mint.signedUrl, buf, contentType);
|
|
2413
|
+
await postFinalize(opts.slug, baseUrl, token, {
|
|
2414
|
+
kind: "hooks",
|
|
2415
|
+
storjKey: mint.storjKey,
|
|
2416
|
+
filename
|
|
2417
|
+
});
|
|
2418
|
+
log.success(` \u2713 uploaded`);
|
|
2419
|
+
succeeded += 1;
|
|
2420
|
+
} catch (err) {
|
|
2421
|
+
log.warn(` \u2717 ${err.message}`);
|
|
2422
|
+
failed += 1;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
log.info(`done \xB7 ${succeeded} uploaded \xB7 ${failed} failed`);
|
|
2426
|
+
if (failed > 0 && succeeded === 0) {
|
|
2427
|
+
throw new Error("all hooks failed to upload");
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
function resolveBridgeToken2(explicit) {
|
|
2431
|
+
if (explicit) return explicit;
|
|
2432
|
+
if (process.env.FORGE_BRIDGE_TOKEN) return process.env.FORGE_BRIDGE_TOKEN;
|
|
2433
|
+
if (process.env.BRIDGE_TOKEN) return process.env.BRIDGE_TOKEN;
|
|
2434
|
+
loadDotEnv();
|
|
2435
|
+
return process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? null;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2334
2438
|
// src/index.ts
|
|
2335
|
-
var VERSION = "0.
|
|
2439
|
+
var VERSION = "0.10.0";
|
|
2336
2440
|
var HELP = `
|
|
2337
2441
|
storyforge \u2014 local bridge for the Forge video production web app
|
|
2338
2442
|
|
|
@@ -2340,6 +2444,8 @@ Usage:
|
|
|
2340
2444
|
storyforge Auto-link current folder, start bridge, open browser (default)
|
|
2341
2445
|
storyforge render <slug> Pull a published project and render it locally to ./out.mp4
|
|
2342
2446
|
storyforge push <slug> Upload locally-edited clips back to Storj
|
|
2447
|
+
storyforge push-hook <slug> <file> [<file>...]
|
|
2448
|
+
Upload standalone hook MP4(s) to a project on Storj
|
|
2343
2449
|
storyforge login Log in to forge.algo-thinker.com
|
|
2344
2450
|
storyforge doctor Probe local renderers (ffmpeg / python / manim / hyperframes)
|
|
2345
2451
|
storyforge install-renderers Install missing renderers (interactive, asks per-tool)
|
|
@@ -2415,6 +2521,31 @@ async function main() {
|
|
|
2415
2521
|
await installRenderersCommand({ yes });
|
|
2416
2522
|
return;
|
|
2417
2523
|
}
|
|
2524
|
+
if (firstArg === "push-hook") {
|
|
2525
|
+
const slug = args[1];
|
|
2526
|
+
if (!slug || slug.startsWith("--")) {
|
|
2527
|
+
console.error("storyforge push-hook: missing slug. Usage: storyforge push-hook <slug> <file> [<file>...]");
|
|
2528
|
+
process.exit(1);
|
|
2529
|
+
}
|
|
2530
|
+
const files = [];
|
|
2531
|
+
let i = 2;
|
|
2532
|
+
while (i < args.length && !args[i].startsWith("--")) {
|
|
2533
|
+
files.push(args[i]);
|
|
2534
|
+
i += 1;
|
|
2535
|
+
}
|
|
2536
|
+
if (files.length === 0) {
|
|
2537
|
+
console.error("storyforge push-hook: no files supplied. Usage: storyforge push-hook <slug> <file> [<file>...]");
|
|
2538
|
+
process.exit(1);
|
|
2539
|
+
}
|
|
2540
|
+
const opts2 = parseArgs(args.slice(i));
|
|
2541
|
+
await pushHookCommand({
|
|
2542
|
+
slug,
|
|
2543
|
+
files,
|
|
2544
|
+
baseUrl: typeof opts2["base-url"] === "string" ? opts2["base-url"] : void 0,
|
|
2545
|
+
token: typeof opts2.token === "string" ? opts2.token : void 0
|
|
2546
|
+
});
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2418
2549
|
if (firstArg === "push") {
|
|
2419
2550
|
const slug = args[1];
|
|
2420
2551
|
if (!slug || slug.startsWith("--")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "StoryForge — local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|