@devinilabs/reelstack 1.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.
Files changed (145) hide show
  1. package/LICENSE +128 -0
  2. package/README.md +125 -0
  3. package/cli/beats.js +124 -0
  4. package/cli/bootstrap.js +124 -0
  5. package/cli/capture.js +34 -0
  6. package/cli/direction.js +114 -0
  7. package/cli/icons.js +49 -0
  8. package/cli/index.js +101 -0
  9. package/cli/init.js +253 -0
  10. package/cli/license.js +168 -0
  11. package/cli/lint.js +865 -0
  12. package/cli/preview.js +59 -0
  13. package/cli/render.js +404 -0
  14. package/cli/scaffold.js +239 -0
  15. package/cli/smoke.js +76 -0
  16. package/cli/update.js +26 -0
  17. package/cli/utils.js +184 -0
  18. package/docs/buyers-guide.md +220 -0
  19. package/docs/design-discipline.md +130 -0
  20. package/docs/family-galleries/dark.md +95 -0
  21. package/docs/family-galleries/forbidden.md +78 -0
  22. package/docs/family-galleries/glass.md +98 -0
  23. package/docs/family-galleries/paper.md +82 -0
  24. package/docs/family-galleries/warm.md +86 -0
  25. package/docs/superpowers/plans/2026-05-09-reelstack-init-readiness-gate.md +1166 -0
  26. package/docs/superpowers/specs/2026-05-09-reelstack-init-readiness-gate-design.md +233 -0
  27. package/families/dark/components/DriftingSpotlights.tsx +59 -0
  28. package/families/dark/components/FilmGrain.tsx +44 -0
  29. package/families/dark/components/ForestCard.tsx +43 -0
  30. package/families/dark/components/GridBackground.tsx +29 -0
  31. package/families/dark/components/RadialVignette.tsx +21 -0
  32. package/families/dark/components/Scanlines.tsx +35 -0
  33. package/families/dark/components/SegmentOpacity.ts +37 -0
  34. package/families/dark/components/index.ts +13 -0
  35. package/families/dark/index.ts +31 -0
  36. package/families/dark/palette.ts +98 -0
  37. package/families/dark/presets/claudedispatch.ts +46 -0
  38. package/families/dark/presets/codedrop.ts +37 -0
  39. package/families/dark/presets/gpt55.ts +54 -0
  40. package/families/dark/presets/notebooklm.ts +50 -0
  41. package/families/dark/presets/resourcescta.ts +35 -0
  42. package/families/dark/presets/skills.ts +40 -0
  43. package/families/dark/presets/stitch.ts +46 -0
  44. package/families/dark/presets/stitch2.ts +43 -0
  45. package/families/dark/typography.ts +16 -0
  46. package/families/forbidden/components/ForbiddenCausticBlobs.tsx +52 -0
  47. package/families/forbidden/components/NewsprintTexture.tsx +28 -0
  48. package/families/forbidden/components/TintedShadow.tsx +36 -0
  49. package/families/forbidden/components/index.ts +38 -0
  50. package/families/forbidden/index.ts +17 -0
  51. package/families/forbidden/palette.ts +88 -0
  52. package/families/forbidden/presets/heretic.ts +44 -0
  53. package/families/forbidden/typography.ts +18 -0
  54. package/families/glass/components/BreakdownCard.tsx +158 -0
  55. package/families/glass/components/CausticBlobs.tsx +49 -0
  56. package/families/glass/components/Counter.tsx +72 -0
  57. package/families/glass/components/EyebrowPill.tsx +59 -0
  58. package/families/glass/components/FilmStrip.tsx +202 -0
  59. package/families/glass/components/FloatingGlyphs.tsx +78 -0
  60. package/families/glass/components/GlassCard.tsx +58 -0
  61. package/families/glass/components/GlassCardBezel.tsx +45 -0
  62. package/families/glass/components/HairlineGrid.tsx +30 -0
  63. package/families/glass/components/IridescentRing.tsx +114 -0
  64. package/families/glass/components/IridescentText.tsx +98 -0
  65. package/families/glass/components/LightBeam.tsx +46 -0
  66. package/families/glass/components/ParticleBurst.tsx +62 -0
  67. package/families/glass/components/SonarRings.tsx +81 -0
  68. package/families/glass/components/StaggeredWords.tsx +74 -0
  69. package/families/glass/components/index.ts +20 -0
  70. package/families/glass/index.ts +31 -0
  71. package/families/glass/palette.ts +93 -0
  72. package/families/glass/presets/claudewatch.ts +64 -0
  73. package/families/glass/presets/claudewatchcta.ts +43 -0
  74. package/families/glass/presets/graphify.ts +45 -0
  75. package/families/glass/presets/gstack.ts +48 -0
  76. package/families/glass/presets/jcode.ts +50 -0
  77. package/families/glass/presets/lilagents.ts +52 -0
  78. package/families/glass/presets/paperclip.ts +43 -0
  79. package/families/glass/typography.ts +15 -0
  80. package/families/index.ts +49 -0
  81. package/families/paper/components/CardSpring.tsx +42 -0
  82. package/families/paper/components/CreamGrid.tsx +26 -0
  83. package/families/paper/components/EditorialSerifText.tsx +51 -0
  84. package/families/paper/components/GreenAccentCard.tsx +10 -0
  85. package/families/paper/components/PaperShadow.tsx +30 -0
  86. package/families/paper/components/ScaleBlurText.tsx +40 -0
  87. package/families/paper/components/index.ts +11 -0
  88. package/families/paper/index.ts +23 -0
  89. package/families/paper/palette.ts +102 -0
  90. package/families/paper/presets/designreel.ts +32 -0
  91. package/families/paper/presets/devini3d.ts +45 -0
  92. package/families/paper/presets/justdrop.ts +39 -0
  93. package/families/paper/presets/opus.ts +48 -0
  94. package/families/paper/typography.ts +17 -0
  95. package/families/warm/components/AccentGlow.tsx +60 -0
  96. package/families/warm/components/BentoCell.tsx +56 -0
  97. package/families/warm/components/BentoGrid.tsx +30 -0
  98. package/families/warm/components/FilmGrain.tsx +36 -0
  99. package/families/warm/components/ScaleBlurCounter.tsx +71 -0
  100. package/families/warm/components/WarmSurface.tsx +35 -0
  101. package/families/warm/components/index.ts +11 -0
  102. package/families/warm/index.ts +19 -0
  103. package/families/warm/palette.ts +81 -0
  104. package/families/warm/presets/huashu.ts +49 -0
  105. package/families/warm/presets/mempalace.ts +51 -0
  106. package/families/warm/typography.ts +17 -0
  107. package/package.json +85 -0
  108. package/reference/dark/claudedispatch.tsx +2441 -0
  109. package/reference/dark/notebooklm.tsx +2316 -0
  110. package/reference/dark/stitch.tsx +3040 -0
  111. package/reference/forbidden/heretic.tsx +2636 -0
  112. package/reference/glass/claudewatch.tsx +3827 -0
  113. package/reference/glass/graphify.tsx +2418 -0
  114. package/reference/glass/paperclip.tsx +2218 -0
  115. package/reference/paper/designreel.tsx +883 -0
  116. package/reference/paper/justdrop.tsx +1898 -0
  117. package/reference/paper/opus.tsx +1770 -0
  118. package/reference/warm/huashu.tsx +3413 -0
  119. package/reference/warm/mempalace.tsx +2909 -0
  120. package/skill/SKILL.md +229 -0
  121. package/skill/commands/reelstack-beats.md +20 -0
  122. package/skill/commands/reelstack-capture.md +24 -0
  123. package/skill/commands/reelstack-critique.md +15 -0
  124. package/skill/commands/reelstack-dark.md +40 -0
  125. package/skill/commands/reelstack-direction.md +17 -0
  126. package/skill/commands/reelstack-forbidden.md +25 -0
  127. package/skill/commands/reelstack-glass.md +39 -0
  128. package/skill/commands/reelstack-icons.md +22 -0
  129. package/skill/commands/reelstack-init.md +17 -0
  130. package/skill/commands/reelstack-lint.md +22 -0
  131. package/skill/commands/reelstack-paper.md +36 -0
  132. package/skill/commands/reelstack-render.md +20 -0
  133. package/skill/commands/reelstack-warm.md +36 -0
  134. package/templates/dark/template.tsx +115 -0
  135. package/templates/forbidden/template.tsx +111 -0
  136. package/templates/glass/template.tsx +201 -0
  137. package/templates/paper/template.tsx +133 -0
  138. package/templates/warm/template.tsx +210 -0
  139. package/utils/ai-purple-blocklist.ts +13 -0
  140. package/utils/banned-fonts.ts +11 -0
  141. package/utils/cubic-bezier.ts +36 -0
  142. package/utils/easing.ts +84 -0
  143. package/utils/grid.ts +13 -0
  144. package/utils/render-presets.json +56 -0
  145. package/utils/safe-zones.tsx +57 -0
package/cli/icons.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `reelstack icons <brand> [<brand2> …]` — pull real brand SVGs via the
3
+ * better-icons CLI into public/icons/.
4
+ *
5
+ * House rule: never hand-draw a brand mark when Iconify has the real SVG.
6
+ */
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+ const { spawnSync } = require("node:child_process");
10
+ const { info, success, fail, warn, c, which } = require("./utils");
11
+
12
+ async function run(argv) {
13
+ const brands = argv.filter((a) => !a.startsWith("--"));
14
+ if (brands.length === 0) {
15
+ fail("Usage: reelstack icons <brand> [<brand2> …]");
16
+ process.exit(2);
17
+ }
18
+ const betterIcons = which("better-icons");
19
+ if (!betterIcons) {
20
+ fail("better-icons CLI not found. Install: npm i -g better-icons");
21
+ process.exit(2);
22
+ }
23
+
24
+ const outDir = path.join(process.cwd(), "public", "icons");
25
+ fs.mkdirSync(outDir, { recursive: true });
26
+
27
+ for (const brand of brands) {
28
+ const id = brand.includes(":") ? brand : `logos:${brand}`;
29
+ const out = path.join(outDir, `${brand.replace(":", "-")}.svg`);
30
+ info(`Fetching ${id} → ${path.relative(process.cwd(), out)}…`);
31
+ const result = spawnSync(betterIcons, ["get", id], { encoding: "utf8" });
32
+ if (result.status !== 0 || !result.stdout) {
33
+ warn(`Could not fetch ${id}. Try: better-icons search ${brand}`);
34
+ continue;
35
+ }
36
+ fs.writeFileSync(out, result.stdout, "utf8");
37
+ success(`Saved ${path.relative(process.cwd(), out)}`);
38
+ }
39
+
40
+ console.log("");
41
+ console.log(c.gray("Wire into your reel:"));
42
+ for (const brand of brands) {
43
+ const safe = brand.replace(":", "-");
44
+ console.log(` ${c.cyan(`<Img src={staticFile("icons/${safe}.svg")} style={{width: 96, height: 96}} />`)}`);
45
+ }
46
+ console.log("");
47
+ }
48
+
49
+ module.exports = { run };
package/cli/index.js ADDED
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ReelStack CLI — bin entry point.
4
+ *
5
+ * Routes:
6
+ * reelstack init → cli/init.js
7
+ * reelstack direction "<brief>" → cli/direction.js
8
+ * reelstack scaffold <flags> → cli/scaffold.js
9
+ * reelstack beats <vo.wav> → cli/beats.js
10
+ * reelstack capture <url> → cli/capture.js
11
+ * reelstack icons <brand> → cli/icons.js
12
+ * reelstack render <id> [flags] → cli/render.js
13
+ * reelstack lint <file> [--critique]→ cli/lint.js
14
+ * reelstack preview → cli/preview.js
15
+ * reelstack update → re-verify license, refresh runtime
16
+ * reelstack --version
17
+ * reelstack --verify → 0 if license valid, non-zero otherwise
18
+ */
19
+ const path = require("node:path");
20
+ const { fail, info, success, c } = require("./utils");
21
+ const license = require("./license");
22
+
23
+ const PKG = require("../package.json");
24
+
25
+ const HELP = `
26
+ ${c.bold("ReelStack")} ${c.gray(`v${PKG.version}`)} — Premium 9:16 Reel OS for Remotion
27
+
28
+ ${c.bold("USAGE")}
29
+ reelstack <command> [flags]
30
+
31
+ ${c.bold("COMMANDS")}
32
+ init First-time setup, license verify, dep check
33
+ direction "<brief>" Get 3 differentiated family picks for a vague brief
34
+ scaffold --family --preset --name [--vo] [--reference=<preset>]
35
+ Scaffold a new reel
36
+ beats <vo.wav> Convert voiceover → BEAT constants
37
+ capture <url> Delegate to reel-capture skill
38
+ icons <brand> Pull real brand SVGs via better-icons
39
+ render <id> [--platform=ig|tiktok|shorts] [--format=mp4,gif] [--bgm=<path>] [--interpolate=60]
40
+ Render with platform presets, optional GIF + BGM mix
41
+ lint <file> [--critique] Validate motion floors, safe zones, audio lock; --critique = 5-dim radar + Keep/Fix/Quick
42
+ preview Render a 10-second free demo (no license)
43
+ update Re-verify license, refresh runtime
44
+
45
+ ${c.bold("FLAGS")}
46
+ --version, -v Print version and exit
47
+ --verify Exit 0 if license valid, non-zero otherwise
48
+ --help, -h Show this help
49
+
50
+ ${c.bold("DOCS")}
51
+ ~/.reelstack/docs/buyers-guide.md
52
+ ~/.reelstack/docs/family-galleries/<family>.md
53
+ https://reelstack.dev
54
+ `;
55
+
56
+ async function main() {
57
+ const argv = process.argv.slice(2);
58
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
59
+ console.log(HELP);
60
+ return;
61
+ }
62
+ if (argv.includes("--version") || argv.includes("-v")) {
63
+ console.log(PKG.version);
64
+ return;
65
+ }
66
+ if (argv.includes("--verify")) {
67
+ process.exit(license.hasValidLicense() ? 0 : 1);
68
+ }
69
+
70
+ const cmd = argv[0];
71
+ const rest = argv.slice(1);
72
+
73
+ // Commands that DO require a license:
74
+ const gated = ["scaffold", "beats", "capture", "icons", "render", "lint", "update", "direction"];
75
+ if (gated.includes(cmd) && !license.hasValidLicense()) {
76
+ fail("ReelStack license missing or expired. Run `npx @devinilabs/reelstack init` to activate.");
77
+ process.exit(1);
78
+ }
79
+
80
+ switch (cmd) {
81
+ case "init": return require("./init").run(rest);
82
+ case "direction": return require("./direction").run(rest);
83
+ case "scaffold": return require("./scaffold").run(rest);
84
+ case "beats": return require("./beats").run(rest);
85
+ case "capture": return require("./capture").run(rest);
86
+ case "icons": return require("./icons").run(rest);
87
+ case "render": return require("./render").run(rest);
88
+ case "lint": return require("./lint").run(rest);
89
+ case "preview": return require("./preview").run(rest);
90
+ case "update": return require("./update").run(rest);
91
+ default:
92
+ fail(`Unknown command "${cmd}". Run \`reelstack --help\`.`);
93
+ process.exit(2);
94
+ }
95
+ }
96
+
97
+ main().catch((err) => {
98
+ fail(err.message || String(err));
99
+ if (process.env.REELSTACK_DEBUG) console.error(err);
100
+ process.exit(1);
101
+ });
package/cli/init.js ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * `reelstack init` — first-time setup. The buyer's first contact.
3
+ *
4
+ * 1. ASCII banner + welcome.
5
+ * 2. License prompt + devini.io validate (via license.js).
6
+ * 3. Dependency check (Node 20+, ffmpeg, whisper-cpp).
7
+ * 4. Scaffold ~/.reelstack/{families,templates,utils,docs}.
8
+ * 5. Install skill at ~/.claude/skills/reelstack/.
9
+ * 6. Install 13 command stubs at ~/.claude/commands/.
10
+ * 7. If buyer is inside a Remotion project, add @devinilabs/reelstack as a dep
11
+ * so scaffolded reels resolve their imports.
12
+ * 8. Run a smoke `scaffold --family=glass --preset=graphify --name=Demo`.
13
+ * 9. Print next-steps.
14
+ */
15
+ const fs = require("node:fs");
16
+ const path = require("node:path");
17
+ const {
18
+ banner, info, success, fail, warn, c, askYesNo, ensureDir, which,
19
+ REELSTACK_HOME, CLAUDE_SKILLS_HOME, CLAUDE_COMMANDS_HOME,
20
+ saveState, getPlatformInstaller,
21
+ } = require("./utils");
22
+ const license = require("./license");
23
+ const bootstrap = require("./bootstrap");
24
+ const smoke = require("./smoke");
25
+
26
+ const REELSTACK_PKG = path.dirname(__dirname);
27
+
28
+ function parseFlags(argv) {
29
+ const flags = {
30
+ skipSmoke: false,
31
+ noBootstrap: false,
32
+ projectName: "reelstack-project",
33
+ };
34
+ for (const arg of argv) {
35
+ if (arg === "--skip-smoke") flags.skipSmoke = true;
36
+ else if (arg === "--no-bootstrap") flags.noBootstrap = true;
37
+ else if (arg.startsWith("--name=")) {
38
+ const value = arg.slice(7);
39
+ if (value) {
40
+ const safe = path.basename(value);
41
+ if (safe && safe !== "." && safe !== "..") flags.projectName = safe;
42
+ }
43
+ }
44
+ }
45
+ return flags;
46
+ }
47
+
48
+ function copyTree(src, dst) {
49
+ if (!fs.existsSync(src)) return;
50
+ ensureDir(dst);
51
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
52
+ const s = path.join(src, entry.name);
53
+ const d = path.join(dst, entry.name);
54
+ if (entry.isDirectory()) copyTree(s, d);
55
+ else fs.copyFileSync(s, d);
56
+ }
57
+ }
58
+
59
+ function checkNode() {
60
+ const major = Number(process.versions.node.split(".")[0]);
61
+ if (major < 20) {
62
+ fail(`Node ${process.versions.node} detected. ReelStack needs Node 20+.`);
63
+ console.log("");
64
+ console.log(c.gray(" Install Node 20+ via:"));
65
+ console.log(` ${c.cyan("nvm install 20 && nvm use 20")} # nvm`);
66
+ console.log(` ${c.cyan("fnm install 20 && fnm use 20")} # fnm`);
67
+ console.log(` ${c.cyan("https://nodejs.org/")} # installer`);
68
+ console.log("");
69
+ process.exit(1);
70
+ }
71
+ success(`Node ${process.versions.node}`);
72
+ return true;
73
+ }
74
+
75
+ async function ensureFfmpeg() {
76
+ if (which("ffmpeg")) {
77
+ success("ffmpeg found");
78
+ saveState({ ffmpeg: "ok" });
79
+ return true;
80
+ }
81
+
82
+ fail("ffmpeg not found.");
83
+ const installer = getPlatformInstaller(process.platform, "ffmpeg");
84
+
85
+ if (installer.kind === "auto") {
86
+ const yes = await askYesNo(`Run \`${installer.command}\`?`);
87
+ if (!yes) {
88
+ console.log("");
89
+ fail("ffmpeg is required for /reelstack-render and /reelstack-beats.");
90
+ console.log(c.gray(` Install later with: ${installer.command}`));
91
+ console.log(c.gray(" Then re-run: reelstack init"));
92
+ process.exit(1);
93
+ }
94
+ const installResult = installer.install();
95
+ if (installResult.status !== 0) {
96
+ fail(`ffmpeg install exited non-zero (status ${installResult.status}). Check the output above.`);
97
+ process.exit(1);
98
+ }
99
+ if (!which("ffmpeg")) {
100
+ fail("ffmpeg install completed without errors but the binary still isn't on PATH.");
101
+ process.exit(1);
102
+ }
103
+ success("ffmpeg installed");
104
+ saveState({ ffmpeg: "ok" });
105
+ return true;
106
+ }
107
+
108
+ // kind === "guide"
109
+ console.log("");
110
+ console.log(c.gray(" Install ffmpeg via:"));
111
+ console.log(c.gray(` ${installer.command}`));
112
+ console.log(c.gray(" Then re-run: reelstack init"));
113
+ console.log("");
114
+ process.exit(1);
115
+ }
116
+
117
+ async function ensureWhisper() {
118
+ if (which("whisper-cli") || which("whisper-cpp")) {
119
+ success("whisper-cli found");
120
+ saveState({ whisperCli: "ok" });
121
+ return true;
122
+ }
123
+
124
+ warn("whisper-cli not found (only required for /reelstack-beats).");
125
+ const installer = getPlatformInstaller(process.platform, "whisper-cpp");
126
+
127
+ if (installer.kind === "auto") {
128
+ const yes = await askYesNo(`Run \`${installer.command}\`?`);
129
+ if (yes) {
130
+ const installResult = installer.install();
131
+ if (installResult.status !== 0) {
132
+ warn(`whisper install exited non-zero (status ${installResult.status}). /reelstack-beats will be unavailable.`);
133
+ } else if (which("whisper-cli") || which("whisper-cpp")) {
134
+ success("whisper-cli installed");
135
+ saveState({ whisperCli: "ok" });
136
+ return true;
137
+ } else {
138
+ warn("whisper install completed without errors but the binary still isn't on PATH. /reelstack-beats will be unavailable.");
139
+ }
140
+ } else {
141
+ warn("Skipped. /reelstack-beats will be unavailable until you install whisper-cpp.");
142
+ }
143
+ } else {
144
+ console.log(c.gray(` Install later with: ${installer.command}`));
145
+ console.log(c.gray(" /reelstack-beats will be unavailable until then."));
146
+ }
147
+
148
+ saveState({ whisperCli: "missing" });
149
+ return false;
150
+ }
151
+
152
+ async function run(argv) {
153
+ const flags = parseFlags(argv || []);
154
+ banner();
155
+ info("Welcome. Let's get ReelStack set up.");
156
+ console.log("");
157
+
158
+ // 1. License
159
+ const licensed = await license.verifyOrPrompt();
160
+ if (!licensed) {
161
+ fail("License required. Visit https://devini.io/reelstack to purchase.");
162
+ process.exit(1);
163
+ }
164
+
165
+ // 2. Tier 1 — Node hard gate (exits on fail)
166
+ console.log("");
167
+ info("Checking dependencies…");
168
+ checkNode();
169
+
170
+ // 3. Tier 2 — System binaries (exits on ffmpeg miss + decline)
171
+ await ensureFfmpeg();
172
+ await ensureWhisper();
173
+
174
+ // 4. Install ~/.reelstack runtime (preserved from original init)
175
+ console.log("");
176
+ info(`Installing ReelStack runtime at ${REELSTACK_HOME}…`);
177
+ ensureDir(REELSTACK_HOME);
178
+ for (const sub of ["families", "templates", "utils", "docs", "cli", "reference"]) {
179
+ copyTree(path.join(REELSTACK_PKG, sub), path.join(REELSTACK_HOME, sub));
180
+ }
181
+ fs.copyFileSync(
182
+ path.join(REELSTACK_PKG, "package.json"),
183
+ path.join(REELSTACK_HOME, "package.json"),
184
+ );
185
+ success("Runtime installed.");
186
+
187
+ // 5. Install Claude skill + slash commands (preserved from original init)
188
+ info(`Installing Claude Code skill at ${path.join(CLAUDE_SKILLS_HOME, "reelstack")}…`);
189
+ ensureDir(CLAUDE_SKILLS_HOME);
190
+ copyTree(path.join(REELSTACK_PKG, "skill"), path.join(CLAUDE_SKILLS_HOME, "reelstack"));
191
+ success("Skill installed.");
192
+
193
+ info(`Installing slash commands at ${CLAUDE_COMMANDS_HOME}…`);
194
+ ensureDir(CLAUDE_COMMANDS_HOME);
195
+ const cmdsSrc = path.join(REELSTACK_PKG, "skill", "commands");
196
+ const cmdFiles = fs.readdirSync(cmdsSrc);
197
+ for (const file of cmdFiles) {
198
+ fs.copyFileSync(path.join(cmdsSrc, file), path.join(CLAUDE_COMMANDS_HOME, file));
199
+ }
200
+ success(`${cmdFiles.length} slash commands installed.`);
201
+
202
+ // 6. Tier 3 — Remotion project bootstrap
203
+ console.log("");
204
+ const tier3 = await bootstrap.run({
205
+ noBootstrap: flags.noBootstrap,
206
+ projectName: flags.projectName,
207
+ });
208
+
209
+ // 7. Tier 4 — Smoke test (only if we have a project to test against)
210
+ if (tier3.skipped) {
211
+ saveState({ smokeTest: "skipped-no-project" });
212
+ } else if (flags.skipSmoke) {
213
+ saveState({ smokeTest: "skipped" });
214
+ warn("Smoke test skipped (--skip-smoke).");
215
+ } else {
216
+ console.log("");
217
+ info("Running smoke test…");
218
+ const result = await smoke.runAll({ projectDir: tier3.projectDir });
219
+ if (!result.passed) {
220
+ saveState({ smokeTest: "failed", smokeFailedAt: result.failedAt });
221
+ fail(`ReelStack is NOT ready — smoke test failed at ${result.failedAt}.`);
222
+ process.exit(1);
223
+ }
224
+ saveState({ smokeTest: "passed" });
225
+ }
226
+
227
+ saveState({ lastInitAt: new Date().toISOString() });
228
+
229
+ // 8. Success banner
230
+ console.log("");
231
+ if (tier3.skipped) {
232
+ success("ReelStack CLI installed. Bootstrap a Remotion project to unlock /reelstack-* commands.");
233
+ } else {
234
+ success("ReelStack is ready.");
235
+ }
236
+ console.log("");
237
+ console.log(c.gray(" Next:"));
238
+ if (tier3.skipped) {
239
+ console.log(` ${c.gray("Bootstrap a Remotion project, then re-run `reelstack init`.")}`);
240
+ console.log(` ${c.cyan("npx create-video@latest --yes --blank my-reels")}`);
241
+ console.log(` ${c.cyan("cd my-reels && npx @devinilabs/reelstack init")}`);
242
+ } else {
243
+ if (tier3.isNew) {
244
+ console.log(` ${c.cyan("cd " + flags.projectName)}`);
245
+ }
246
+ console.log(` ${c.cyan("npm run dev")} ${c.gray("# open Remotion Studio")}`);
247
+ console.log(` ${c.cyan("/reelstack-glass")} ${c.gray("# scaffold a Glass Iridescent reel")}`);
248
+ console.log(` ${c.cyan("/reelstack-paper")} ${c.gray("# … or any of the other 4 family commands")}`);
249
+ }
250
+ console.log("");
251
+ }
252
+
253
+ module.exports = { run, parseFlags };
package/cli/license.js ADDED
@@ -0,0 +1,168 @@
1
+ /**
2
+ * ReelStack license verification.
3
+ *
4
+ * Buyer pays on devini.io/reelstack → license key emailed →
5
+ * `reelstack init` prompts for the key → POSTs to devini.io validate API
6
+ * with { license_key, machine_id } → caches to ~/.reelstack/.license
7
+ * (mode 0600) on success.
8
+ *
9
+ * License is only checked at install / `init` / `update` / explicit
10
+ * `--verify` invocations, but `hasValidLicense()` enforces a 7-day TTL on
11
+ * the cached `validatedAt` so revocations propagate even when the CLI runs
12
+ * offline most of the time.
13
+ *
14
+ * The validation API enforces a 3-machine cap (rolling 30-day window) by
15
+ * keying on the random UUID we persist at `~/.reelstack/.machine-id`.
16
+ */
17
+ const fs = require("node:fs");
18
+ const path = require("node:path");
19
+ const { randomUUID } = require("node:crypto");
20
+ const {
21
+ REELSTACK_HOME,
22
+ LICENSE_CACHE,
23
+ ensureDir,
24
+ writeFileSafe,
25
+ fail,
26
+ info,
27
+ success,
28
+ ask,
29
+ c,
30
+ } = require("./utils");
31
+
32
+ const VALIDATE_URL =
33
+ process.env.REELSTACK_VALIDATE_URL ||
34
+ "https://devini.io/api/license/validate";
35
+
36
+ const MACHINE_ID_FILE = path.join(REELSTACK_HOME, ".machine-id");
37
+
38
+ // DEV_OVERRIDE intentionally stripped from published build.
39
+ const DEV_OVERRIDE = () => false;
40
+
41
+ const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
42
+
43
+ function readOrCreateMachineId() {
44
+ try {
45
+ const raw = fs.readFileSync(MACHINE_ID_FILE, "utf8").trim();
46
+ if (/^[0-9a-f-]{36}$/i.test(raw)) return raw;
47
+ } catch {}
48
+ const id = randomUUID();
49
+ ensureDir(path.dirname(MACHINE_ID_FILE));
50
+ fs.writeFileSync(MACHINE_ID_FILE, id, { mode: 0o644 });
51
+ return id;
52
+ }
53
+
54
+ async function validateRemote(key) {
55
+ if (DEV_OVERRIDE()) {
56
+ return { valid: true, dev: true };
57
+ }
58
+ try {
59
+ const machineId = readOrCreateMachineId();
60
+ const body = new URLSearchParams({
61
+ license_key: key,
62
+ machine_id: machineId,
63
+ });
64
+ const res = await fetch(VALIDATE_URL, {
65
+ method: "POST",
66
+ headers: { Accept: "application/json" },
67
+ body,
68
+ });
69
+ const data = await res.json().catch(() => ({}));
70
+ if (!res.ok) {
71
+ return {
72
+ valid: false,
73
+ status: res.status,
74
+ revoked: !!(data && data.revoked),
75
+ machine_limit_exceeded: !!(data && data.machine_limit_exceeded),
76
+ reason: (data && (data.reason || data.error)) || undefined,
77
+ };
78
+ }
79
+ return { valid: !!data.valid, raw: data };
80
+ } catch (err) {
81
+ return { valid: false, error: err.message };
82
+ }
83
+ }
84
+
85
+ function readCachedLicense() {
86
+ if (!fs.existsSync(LICENSE_CACHE)) return null;
87
+ try {
88
+ return JSON.parse(fs.readFileSync(LICENSE_CACHE, "utf8"));
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function cacheLicense(key, raw) {
95
+ ensureDir(path.dirname(LICENSE_CACHE));
96
+ writeFileSafe(
97
+ LICENSE_CACHE,
98
+ JSON.stringify({ key, validatedAt: Date.now(), raw }, null, 2),
99
+ );
100
+ try { fs.chmodSync(LICENSE_CACHE, 0o600); } catch {}
101
+ }
102
+
103
+ /**
104
+ * Returns true if a valid cached license exists AND was validated within
105
+ * the last 7 days. Does NOT re-call the API — that happens only on
106
+ * `init` / `update`. Use `verifyOrPrompt()` if you need a guaranteed
107
+ * fresh check.
108
+ */
109
+ function hasValidLicense() {
110
+ if (DEV_OVERRIDE()) return true;
111
+ const cached = readCachedLicense();
112
+ if (!cached || !cached.key) return false;
113
+
114
+ const ageMs = Date.now() - (cached.validatedAt ?? 0);
115
+ if (ageMs > SEVEN_DAYS_MS) return false; // force re-validation
116
+
117
+ return true;
118
+ }
119
+
120
+ async function verifyOrPrompt() {
121
+ if (hasValidLicense()) return true;
122
+ console.log("");
123
+ info("ReelStack needs a license to continue.");
124
+ console.log(` ${c.gray("Don't have one yet? Buy at")} ${c.cyan("https://devini.io/reelstack")}`);
125
+ console.log("");
126
+ const key = await ask("Paste your license key:");
127
+ if (!key) {
128
+ fail("No license key provided.");
129
+ return false;
130
+ }
131
+ const result = await validateRemote(key);
132
+ if (!result.valid) {
133
+ if (result.revoked) {
134
+ fail(`This license has been revoked${result.reason ? `: ${result.reason}` : ""}. Email abhishek@devini.io if this is a mistake.`);
135
+ return false;
136
+ }
137
+ if (result.machine_limit_exceeded) {
138
+ fail(`This license is already active on its 3 machines. Email abhishek@devini.io to release a slot for this machine.`);
139
+ return false;
140
+ }
141
+ fail(`License key not recognized${result.error ? `: ${result.error}` : ""}.`);
142
+ return false;
143
+ }
144
+ cacheLicense(key, result.raw);
145
+ success("License validated and cached at ~/.reelstack/.license.");
146
+ return true;
147
+ }
148
+
149
+ async function reverify() {
150
+ const cached = readCachedLicense();
151
+ if (!cached || !cached.key) return false;
152
+ const result = await validateRemote(cached.key);
153
+ if (!result.valid) return false;
154
+ cacheLicense(cached.key, result.raw);
155
+ return true;
156
+ }
157
+
158
+ module.exports = {
159
+ hasValidLicense,
160
+ verifyOrPrompt,
161
+ reverify,
162
+ readCachedLicense,
163
+ cacheLicense,
164
+ validateRemote,
165
+ readOrCreateMachineId,
166
+ VALIDATE_URL,
167
+ MACHINE_ID_FILE,
168
+ };