reelme 0.2.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/package.json +34 -0
- package/src/cache.mjs +148 -0
- package/src/cli.mjs +57 -0
- package/src/render.mjs +47 -0
- package/template/package.json +29 -0
- package/template/pnpm-lock.yaml +3522 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/public/.gitkeep +0 -0
- package/template/remotion.config.ts +4 -0
- package/template/src/Root.tsx +127 -0
- package/template/src/brief.json +131 -0
- package/template/src/brief.ts +195 -0
- package/template/src/components/primitives/Arrow.tsx +83 -0
- package/template/src/components/primitives/Caption.tsx +52 -0
- package/template/src/components/primitives/CodeBlock.tsx +159 -0
- package/template/src/components/primitives/Icon.tsx +68 -0
- package/template/src/components/primitives/KeyPill.tsx +44 -0
- package/template/src/components/primitives/Label.tsx +49 -0
- package/template/src/components/primitives/Terminal.tsx +110 -0
- package/template/src/components/scenes/BrowserFrame.tsx +139 -0
- package/template/src/components/scenes/CTA.tsx +118 -0
- package/template/src/components/scenes/CodeReveal.tsx +41 -0
- package/template/src/components/scenes/DataFlow.tsx +102 -0
- package/template/src/components/scenes/FeatureList.tsx +110 -0
- package/template/src/components/scenes/FileTree.tsx +125 -0
- package/template/src/components/scenes/HotkeyScene.tsx +77 -0
- package/template/src/components/scenes/MobileScreen.tsx +212 -0
- package/template/src/components/scenes/OSWindowScene.tsx +166 -0
- package/template/src/components/scenes/Problem.tsx +105 -0
- package/template/src/components/scenes/SplitComparison.tsx +136 -0
- package/template/src/components/scenes/StatCallout.tsx +110 -0
- package/template/src/components/scenes/TerminalScene.tsx +41 -0
- package/template/src/duration.ts +55 -0
- package/template/src/fonts.ts +49 -0
- package/template/src/index.ts +106 -0
- package/template/src/platforms.json +78 -0
- package/template/src/platforms.ts +36 -0
- package/template/src/theme.ts +88 -0
- package/template/tsconfig.json +15 -0
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reelme",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Turn your repo into launch videos for social platforms. Local Remotion rendering, zero cloud.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"reelme": "src/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"src",
|
|
11
|
+
"template",
|
|
12
|
+
"!template/out",
|
|
13
|
+
"!template/src/__tests__",
|
|
14
|
+
"!template/eslint.config.mjs",
|
|
15
|
+
"!template/vitest.config.ts",
|
|
16
|
+
"!template/public/logo.png"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"launch-video",
|
|
23
|
+
"remotion",
|
|
24
|
+
"video",
|
|
25
|
+
"social",
|
|
26
|
+
"readme",
|
|
27
|
+
"devtools"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/RubenGlez/reelme.git"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT"
|
|
34
|
+
}
|
package/src/cache.mjs
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Cache and scaffold management. The Remotion project never lives in the
|
|
2
|
+
// user's repo: it is scaffolded from the bundled template into
|
|
3
|
+
// ~/.reelme/cache/<project-hash>/ and reused across renders.
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import {
|
|
7
|
+
cpSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
} from "node:fs";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { basename, dirname, join } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { spawnSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
export const CACHE_ROOT = join(homedir(), ".reelme", "cache");
|
|
20
|
+
|
|
21
|
+
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
22
|
+
export const TEMPLATE_DIR = join(PKG_ROOT, "template");
|
|
23
|
+
|
|
24
|
+
const CLI_VERSION = JSON.parse(
|
|
25
|
+
readFileSync(join(PKG_ROOT, "package.json"), "utf8")
|
|
26
|
+
).version;
|
|
27
|
+
|
|
28
|
+
// Not copied into the cache scaffold (mirrors the old SKILL.md rsync excludes).
|
|
29
|
+
const SCAFFOLD_EXCLUDES = new Set([
|
|
30
|
+
"node_modules",
|
|
31
|
+
"out",
|
|
32
|
+
"__tests__",
|
|
33
|
+
".gitignore",
|
|
34
|
+
"eslint.config.mjs",
|
|
35
|
+
"vitest.config.ts",
|
|
36
|
+
"logo.png", // template sample; users provide their own logo
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export function fail(message) {
|
|
40
|
+
console.error(`reelme: ${message}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadPlatforms() {
|
|
45
|
+
return JSON.parse(
|
|
46
|
+
readFileSync(join(TEMPLATE_DIR, "src", "platforms.json"), "utf8")
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readBrief(repoRoot) {
|
|
51
|
+
const briefPath = join(repoRoot, "reelme.json");
|
|
52
|
+
if (!existsSync(briefPath)) {
|
|
53
|
+
fail(
|
|
54
|
+
`no reelme.json found in ${repoRoot}. Run the /reelme skill to create one.`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
let brief;
|
|
58
|
+
try {
|
|
59
|
+
brief = JSON.parse(readFileSync(briefPath, "utf8"));
|
|
60
|
+
} catch (e) {
|
|
61
|
+
fail(`reelme.json is not valid JSON: ${e.message}`);
|
|
62
|
+
}
|
|
63
|
+
if (brief.schemaVersion !== 2) {
|
|
64
|
+
fail(
|
|
65
|
+
`reelme.json has schemaVersion ${brief.schemaVersion ?? "(none)"}; this CLI requires schemaVersion 2. ` +
|
|
66
|
+
`Briefs with a top-level "scenes" array or a "format" field are v1 — re-run the /reelme skill to migrate.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const platforms = loadPlatforms();
|
|
70
|
+
const ids = brief.project?.platforms;
|
|
71
|
+
if (!Array.isArray(ids) || ids.length === 0) {
|
|
72
|
+
fail(
|
|
73
|
+
`project.platforms is missing or empty. Pick at least one of: ${Object.keys(platforms).join(", ")}.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const unknown = ids.filter((id) => !platforms[id]);
|
|
77
|
+
if (unknown.length > 0) {
|
|
78
|
+
fail(
|
|
79
|
+
`unknown platform(s): ${unknown.join(", ")}. Valid platforms: ${Object.keys(platforms).join(", ")}.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(brief.cuts?.main) || brief.cuts.main.length === 0) {
|
|
83
|
+
fail(`cuts.main is missing or empty — the main cut is required.`);
|
|
84
|
+
}
|
|
85
|
+
return brief;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function projectCacheDir(repoRoot) {
|
|
89
|
+
const hash = createHash("sha256").update(repoRoot).digest("hex").slice(0, 12);
|
|
90
|
+
return join(CACHE_ROOT, hash);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function run(command, args, cwd) {
|
|
94
|
+
const result = spawnSync(command, args, { cwd, stdio: "inherit" });
|
|
95
|
+
if (result.error) fail(`failed to run ${command}: ${result.error.message}`);
|
|
96
|
+
if (result.status !== 0) {
|
|
97
|
+
fail(`${command} ${args.join(" ")} exited with status ${result.status}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Scaffolds (or reuses) the cache project and syncs the user's brief into it.
|
|
102
|
+
// Returns the cache directory. Re-renders never reinstall; a CLI version bump
|
|
103
|
+
// rebuilds the scaffold so template fixes reach every user on next render.
|
|
104
|
+
export function ensureScaffold(repoRoot) {
|
|
105
|
+
const cacheDir = projectCacheDir(repoRoot);
|
|
106
|
+
const versionMarker = join(cacheDir, ".reelme-template-version");
|
|
107
|
+
|
|
108
|
+
const stale =
|
|
109
|
+
existsSync(versionMarker) &&
|
|
110
|
+
readFileSync(versionMarker, "utf8").trim() !== CLI_VERSION;
|
|
111
|
+
if (stale) {
|
|
112
|
+
console.log(
|
|
113
|
+
`reelme: template updated (CLI ${CLI_VERSION}) — rebuilding the cache scaffold.`
|
|
114
|
+
);
|
|
115
|
+
rmSync(cacheDir, { recursive: true, force: true });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!existsSync(join(cacheDir, "package.json"))) {
|
|
119
|
+
console.log(`reelme: scaffolding Remotion project in ${cacheDir}`);
|
|
120
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
121
|
+
cpSync(TEMPLATE_DIR, cacheDir, {
|
|
122
|
+
recursive: true,
|
|
123
|
+
filter: (src) => !SCAFFOLD_EXCLUDES.has(basename(src)),
|
|
124
|
+
});
|
|
125
|
+
writeFileSync(versionMarker, `${CLI_VERSION}\n`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The user's brief always wins over the template sample.
|
|
129
|
+
cpSync(join(repoRoot, "reelme.json"), join(cacheDir, "src", "brief.json"));
|
|
130
|
+
|
|
131
|
+
if (!existsSync(join(cacheDir, "node_modules"))) {
|
|
132
|
+
console.log("reelme: installing render dependencies (first run only)…");
|
|
133
|
+
run("pnpm", ["install"], cacheDir);
|
|
134
|
+
// esbuild needs its post-install script; pnpm-workspace.yaml persists the approval.
|
|
135
|
+
run("pnpm", ["approve-builds", "--all"], cacheDir);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return cacheDir;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function cleanCache() {
|
|
142
|
+
if (!existsSync(CACHE_ROOT)) {
|
|
143
|
+
console.log("reelme: cache is already empty.");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
rmSync(CACHE_ROOT, { recursive: true, force: true });
|
|
147
|
+
console.log(`reelme: removed ${CACHE_ROOT}`);
|
|
148
|
+
}
|
package/src/cli.mjs
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// reelme — thin CLI owning the mechanics (ADR 0003): scaffold in a global
|
|
3
|
+
// cache, render per-platform variants, open Studio, clean up. The agent
|
|
4
|
+
// skill owns the intelligence and writes reelme.json; this CLI never asks
|
|
5
|
+
// questions.
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { ensureScaffold, cleanCache, fail } from "./cache.mjs";
|
|
9
|
+
import { render } from "./render.mjs";
|
|
10
|
+
|
|
11
|
+
const USAGE = `reelme — launch videos for your repo, rendered locally
|
|
12
|
+
|
|
13
|
+
Usage: npx reelme <command>
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
render Render every platform in reelme.json to ./reelme-out/
|
|
17
|
+
studio Open Remotion Studio against the cached project (preview/tweak)
|
|
18
|
+
clean Remove the reelme cache (~/.reelme/cache)
|
|
19
|
+
|
|
20
|
+
The brief (reelme.json) lives at your repo root — create it with the
|
|
21
|
+
/reelme agent skill: npx skills add RubenGlez/reelme
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
function studio(repoRoot) {
|
|
25
|
+
const cacheDir = ensureScaffold(repoRoot);
|
|
26
|
+
console.log("reelme: opening Remotion Studio (Ctrl+C to stop)…");
|
|
27
|
+
const result = spawnSync("pnpm", ["exec", "remotion", "studio"], {
|
|
28
|
+
cwd: cacheDir,
|
|
29
|
+
stdio: "inherit",
|
|
30
|
+
});
|
|
31
|
+
if (result.error) fail(`failed to open Studio: ${result.error.message}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const command = process.argv[2];
|
|
35
|
+
const repoRoot = process.cwd();
|
|
36
|
+
|
|
37
|
+
switch (command) {
|
|
38
|
+
case "render":
|
|
39
|
+
render(repoRoot);
|
|
40
|
+
break;
|
|
41
|
+
case "studio":
|
|
42
|
+
studio(repoRoot);
|
|
43
|
+
break;
|
|
44
|
+
case "clean":
|
|
45
|
+
cleanCache();
|
|
46
|
+
break;
|
|
47
|
+
case "--help":
|
|
48
|
+
case "-h":
|
|
49
|
+
case "help":
|
|
50
|
+
case undefined:
|
|
51
|
+
console.log(USAGE);
|
|
52
|
+
break;
|
|
53
|
+
default:
|
|
54
|
+
console.error(`reelme: unknown command "${command}"\n`);
|
|
55
|
+
console.log(USAGE);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
package/src/render.mjs
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The render command: one output file per selected platform, plus a teaser
|
|
2
|
+
// variant per social platform when the brief has a teaser cut. Outputs land
|
|
3
|
+
// in <repo>/reelme-out/; the heavy Remotion project stays in the cache.
|
|
4
|
+
|
|
5
|
+
import { cpSync, mkdirSync } from "node:fs";
|
|
6
|
+
import { basename, join } from "node:path";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { ensureScaffold, loadPlatforms, readBrief, fail } from "./cache.mjs";
|
|
9
|
+
|
|
10
|
+
function renderComposition(cacheDir, compositionId, outFile, codec) {
|
|
11
|
+
const args = ["exec", "remotion", "render", compositionId, join("out", outFile)];
|
|
12
|
+
if (codec === "gif") args.push("--codec=gif");
|
|
13
|
+
console.log(`reelme: rendering ${compositionId} → reelme-out/${outFile}`);
|
|
14
|
+
const result = spawnSync("pnpm", args, { cwd: cacheDir, stdio: "inherit" });
|
|
15
|
+
if (result.error || result.status !== 0) {
|
|
16
|
+
fail(`render failed for platform composition "${compositionId}".`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function render(repoRoot) {
|
|
21
|
+
const brief = readBrief(repoRoot);
|
|
22
|
+
const platforms = loadPlatforms();
|
|
23
|
+
const cacheDir = ensureScaffold(repoRoot);
|
|
24
|
+
const outDir = join(repoRoot, "reelme-out");
|
|
25
|
+
mkdirSync(outDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
for (const id of brief.project.platforms) {
|
|
28
|
+
const preset = platforms[id];
|
|
29
|
+
const outFile = basename(preset.output.file);
|
|
30
|
+
renderComposition(cacheDir, `Reel-${id}`, outFile, preset.output.codec);
|
|
31
|
+
cpSync(join(cacheDir, "out", outFile), join(outDir, outFile));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const hasTeaser = Array.isArray(brief.cuts.teaser) && brief.cuts.teaser.length > 0;
|
|
35
|
+
if (hasTeaser) {
|
|
36
|
+
const socialIds = brief.project.platforms.filter(
|
|
37
|
+
(id) => platforms[id].output.codec !== "gif"
|
|
38
|
+
);
|
|
39
|
+
for (const id of socialIds) {
|
|
40
|
+
const outFile = `${id}-teaser.mp4`;
|
|
41
|
+
renderComposition(cacheDir, `Reel-${id}-teaser`, outFile, "h264");
|
|
42
|
+
cpSync(join(cacheDir, "out", outFile), join(outDir, outFile));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.log(`reelme: done — outputs in ${outDir}`);
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "reelme-video",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"start": "npx remotion studio",
|
|
7
|
+
"typecheck": "tsc --noEmit",
|
|
8
|
+
"lint": "eslint src",
|
|
9
|
+
"test": "vitest run"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@remotion/cli": "4.0.290",
|
|
13
|
+
"@remotion/google-fonts": "4.0.290",
|
|
14
|
+
"chroma-js": "^3.1.2",
|
|
15
|
+
"lucide-react": "^0.511.0",
|
|
16
|
+
"remotion": "4.0.290"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"@types/chroma-js": "^2.4.4",
|
|
20
|
+
"@types/react": "^18.3.0",
|
|
21
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
22
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
23
|
+
"eslint": "^9.0.0",
|
|
24
|
+
"react": "^18.3.0",
|
|
25
|
+
"react-dom": "^18.3.0",
|
|
26
|
+
"typescript": "^5.4.0",
|
|
27
|
+
"vitest": "^4.1.0"
|
|
28
|
+
}
|
|
29
|
+
}
|