storyforge 0.17.0 → 0.18.1
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
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
log
|
|
4
4
|
} from "./chunk-GJQ45C5W.js";
|
|
5
|
+
import {
|
|
6
|
+
concatWithExtras
|
|
7
|
+
} from "./chunk-7KBSDIP5.js";
|
|
8
|
+
import "./chunk-ZHECC3YI.js";
|
|
5
9
|
import "./chunk-NSPRIPOP.js";
|
|
6
10
|
|
|
7
11
|
// src/commands/dev.ts
|
|
@@ -9,9 +13,18 @@ import * as fs2 from "fs";
|
|
|
9
13
|
import * as os2 from "os";
|
|
10
14
|
import * as path2 from "path";
|
|
11
15
|
import * as http from "http";
|
|
12
|
-
import {
|
|
16
|
+
import { exec as execCb, execSync, spawn } from "child_process";
|
|
13
17
|
import { promisify } from "util";
|
|
14
18
|
|
|
19
|
+
// src/utils/open-browser.ts
|
|
20
|
+
import { execFile } from "child_process";
|
|
21
|
+
function openBrowser(url) {
|
|
22
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
23
|
+
execFile(cmd, [url], (err) => {
|
|
24
|
+
if (err) log.warn(`Could not open browser automatically. Open manually: ${url}`);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
// src/config.ts
|
|
16
29
|
import * as fs from "fs";
|
|
17
30
|
import * as path from "path";
|
|
@@ -24,13 +37,30 @@ var FORGE_ENV_PATHS = [
|
|
|
24
37
|
path.join(os.homedir(), ".forge.env")
|
|
25
38
|
// ~/.forge.env (legacy short form)
|
|
26
39
|
];
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
return FORGE_DIR;
|
|
40
|
+
function normalizeBridgeUrl(url) {
|
|
41
|
+
return url.replace(/\/+$/, "");
|
|
30
42
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
function readCredsFile(credsPath) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(credsPath, "utf-8"));
|
|
46
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
47
|
+
} catch {
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function readLinks(file) {
|
|
52
|
+
const raw = file.links;
|
|
53
|
+
if (!Array.isArray(raw)) return [];
|
|
54
|
+
return raw.filter(
|
|
55
|
+
(l) => !!l && typeof l === "object" && typeof l.url === "string" && typeof l.workerToken === "string" && typeof l.label === "string"
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function saveCredentials(creds, credsPath = CREDS_PATH) {
|
|
59
|
+
fs.mkdirSync(path.dirname(credsPath), { recursive: true });
|
|
60
|
+
const links = readLinks(readCredsFile(credsPath));
|
|
61
|
+
const out = { ...creds };
|
|
62
|
+
if (links.length > 0) out.links = links;
|
|
63
|
+
fs.writeFileSync(credsPath, JSON.stringify(out, null, 2));
|
|
34
64
|
}
|
|
35
65
|
function loadCredentials() {
|
|
36
66
|
try {
|
|
@@ -39,6 +69,35 @@ function loadCredentials() {
|
|
|
39
69
|
return null;
|
|
40
70
|
}
|
|
41
71
|
}
|
|
72
|
+
function saveLink(entry, credsPath = CREDS_PATH) {
|
|
73
|
+
fs.mkdirSync(path.dirname(credsPath), { recursive: true });
|
|
74
|
+
const file = readCredsFile(credsPath);
|
|
75
|
+
const url = normalizeBridgeUrl(entry.url);
|
|
76
|
+
const links = readLinks(file).filter((l) => normalizeBridgeUrl(l.url) !== url);
|
|
77
|
+
links.push({ url, workerToken: entry.workerToken, label: entry.label });
|
|
78
|
+
file.links = links;
|
|
79
|
+
fs.writeFileSync(credsPath, JSON.stringify(file, null, 2), { mode: 384 });
|
|
80
|
+
try {
|
|
81
|
+
fs.chmodSync(credsPath, 384);
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function findLink(url, credsPath = CREDS_PATH) {
|
|
86
|
+
const target = normalizeBridgeUrl(url);
|
|
87
|
+
return readLinks(readCredsFile(credsPath)).find((l) => normalizeBridgeUrl(l.url) === target) ?? null;
|
|
88
|
+
}
|
|
89
|
+
function resolveBridgeToken(opts) {
|
|
90
|
+
const env = opts.env ?? process.env;
|
|
91
|
+
const envToken = env.FORGE_BRIDGE_TOKEN ?? env.BRIDGE_TOKEN;
|
|
92
|
+
if (envToken) return { token: envToken, source: "env" };
|
|
93
|
+
const link = findLink(opts.url, opts.credsPath);
|
|
94
|
+
if (link?.workerToken) return { token: link.workerToken, source: "link", label: link.label };
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function resolveBridgeTokenForCommand(opts) {
|
|
98
|
+
if (opts.explicit) return opts.explicit;
|
|
99
|
+
return resolveBridgeToken({ url: opts.url, env: opts.env, credsPath: opts.credsPath })?.token ?? null;
|
|
100
|
+
}
|
|
42
101
|
function findEnvFilesWalkingUp(startDir) {
|
|
43
102
|
const found = [];
|
|
44
103
|
const home = os.homedir();
|
|
@@ -886,12 +945,6 @@ function collectFiles(dir, base = "") {
|
|
|
886
945
|
function stripFences(text) {
|
|
887
946
|
return text.replace(/^```(?:tsx?|jsx?|typescript|javascript)?\n?/m, "").replace(/\n?```\s*$/m, "").trim();
|
|
888
947
|
}
|
|
889
|
-
function openBrowser(url) {
|
|
890
|
-
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
891
|
-
execFile(cmd, [url], (err) => {
|
|
892
|
-
if (err) log.warn(`Could not open browser automatically. Open manually: ${url}`);
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
948
|
async function devCommand(options) {
|
|
896
949
|
const env = loadDotEnv();
|
|
897
950
|
const port = parseInt(options.port || String(PORT), 10);
|
|
@@ -901,10 +954,10 @@ async function devCommand(options) {
|
|
|
901
954
|
if (env.loaded.length > 0) {
|
|
902
955
|
log.info(`Loaded env: ${env.loaded.join(", ")} from ${env.sources.join(", ")}`);
|
|
903
956
|
}
|
|
904
|
-
const
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const credResult = await fetchBridgeCredentials({ apiUrl, token:
|
|
957
|
+
const bridgeApiUrl = process.env.FORGE_BRIDGE_URL ?? WEB_URL;
|
|
958
|
+
const bridge = resolveBridgeToken({ url: bridgeApiUrl });
|
|
959
|
+
if (bridge) {
|
|
960
|
+
const credResult = await fetchBridgeCredentials({ apiUrl: bridgeApiUrl, token: bridge.token });
|
|
908
961
|
if (credResult.error) {
|
|
909
962
|
log.info(`Bridge cred fetch skipped: ${credResult.error}`);
|
|
910
963
|
} else if (credResult.loaded.length > 0) {
|
|
@@ -1676,9 +1729,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1676
1729
|
console.log(` Script generation \u2192 ${scriptGenModel}`);
|
|
1677
1730
|
console.log(` POST http://localhost:${port}/api/script-gen`);
|
|
1678
1731
|
}
|
|
1679
|
-
|
|
1680
|
-
const bridgeToken2 = process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? "";
|
|
1681
|
-
if (bridgeToken2) {
|
|
1732
|
+
if (bridge) {
|
|
1682
1733
|
const pkgVersion = (() => {
|
|
1683
1734
|
try {
|
|
1684
1735
|
const here = path2.dirname(new URL(import.meta.url).pathname);
|
|
@@ -1689,20 +1740,21 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
|
|
|
1689
1740
|
return "0.0.0";
|
|
1690
1741
|
})();
|
|
1691
1742
|
void (async () => {
|
|
1692
|
-
const { BridgePoller } = await import("./bridge-poller-
|
|
1693
|
-
const poller = new BridgePoller({ baseUrl:
|
|
1743
|
+
const { BridgePoller } = await import("./bridge-poller-67VQZZ3U.js");
|
|
1744
|
+
const poller = new BridgePoller({ baseUrl: bridgeApiUrl, token: bridge.token, clientVersion: `storyforge ${pkgVersion}` });
|
|
1694
1745
|
poller.start();
|
|
1695
1746
|
})();
|
|
1696
1747
|
console.log("");
|
|
1697
|
-
|
|
1748
|
+
const via = bridge.source === "link" ? ` (linked as ${bridge.label})` : "";
|
|
1749
|
+
console.log(` Bridge tunnel \u2192 ${bridgeApiUrl}/api/cli-bridge/* (outbound polling)${via}`);
|
|
1698
1750
|
} else {
|
|
1699
1751
|
console.log("");
|
|
1700
|
-
|
|
1701
|
-
console.log(
|
|
1752
|
+
log.warn("Bridge tunnel disabled \u2014 no worker token for this deployment.");
|
|
1753
|
+
console.log(` Run storyforge link to pair this machine with ${bridgeApiUrl} (recommended).`);
|
|
1754
|
+
console.log(" Or provide FORGE_BRIDGE_TOKEN yourself:");
|
|
1702
1755
|
console.log(" a) export FORGE_BRIDGE_TOKEN=xxx in your shell, then re-run storyforge");
|
|
1703
1756
|
console.log(" b) drop FORGE_BRIDGE_TOKEN=xxx into ./.env (cwd) or ~/.forge/.env");
|
|
1704
1757
|
console.log(" c) one-shot: FORGE_BRIDGE_TOKEN=xxx npx storyforge");
|
|
1705
|
-
console.log(` Use the SAME token value you set on Vercel as FORGE_BRIDGE_TOKEN.`);
|
|
1706
1758
|
}
|
|
1707
1759
|
console.log("");
|
|
1708
1760
|
console.log(" Ctrl+C to stop");
|
|
@@ -1753,17 +1805,146 @@ async function loginCommand() {
|
|
|
1753
1805
|
}
|
|
1754
1806
|
}
|
|
1755
1807
|
|
|
1808
|
+
// src/commands/link.ts
|
|
1809
|
+
import * as os3 from "os";
|
|
1810
|
+
|
|
1811
|
+
// package.json
|
|
1812
|
+
var package_default = {
|
|
1813
|
+
name: "storyforge",
|
|
1814
|
+
version: "0.18.1",
|
|
1815
|
+
description: "StoryForge \u2014 local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
|
|
1816
|
+
type: "module",
|
|
1817
|
+
bin: {
|
|
1818
|
+
storyforge: "./dist/index.js"
|
|
1819
|
+
},
|
|
1820
|
+
files: [
|
|
1821
|
+
"dist/",
|
|
1822
|
+
"README.md"
|
|
1823
|
+
],
|
|
1824
|
+
scripts: {
|
|
1825
|
+
build: "tsup",
|
|
1826
|
+
lint: "tsc --noEmit",
|
|
1827
|
+
test: "vitest run",
|
|
1828
|
+
prepublishOnly: "npm run build"
|
|
1829
|
+
},
|
|
1830
|
+
dependencies: {
|
|
1831
|
+
"@ccusage/codex": "18.0.11",
|
|
1832
|
+
"@mozilla/readability": "0.6.0",
|
|
1833
|
+
ccusage: "18.0.11",
|
|
1834
|
+
jsdom: "29.1.1",
|
|
1835
|
+
"react-tweet": "3.3.0"
|
|
1836
|
+
},
|
|
1837
|
+
devDependencies: {
|
|
1838
|
+
tsup: "8.5.0",
|
|
1839
|
+
typescript: "5.8.3"
|
|
1840
|
+
},
|
|
1841
|
+
publishConfig: {
|
|
1842
|
+
access: "public"
|
|
1843
|
+
},
|
|
1844
|
+
keywords: [
|
|
1845
|
+
"video",
|
|
1846
|
+
"remotion",
|
|
1847
|
+
"storyforge",
|
|
1848
|
+
"cli",
|
|
1849
|
+
"forge"
|
|
1850
|
+
],
|
|
1851
|
+
license: "MIT",
|
|
1852
|
+
engines: {
|
|
1853
|
+
node: ">=18.0.0"
|
|
1854
|
+
},
|
|
1855
|
+
repository: {
|
|
1856
|
+
type: "git",
|
|
1857
|
+
url: "git+https://github.com/backspacevenkat/forge.git",
|
|
1858
|
+
directory: "packages/cli"
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
|
|
1862
|
+
// src/commands/link.ts
|
|
1863
|
+
var DEFAULT_BRIDGE_URL = "https://forge.algo-thinker.com";
|
|
1864
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1865
|
+
async function linkCommand(opts = {}) {
|
|
1866
|
+
const baseUrl = normalizeBridgeUrl(opts.baseUrl ?? process.env.FORGE_BRIDGE_URL ?? DEFAULT_BRIDGE_URL);
|
|
1867
|
+
log.banner("\nStoryForge \u2014 Link this machine\n");
|
|
1868
|
+
let start;
|
|
1869
|
+
try {
|
|
1870
|
+
const resp = await fetch(`${baseUrl}/api/cli-bridge/link/start`, {
|
|
1871
|
+
method: "POST",
|
|
1872
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
1873
|
+
body: JSON.stringify({ cliVersion: package_default.version, host: os3.hostname() })
|
|
1874
|
+
});
|
|
1875
|
+
if (!resp.ok) {
|
|
1876
|
+
const text = await resp.text().catch(() => "");
|
|
1877
|
+
log.error(`Could not start link (${resp.status}) at ${baseUrl}: ${text.slice(0, 200)}`);
|
|
1878
|
+
process.exit(1);
|
|
1879
|
+
}
|
|
1880
|
+
start = await resp.json();
|
|
1881
|
+
} catch (err) {
|
|
1882
|
+
log.error(`Could not reach ${baseUrl}: ${err.message}`);
|
|
1883
|
+
process.exit(1);
|
|
1884
|
+
}
|
|
1885
|
+
console.log(` Verification code: ${start.code}`);
|
|
1886
|
+
console.log(` Approve at: ${start.approveUrl}`);
|
|
1887
|
+
console.log("");
|
|
1888
|
+
console.log(" Check that the code above matches the one shown in your browser,");
|
|
1889
|
+
console.log(" then approve. Waiting for approval\u2026");
|
|
1890
|
+
console.log("");
|
|
1891
|
+
if (!opts.noOpen) openBrowser(start.approveUrl);
|
|
1892
|
+
const pollUrl = `${baseUrl}/api/cli-bridge/link/poll?linkId=${encodeURIComponent(start.linkId)}`;
|
|
1893
|
+
const intervalMs = Math.max(1e3, start.pollIntervalMs || 2e3);
|
|
1894
|
+
const expiresAtMs = Date.parse(start.expiresAt);
|
|
1895
|
+
const deadline = Number.isFinite(expiresAtMs) ? expiresAtMs : Date.now() + 10 * 60 * 1e3;
|
|
1896
|
+
while (Date.now() < deadline) {
|
|
1897
|
+
await sleep(intervalMs);
|
|
1898
|
+
let body;
|
|
1899
|
+
try {
|
|
1900
|
+
const resp = await fetch(pollUrl, { headers: { Accept: "application/json" } });
|
|
1901
|
+
if (!resp.ok) continue;
|
|
1902
|
+
body = await resp.json();
|
|
1903
|
+
} catch {
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
switch (body.status) {
|
|
1907
|
+
case "pending":
|
|
1908
|
+
continue;
|
|
1909
|
+
case "approved":
|
|
1910
|
+
if (!body.workerToken || !body.label) {
|
|
1911
|
+
log.error("Approval response was missing the worker token. Run `storyforge link` again.");
|
|
1912
|
+
process.exit(1);
|
|
1913
|
+
}
|
|
1914
|
+
saveLink({ url: baseUrl, workerToken: body.workerToken, label: body.label });
|
|
1915
|
+
log.success(`Linked as ${body.label}`);
|
|
1916
|
+
return;
|
|
1917
|
+
case "denied":
|
|
1918
|
+
log.error("Link request was denied in the browser.");
|
|
1919
|
+
process.exit(1);
|
|
1920
|
+
return;
|
|
1921
|
+
case "consumed":
|
|
1922
|
+
log.error("Link request was already used. Run `storyforge link` again.");
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
return;
|
|
1925
|
+
case "expired":
|
|
1926
|
+
log.error("Link request expired, run `storyforge link` again.");
|
|
1927
|
+
process.exit(1);
|
|
1928
|
+
return;
|
|
1929
|
+
default:
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
log.error("Link request expired, run `storyforge link` again.");
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1756
1937
|
// src/commands/doctor.ts
|
|
1757
1938
|
import { spawn as spawn2 } from "child_process";
|
|
1758
1939
|
import { promisify as promisify2 } from "util";
|
|
1759
1940
|
import { execFile as execFileCb } from "child_process";
|
|
1760
|
-
import
|
|
1941
|
+
import os4 from "os";
|
|
1761
1942
|
var execFile2 = promisify2(execFileCb);
|
|
1762
1943
|
async function runDoctor() {
|
|
1763
|
-
const platform =
|
|
1764
|
-
const arch =
|
|
1765
|
-
const cores =
|
|
1766
|
-
const ramGb = Math.round(
|
|
1944
|
+
const platform = os4.platform();
|
|
1945
|
+
const arch = os4.arch();
|
|
1946
|
+
const cores = os4.cpus().length;
|
|
1947
|
+
const ramGb = Math.round(os4.totalmem() / 1024 ** 3);
|
|
1767
1948
|
const renderers = await Promise.all([
|
|
1768
1949
|
checkFfmpeg(platform),
|
|
1769
1950
|
checkPython(),
|
|
@@ -1834,7 +2015,7 @@ async function checkPython() {
|
|
|
1834
2015
|
installed: probe.installed,
|
|
1835
2016
|
version: probe.version,
|
|
1836
2017
|
rationale: "manim engine runtime",
|
|
1837
|
-
installCommand:
|
|
2018
|
+
installCommand: os4.platform() === "darwin" ? "brew install python@3.12" : "sudo apt install -y python3",
|
|
1838
2019
|
required: false
|
|
1839
2020
|
};
|
|
1840
2021
|
}
|
|
@@ -1936,7 +2117,7 @@ async function doctorCommand() {
|
|
|
1936
2117
|
import { spawn as spawn3 } from "child_process";
|
|
1937
2118
|
import readline2 from "readline/promises";
|
|
1938
2119
|
import { stdin as input, stdout as output } from "process";
|
|
1939
|
-
import
|
|
2120
|
+
import os5 from "os";
|
|
1940
2121
|
function planInstall(missing, platform) {
|
|
1941
2122
|
const steps = [];
|
|
1942
2123
|
for (const r of missing) {
|
|
@@ -2066,15 +2247,13 @@ async function installRenderersCommand(opts = {}) {
|
|
|
2066
2247
|
}
|
|
2067
2248
|
|
|
2068
2249
|
// src/commands/render.ts
|
|
2069
|
-
import { spawn as spawn4 } from "child_process";
|
|
2070
2250
|
import * as fs5 from "fs";
|
|
2071
|
-
import * as os6 from "os";
|
|
2072
2251
|
import * as path5 from "path";
|
|
2073
2252
|
|
|
2074
2253
|
// src/lib/manifest.ts
|
|
2075
2254
|
import * as fs3 from "fs";
|
|
2076
2255
|
import * as path3 from "path";
|
|
2077
|
-
import * as
|
|
2256
|
+
import * as os6 from "os";
|
|
2078
2257
|
var DEFAULT_BASE_URL = "https://forge.algo-thinker.com";
|
|
2079
2258
|
function localCacheRelPath(artifact) {
|
|
2080
2259
|
if (artifact.kind === "clips" && artifact.aspect) {
|
|
@@ -2087,7 +2266,7 @@ function resolveCacheRoot(slug, override) {
|
|
|
2087
2266
|
if (override) return path3.resolve(override, slug);
|
|
2088
2267
|
const envOverride = process.env.FORGE_CACHE_DIR;
|
|
2089
2268
|
if (envOverride) return path3.resolve(envOverride, slug);
|
|
2090
|
-
return path3.join(
|
|
2269
|
+
return path3.join(os6.homedir(), ".forge", "cache", slug);
|
|
2091
2270
|
}
|
|
2092
2271
|
function resolveBaseUrl(override) {
|
|
2093
2272
|
return (override ?? process.env.FORGE_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
@@ -2206,6 +2385,16 @@ async function downloadFile(url, absPath, signal) {
|
|
|
2206
2385
|
function clipsInOrder(ready, aspect) {
|
|
2207
2386
|
return ready.filter(({ artifact }) => artifact.kind === "clips" && (artifact.aspect ?? "16:9") === aspect).map(({ absPath }) => absPath);
|
|
2208
2387
|
}
|
|
2388
|
+
function brandExtrasFromPull(ready) {
|
|
2389
|
+
let watermarkPath;
|
|
2390
|
+
let brandAudioPath;
|
|
2391
|
+
for (const { artifact, absPath } of ready) {
|
|
2392
|
+
if (artifact.kind !== "brand") continue;
|
|
2393
|
+
if (artifact.brandSlot === "watermark") watermarkPath = absPath;
|
|
2394
|
+
else if (artifact.brandSlot === "brand_audio") brandAudioPath = absPath;
|
|
2395
|
+
}
|
|
2396
|
+
return { watermarkPath, brandAudioPath };
|
|
2397
|
+
}
|
|
2209
2398
|
function formatPullSummary(result) {
|
|
2210
2399
|
const parts = [
|
|
2211
2400
|
`${result.fresh} cached`,
|
|
@@ -2228,8 +2417,11 @@ async function renderCommand(opts) {
|
|
|
2228
2417
|
const outPath = path5.resolve(opts.out ?? "out.mp4");
|
|
2229
2418
|
log.info(`render \xB7 slug=${opts.slug} \xB7 aspect=${aspect}`);
|
|
2230
2419
|
log.info(`cache \xB7 ${cacheRoot}`);
|
|
2420
|
+
loadDotEnv();
|
|
2421
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
2422
|
+
const token = resolveBridgeTokenForCommand({ url: baseUrl, explicit: opts.token }) ?? void 0;
|
|
2231
2423
|
log.info(`fetching manifest\u2026`);
|
|
2232
|
-
const manifest = await fetchManifest(opts.slug, { baseUrl
|
|
2424
|
+
const manifest = await fetchManifest(opts.slug, { baseUrl, token });
|
|
2233
2425
|
log.info(`manifest \xB7 ${manifest.artifacts.length} artifact(s) \xB7 updated ${manifest.updatedAt}`);
|
|
2234
2426
|
if (opts.clean && fs5.existsSync(cacheRoot)) {
|
|
2235
2427
|
log.info(`--clean: wiping cache dir`);
|
|
@@ -2254,41 +2446,23 @@ async function renderCommand(opts) {
|
|
|
2254
2446
|
if (clipPaths.length === 0) {
|
|
2255
2447
|
throw new Error(`no clips for aspect ${aspect} \u2014 manifest may be empty or all pulls failed`);
|
|
2256
2448
|
}
|
|
2257
|
-
|
|
2258
|
-
|
|
2449
|
+
const brand = brandExtrasFromPull(pullResult.ready);
|
|
2450
|
+
const brandNote = [
|
|
2451
|
+
brand.watermarkPath ? "watermark" : null,
|
|
2452
|
+
brand.brandAudioPath ? "audio bed" : null
|
|
2453
|
+
].filter(Boolean).join(" + ");
|
|
2454
|
+
log.info(`stitching ${clipPaths.length} clip(s)${brandNote ? ` + brand (${brandNote})` : ""} \u2192 ${outPath}`);
|
|
2455
|
+
await ffmpegConcat(clipPaths, outPath, brand);
|
|
2259
2456
|
const finalStat = fs5.statSync(outPath);
|
|
2260
2457
|
log.success(`done \xB7 ${outPath} (${(finalStat.size / 1024 / 1024).toFixed(1)} MB)`);
|
|
2261
2458
|
}
|
|
2262
|
-
async function ffmpegConcat(inputs, outPath) {
|
|
2263
|
-
const tmpDir = fs5.mkdtempSync(path5.join(os6.tmpdir(), "storyforge-render-"));
|
|
2264
|
-
const listPath = path5.join(tmpDir, "concat.txt");
|
|
2265
|
-
const listBody = inputs.map((p) => `file '${p.replace(/'/g, "'\\''")}'`).join("\n") + "\n";
|
|
2266
|
-
fs5.writeFileSync(listPath, listBody);
|
|
2459
|
+
async function ffmpegConcat(inputs, outPath, brand = {}) {
|
|
2267
2460
|
fs5.mkdirSync(path5.dirname(outPath), { recursive: true });
|
|
2268
|
-
await
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
"-safe",
|
|
2274
|
-
"0",
|
|
2275
|
-
"-i",
|
|
2276
|
-
listPath,
|
|
2277
|
-
"-c",
|
|
2278
|
-
"copy",
|
|
2279
|
-
"-movflags",
|
|
2280
|
-
"+faststart",
|
|
2281
|
-
outPath
|
|
2282
|
-
], { stdio: ["ignore", "inherit", "inherit"] });
|
|
2283
|
-
proc.on("error", reject);
|
|
2284
|
-
proc.on("close", (code) => {
|
|
2285
|
-
try {
|
|
2286
|
-
fs5.rmSync(tmpDir, { recursive: true, force: true });
|
|
2287
|
-
} catch {
|
|
2288
|
-
}
|
|
2289
|
-
if (code === 0) resolve6();
|
|
2290
|
-
else reject(new Error(`ffmpeg exited ${code}`));
|
|
2291
|
-
});
|
|
2461
|
+
await concatWithExtras({
|
|
2462
|
+
segmentPaths: inputs,
|
|
2463
|
+
outPath,
|
|
2464
|
+
watermarkPath: brand.watermarkPath,
|
|
2465
|
+
brandAudioPath: brand.brandAudioPath
|
|
2292
2466
|
});
|
|
2293
2467
|
}
|
|
2294
2468
|
|
|
@@ -2297,13 +2471,14 @@ import * as fs6 from "fs";
|
|
|
2297
2471
|
import * as path6 from "path";
|
|
2298
2472
|
import * as readline3 from "readline";
|
|
2299
2473
|
async function pushCommand(opts) {
|
|
2300
|
-
|
|
2474
|
+
loadDotEnv();
|
|
2475
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
2476
|
+
const token = resolveBridgeTokenForCommand({ url: baseUrl, explicit: opts.token });
|
|
2301
2477
|
if (!token) {
|
|
2302
2478
|
throw new Error(
|
|
2303
|
-
"push requires a bridge token. Run `storyforge
|
|
2479
|
+
"push requires a bridge token. Run `storyforge link`, set BRIDGE_TOKEN, or pass --token <bearer>."
|
|
2304
2480
|
);
|
|
2305
2481
|
}
|
|
2306
|
-
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
2307
2482
|
const cacheRoot = resolveCacheRoot(opts.slug, opts.cache);
|
|
2308
2483
|
log.info(`push \xB7 slug=${opts.slug}`);
|
|
2309
2484
|
log.info(`cache \xB7 ${cacheRoot}`);
|
|
@@ -2421,13 +2596,6 @@ async function postFinalize(slug, baseUrl, token, args) {
|
|
|
2421
2596
|
if (!resp.ok) throw new Error(`finalize HTTP ${resp.status}: ${text.slice(0, 300)}`);
|
|
2422
2597
|
return JSON.parse(text);
|
|
2423
2598
|
}
|
|
2424
|
-
function resolveBridgeToken(explicit) {
|
|
2425
|
-
if (explicit) return explicit;
|
|
2426
|
-
if (process.env.FORGE_BRIDGE_TOKEN) return process.env.FORGE_BRIDGE_TOKEN;
|
|
2427
|
-
if (process.env.BRIDGE_TOKEN) return process.env.BRIDGE_TOKEN;
|
|
2428
|
-
loadDotEnv();
|
|
2429
|
-
return process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? null;
|
|
2430
|
-
}
|
|
2431
2599
|
function humanBytes(n) {
|
|
2432
2600
|
if (n < 1024) return `${n}B`;
|
|
2433
2601
|
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`;
|
|
@@ -2445,13 +2613,14 @@ import * as fs7 from "fs";
|
|
|
2445
2613
|
import * as path7 from "path";
|
|
2446
2614
|
var ALLOWED_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov"]);
|
|
2447
2615
|
async function pushHookCommand(opts) {
|
|
2448
|
-
|
|
2616
|
+
loadDotEnv();
|
|
2617
|
+
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
2618
|
+
const token = resolveBridgeTokenForCommand({ url: baseUrl, explicit: opts.token });
|
|
2449
2619
|
if (!token) {
|
|
2450
2620
|
throw new Error(
|
|
2451
|
-
"push-hook requires a bridge token. Run `storyforge
|
|
2621
|
+
"push-hook requires a bridge token. Run `storyforge link`, set BRIDGE_TOKEN, or pass --token <bearer>."
|
|
2452
2622
|
);
|
|
2453
2623
|
}
|
|
2454
|
-
const baseUrl = resolveBaseUrl(opts.baseUrl);
|
|
2455
2624
|
if (opts.files.length === 0) throw new Error("no files to upload");
|
|
2456
2625
|
log.info(`push-hook \xB7 slug=${opts.slug}`);
|
|
2457
2626
|
log.info(`api \xB7 ${baseUrl}`);
|
|
@@ -2501,63 +2670,6 @@ async function pushHookCommand(opts) {
|
|
|
2501
2670
|
throw new Error("all hooks failed to upload");
|
|
2502
2671
|
}
|
|
2503
2672
|
}
|
|
2504
|
-
function resolveBridgeToken2(explicit) {
|
|
2505
|
-
if (explicit) return explicit;
|
|
2506
|
-
if (process.env.FORGE_BRIDGE_TOKEN) return process.env.FORGE_BRIDGE_TOKEN;
|
|
2507
|
-
if (process.env.BRIDGE_TOKEN) return process.env.BRIDGE_TOKEN;
|
|
2508
|
-
loadDotEnv();
|
|
2509
|
-
return process.env.FORGE_BRIDGE_TOKEN ?? process.env.BRIDGE_TOKEN ?? null;
|
|
2510
|
-
}
|
|
2511
|
-
|
|
2512
|
-
// package.json
|
|
2513
|
-
var package_default = {
|
|
2514
|
-
name: "storyforge",
|
|
2515
|
-
version: "0.17.0",
|
|
2516
|
-
description: "StoryForge \u2014 local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
|
|
2517
|
-
type: "module",
|
|
2518
|
-
bin: {
|
|
2519
|
-
storyforge: "./dist/index.js"
|
|
2520
|
-
},
|
|
2521
|
-
files: [
|
|
2522
|
-
"dist/",
|
|
2523
|
-
"README.md"
|
|
2524
|
-
],
|
|
2525
|
-
scripts: {
|
|
2526
|
-
build: "tsup",
|
|
2527
|
-
lint: "tsc --noEmit",
|
|
2528
|
-
prepublishOnly: "npm run build"
|
|
2529
|
-
},
|
|
2530
|
-
dependencies: {
|
|
2531
|
-
"@ccusage/codex": "18.0.11",
|
|
2532
|
-
"@mozilla/readability": "0.6.0",
|
|
2533
|
-
ccusage: "18.0.11",
|
|
2534
|
-
jsdom: "29.1.1",
|
|
2535
|
-
"react-tweet": "3.3.0"
|
|
2536
|
-
},
|
|
2537
|
-
devDependencies: {
|
|
2538
|
-
tsup: "8.5.0",
|
|
2539
|
-
typescript: "5.8.3"
|
|
2540
|
-
},
|
|
2541
|
-
publishConfig: {
|
|
2542
|
-
access: "public"
|
|
2543
|
-
},
|
|
2544
|
-
keywords: [
|
|
2545
|
-
"video",
|
|
2546
|
-
"remotion",
|
|
2547
|
-
"storyforge",
|
|
2548
|
-
"cli",
|
|
2549
|
-
"forge"
|
|
2550
|
-
],
|
|
2551
|
-
license: "MIT",
|
|
2552
|
-
engines: {
|
|
2553
|
-
node: ">=18.0.0"
|
|
2554
|
-
},
|
|
2555
|
-
repository: {
|
|
2556
|
-
type: "git",
|
|
2557
|
-
url: "git+https://github.com/backspacevenkat/forge.git",
|
|
2558
|
-
directory: "packages/cli"
|
|
2559
|
-
}
|
|
2560
|
-
};
|
|
2561
2673
|
|
|
2562
2674
|
// src/index.ts
|
|
2563
2675
|
var VERSION = package_default.version;
|
|
@@ -2571,6 +2683,7 @@ Usage:
|
|
|
2571
2683
|
storyforge push-hook <slug> <file> [<file>...]
|
|
2572
2684
|
Upload standalone hook MP4(s) to a project on Storj
|
|
2573
2685
|
storyforge login Log in to forge.algo-thinker.com
|
|
2686
|
+
storyforge link Pair this machine with a Forge deployment (mints a bridge worker token)
|
|
2574
2687
|
storyforge doctor Probe local renderers (ffmpeg / python / manim / hyperframes)
|
|
2575
2688
|
storyforge install-renderers Install missing renderers (interactive, asks per-tool)
|
|
2576
2689
|
storyforge install-renderers -y Same, but auto-confirm every prompt
|
|
@@ -2636,6 +2749,14 @@ async function main() {
|
|
|
2636
2749
|
await loginCommand();
|
|
2637
2750
|
return;
|
|
2638
2751
|
}
|
|
2752
|
+
if (firstArg === "link") {
|
|
2753
|
+
const opts2 = parseArgs(args.slice(1));
|
|
2754
|
+
await linkCommand({
|
|
2755
|
+
baseUrl: typeof opts2["base-url"] === "string" ? opts2["base-url"] : void 0,
|
|
2756
|
+
noOpen: opts2.open === false
|
|
2757
|
+
});
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2639
2760
|
if (firstArg === "doctor") {
|
|
2640
2761
|
await doctorCommand();
|
|
2641
2762
|
return;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
LEGACY_SECTION_TRANSITION_ROOT,
|
|
4
|
+
buildFinalEncodeContract,
|
|
5
|
+
channelPackageProvenance,
|
|
6
|
+
concatWithExtras,
|
|
7
|
+
formatSrtTime,
|
|
8
|
+
normalizeClip,
|
|
9
|
+
renderCrossfadeFallback,
|
|
10
|
+
renderSectionTransition,
|
|
11
|
+
resolutionFor,
|
|
12
|
+
resolveChannelPackage,
|
|
13
|
+
srtFromTimings,
|
|
14
|
+
stitchFinalVideo
|
|
15
|
+
} from "./chunk-7KBSDIP5.js";
|
|
16
|
+
import {
|
|
17
|
+
FINAL_LOUDNORM_FILTER,
|
|
18
|
+
buildFinalConcatArgs,
|
|
19
|
+
buildFinalEncodeArgs
|
|
20
|
+
} from "./chunk-ZHECC3YI.js";
|
|
21
|
+
import "./chunk-NSPRIPOP.js";
|
|
22
|
+
export {
|
|
23
|
+
FINAL_LOUDNORM_FILTER,
|
|
24
|
+
LEGACY_SECTION_TRANSITION_ROOT,
|
|
25
|
+
buildFinalConcatArgs,
|
|
26
|
+
buildFinalEncodeArgs,
|
|
27
|
+
buildFinalEncodeContract,
|
|
28
|
+
channelPackageProvenance,
|
|
29
|
+
concatWithExtras,
|
|
30
|
+
formatSrtTime,
|
|
31
|
+
normalizeClip,
|
|
32
|
+
renderCrossfadeFallback,
|
|
33
|
+
renderSectionTransition,
|
|
34
|
+
resolutionFor,
|
|
35
|
+
resolveChannelPackage,
|
|
36
|
+
srtFromTimings,
|
|
37
|
+
stitchFinalVideo
|
|
38
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "storyforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
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": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "tsup",
|
|
15
15
|
"lint": "tsc --noEmit",
|
|
16
|
+
"test": "vitest run",
|
|
16
17
|
"prepublishOnly": "npm run build"
|
|
17
18
|
},
|
|
18
19
|
"dependencies": {
|