@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
@@ -0,0 +1,1166 @@
1
+ # ReelStack Init Readiness Gate — Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Replace ReelStack's warn-and-continue init with a 4-tier readiness gate so that after `init` exits 0, every `/reelstack-*` command works.
6
+
7
+ **Architecture:** Restructure [cli/init.js](../../../cli/init.js) into ordered tiers (Node version hard gate → system binaries → Remotion project bootstrap → smoke test). Add two new modules: `cli/bootstrap.js` (Tier 3) and `cli/smoke.js` (Tier 4). Extend [cli/utils.js](../../../cli/utils.js) with platform-aware installer helpers and a small JSON state store at `~/.reelstack/state.json`. Read the spec at [`docs/superpowers/specs/2026-05-09-reelstack-init-readiness-gate-design.md`](../specs/2026-05-09-reelstack-init-readiness-gate-design.md) for full design rationale.
8
+
9
+ **Tech Stack:** Node 20+, CommonJS, `node:test` runner, `node:child_process.spawnSync`. No new dependencies.
10
+
11
+ ---
12
+
13
+ ## File map
14
+
15
+ | File | Responsibility | Action |
16
+ |---|---|---|
17
+ | `cli/utils.js` | Add `getPlatformInstaller()`, `loadState()`, `saveState()`, `STATE_FILE` constant | Modify |
18
+ | `cli/bootstrap.js` | Tier 3: detect cwd state, run `npx create-video@latest`, install `@devinilabs/reelstack` | Create |
19
+ | `cli/smoke.js` | Tier 4: scaffold Demo, validate via `npx remotion compositions`, validate `ffmpeg -version` | Create |
20
+ | `cli/init.js` | Compose tiers, parse flags, write final state | Modify (substantial refactor) |
21
+ | `cli/beats.js` | Read state.json; fail-fast if `whisperCli === "missing-declined"` | Modify (small) |
22
+ | `test/utils-state.test.js` | Tests for `loadState` / `saveState` / `getPlatformInstaller` | Create |
23
+ | `test/bootstrap.test.js` | Tests for cwd-state detection (no-pkg / non-remotion / remotion) | Create |
24
+ | `test/smoke.test.js` | Tests for smoke-test step orchestration with mocked spawnSync | Create |
25
+ | `test/init-flow.test.js` | Integration-ish test for init flag handling (`--skip-smoke`, `--no-bootstrap`) | Create |
26
+
27
+ ---
28
+
29
+ ## Task 1: Add state store + platform installer helper to utils.js
30
+
31
+ **Files:**
32
+ - Modify: `cli/utils.js`
33
+ - Test: `test/utils-state.test.js`
34
+
35
+ The state store records what init detected/did so subcommands can fail-fast with helpful messages. The platform helper returns a `{ kind, command, install }` triple per OS so Tier 2 has one place to ask "how do I install $bin on this machine?".
36
+
37
+ - [ ] **Step 1: Write failing tests**
38
+
39
+ Create `test/utils-state.test.js`:
40
+
41
+ ```javascript
42
+ "use strict";
43
+
44
+ const { test, beforeEach } = require("node:test");
45
+ const assert = require("node:assert/strict");
46
+ const fs = require("node:fs");
47
+ const path = require("node:path");
48
+ const os = require("node:os");
49
+
50
+ const TMP_HOME = path.join(
51
+ os.tmpdir(),
52
+ `reelstack-utils-test-${process.pid}-${Date.now()}`,
53
+ );
54
+ process.env.HOME = TMP_HOME;
55
+ process.env.USERPROFILE = TMP_HOME;
56
+
57
+ const utils = require("../cli/utils");
58
+
59
+ beforeEach(() => {
60
+ fs.rmSync(TMP_HOME, { recursive: true, force: true });
61
+ });
62
+
63
+ test("STATE_FILE resolves under REELSTACK_HOME", () => {
64
+ assert.equal(
65
+ utils.STATE_FILE,
66
+ path.join(TMP_HOME, ".reelstack", "state.json"),
67
+ );
68
+ });
69
+
70
+ test("loadState returns {} when state file does not exist", () => {
71
+ assert.deepEqual(utils.loadState(), {});
72
+ });
73
+
74
+ test("saveState writes JSON, loadState round-trips", () => {
75
+ utils.saveState({ ffmpeg: "ok", whisperCli: "missing-declined" });
76
+ assert.deepEqual(utils.loadState(), {
77
+ ffmpeg: "ok",
78
+ whisperCli: "missing-declined",
79
+ });
80
+ });
81
+
82
+ test("saveState merges with existing state, doesn't replace", () => {
83
+ utils.saveState({ ffmpeg: "ok" });
84
+ utils.saveState({ whisperCli: "ok" });
85
+ assert.deepEqual(utils.loadState(), {
86
+ ffmpeg: "ok",
87
+ whisperCli: "ok",
88
+ });
89
+ });
90
+
91
+ test("getPlatformInstaller darwin returns brew when brew exists", () => {
92
+ const result = utils.getPlatformInstaller("darwin", "ffmpeg", {
93
+ whichFn: (b) => (b === "brew" ? "/opt/homebrew/bin/brew" : null),
94
+ });
95
+ assert.equal(result.kind, "auto");
96
+ assert.equal(result.command, "brew install ffmpeg");
97
+ assert.equal(typeof result.install, "function");
98
+ });
99
+
100
+ test("getPlatformInstaller darwin returns guidance when no brew", () => {
101
+ const result = utils.getPlatformInstaller("darwin", "ffmpeg", {
102
+ whichFn: () => null,
103
+ });
104
+ assert.equal(result.kind, "guide");
105
+ assert.match(result.command, /brew\.sh/);
106
+ });
107
+
108
+ test("getPlatformInstaller linux returns guide with apt+dnf+pacman", () => {
109
+ const result = utils.getPlatformInstaller("linux", "ffmpeg", {
110
+ whichFn: () => null,
111
+ });
112
+ assert.equal(result.kind, "guide");
113
+ assert.match(result.command, /apt-get install ffmpeg/);
114
+ assert.match(result.command, /dnf install ffmpeg/);
115
+ assert.match(result.command, /pacman -S ffmpeg/);
116
+ });
117
+
118
+ test("getPlatformInstaller win32 returns guide with choco+scoop", () => {
119
+ const result = utils.getPlatformInstaller("win32", "ffmpeg", {
120
+ whichFn: () => null,
121
+ });
122
+ assert.equal(result.kind, "guide");
123
+ assert.match(result.command, /choco install ffmpeg/);
124
+ assert.match(result.command, /scoop install ffmpeg/);
125
+ });
126
+ ```
127
+
128
+ - [ ] **Step 2: Run tests to verify they fail**
129
+
130
+ ```bash
131
+ cd /Users/abhishekraj/reelstack && node --test test/utils-state.test.js
132
+ ```
133
+
134
+ Expected: FAIL with "STATE_FILE is undefined" / "loadState is not a function" etc.
135
+
136
+ - [ ] **Step 3: Implement in cli/utils.js**
137
+
138
+ Add to `cli/utils.js` (after the `which()` function, before `module.exports`):
139
+
140
+ ```javascript
141
+ const STATE_FILE = path.join(REELSTACK_HOME, "state.json");
142
+
143
+ function loadState() {
144
+ try {
145
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"));
146
+ } catch {
147
+ return {};
148
+ }
149
+ }
150
+
151
+ function saveState(patch) {
152
+ const current = loadState();
153
+ const next = { ...current, ...patch };
154
+ ensureDir(path.dirname(STATE_FILE));
155
+ fs.writeFileSync(STATE_FILE, JSON.stringify(next, null, 2), "utf8");
156
+ }
157
+
158
+ function getPlatformInstaller(platform, bin, opts = {}) {
159
+ const whichFn = opts.whichFn || which;
160
+ const { spawnSync } = require("node:child_process");
161
+
162
+ if (platform === "darwin") {
163
+ if (whichFn("brew")) {
164
+ return {
165
+ kind: "auto",
166
+ command: `brew install ${bin}`,
167
+ install() {
168
+ return spawnSync("brew", ["install", bin], { stdio: "inherit" });
169
+ },
170
+ };
171
+ }
172
+ return {
173
+ kind: "guide",
174
+ command: `Install Homebrew (https://brew.sh), then: brew install ${bin}`,
175
+ install: null,
176
+ };
177
+ }
178
+
179
+ if (platform === "linux") {
180
+ return {
181
+ kind: "guide",
182
+ command: [
183
+ `apt-get install ${bin} # Debian/Ubuntu`,
184
+ `dnf install ${bin} # Fedora`,
185
+ `pacman -S ${bin} # Arch`,
186
+ ].join("\n "),
187
+ install: null,
188
+ };
189
+ }
190
+
191
+ if (platform === "win32") {
192
+ return {
193
+ kind: "guide",
194
+ command: [
195
+ `choco install ${bin} # Chocolatey`,
196
+ `scoop install ${bin} # Scoop`,
197
+ ].join("\n "),
198
+ install: null,
199
+ };
200
+ }
201
+
202
+ return {
203
+ kind: "guide",
204
+ command: `Install ${bin} via your platform's package manager`,
205
+ install: null,
206
+ };
207
+ }
208
+ ```
209
+
210
+ Update `module.exports` to add `STATE_FILE`, `loadState`, `saveState`, `getPlatformInstaller`.
211
+
212
+ - [ ] **Step 4: Run tests, verify pass**
213
+
214
+ ```bash
215
+ cd /Users/abhishekraj/reelstack && node --test test/utils-state.test.js
216
+ ```
217
+
218
+ Expected: all 8 tests PASS.
219
+
220
+ - [ ] **Step 5: Commit**
221
+
222
+ ```bash
223
+ cd /Users/abhishekraj/reelstack && git add cli/utils.js test/utils-state.test.js
224
+ git commit -m "ReelStack init — add state store + platform installer helper
225
+
226
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
227
+ ```
228
+
229
+ ---
230
+
231
+ ## Task 2: Tier 1 — Node version hard gate
232
+
233
+ **Files:**
234
+ - Modify: `cli/init.js` (the `checkNode()` function)
235
+
236
+ The current `checkNode()` returns false on old Node but the caller doesn't act on it. Make it exit-on-fail with a clear remediation message. Keep the function name + signature stable so other callers (if any) aren't surprised.
237
+
238
+ - [ ] **Step 1: Update `checkNode()` in cli/init.js**
239
+
240
+ Replace [cli/init.js:40-48](../../../cli/init.js#L40):
241
+
242
+ ```javascript
243
+ function checkNode() {
244
+ const major = Number(process.versions.node.split(".")[0]);
245
+ if (major < 20) {
246
+ fail(`Node ${process.versions.node} detected. ReelStack needs Node 20+.`);
247
+ console.log("");
248
+ console.log(c.gray(" Install Node 20+ via:"));
249
+ console.log(` ${c.cyan("nvm install 20 && nvm use 20")} # nvm`);
250
+ console.log(` ${c.cyan("fnm install 20 && fnm use 20")} # fnm`);
251
+ console.log(` ${c.cyan("https://nodejs.org/")} # installer`);
252
+ console.log("");
253
+ process.exit(1);
254
+ }
255
+ success(`Node ${process.versions.node}`);
256
+ return true;
257
+ }
258
+ ```
259
+
260
+ No tests required (single-branch logic, tested via integration test in Task 6).
261
+
262
+ - [ ] **Step 2: Manually verify with current Node**
263
+
264
+ ```bash
265
+ cd /Users/abhishekraj/reelstack && node cli/index.js --help
266
+ ```
267
+
268
+ Expected: command runs without `process.exit(1)` firing (you're on Node 20+).
269
+
270
+ - [ ] **Step 3: Commit**
271
+
272
+ ```bash
273
+ cd /Users/abhishekraj/reelstack && git add cli/init.js
274
+ git commit -m "ReelStack init — Tier 1: Node hard gate with install guidance
275
+
276
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Task 3: Tier 2 — System binaries with platform-aware install
282
+
283
+ **Files:**
284
+ - Modify: `cli/init.js` (replace `checkFfmpeg()` and `checkWhisper()`)
285
+
286
+ Replace warn-only checks with the consent-driven install flow from the spec. ffmpeg is **required** (exit on miss + decline). whisper-cli is **soft** (declining records `whisperCli: "missing-declined"` in state.json and continues).
287
+
288
+ - [ ] **Step 1: Replace `checkFfmpeg()` and `checkWhisper()` in cli/init.js**
289
+
290
+ Replace [cli/init.js:50-66](../../../cli/init.js#L50):
291
+
292
+ ```javascript
293
+ async function ensureFfmpeg() {
294
+ if (which("ffmpeg")) {
295
+ success("ffmpeg found");
296
+ saveState({ ffmpeg: "ok" });
297
+ return true;
298
+ }
299
+
300
+ fail("ffmpeg not found.");
301
+ const installer = getPlatformInstaller(process.platform, "ffmpeg");
302
+
303
+ if (installer.kind === "auto") {
304
+ const yes = await askYesNo(`Run \`${installer.command}\`?`);
305
+ if (!yes) {
306
+ console.log("");
307
+ fail("ffmpeg is required for /reelstack-render and /reelstack-beats.");
308
+ console.log(c.gray(` Install later with: ${installer.command}`));
309
+ console.log(c.gray(" Then re-run: reelstack init"));
310
+ process.exit(1);
311
+ }
312
+ installer.install();
313
+ if (!which("ffmpeg")) {
314
+ fail("ffmpeg install did not complete. Check brew output above.");
315
+ process.exit(1);
316
+ }
317
+ success("ffmpeg installed");
318
+ saveState({ ffmpeg: "ok" });
319
+ return true;
320
+ }
321
+
322
+ // kind === "guide"
323
+ console.log("");
324
+ console.log(c.gray(" Install ffmpeg via:"));
325
+ console.log(c.gray(` ${installer.command}`));
326
+ console.log(c.gray(" Then re-run: reelstack init"));
327
+ console.log("");
328
+ process.exit(1);
329
+ }
330
+
331
+ async function ensureWhisper() {
332
+ if (which("whisper-cli") || which("whisper-cpp")) {
333
+ success("whisper-cli found");
334
+ saveState({ whisperCli: "ok" });
335
+ return true;
336
+ }
337
+
338
+ warn("whisper-cli not found (only required for /reelstack-beats).");
339
+ const installer = getPlatformInstaller(process.platform, "whisper-cpp");
340
+
341
+ if (installer.kind === "auto") {
342
+ const yes = await askYesNo(`Run \`${installer.command}\`?`);
343
+ if (yes) {
344
+ installer.install();
345
+ if (which("whisper-cli") || which("whisper-cpp")) {
346
+ success("whisper-cli installed");
347
+ saveState({ whisperCli: "ok" });
348
+ return true;
349
+ }
350
+ warn("whisper install did not complete. /reelstack-beats will be unavailable.");
351
+ } else {
352
+ warn("Skipped. /reelstack-beats will be unavailable until you install whisper-cpp.");
353
+ }
354
+ } else {
355
+ console.log(c.gray(` Install later with: ${installer.command}`));
356
+ console.log(c.gray(" /reelstack-beats will be unavailable until then."));
357
+ }
358
+
359
+ saveState({ whisperCli: "missing-declined" });
360
+ return false;
361
+ }
362
+ ```
363
+
364
+ Add to the imports at the top of [cli/init.js:19-22](../../../cli/init.js#L19):
365
+
366
+ ```javascript
367
+ const {
368
+ banner, info, success, fail, warn, c, ask, askYesNo, ensureDir, which,
369
+ REELSTACK_HOME, CLAUDE_SKILLS_HOME, CLAUDE_COMMANDS_HOME,
370
+ saveState, getPlatformInstaller,
371
+ } = require("./utils");
372
+ ```
373
+
374
+ - [ ] **Step 2: Update the dep-check block in `run()`**
375
+
376
+ Replace [cli/init.js:80-85](../../../cli/init.js#L80):
377
+
378
+ ```javascript
379
+ // 2. Dependencies
380
+ console.log("");
381
+ info("Checking dependencies…");
382
+ checkNode();
383
+ await ensureFfmpeg();
384
+ await ensureWhisper();
385
+ ```
386
+
387
+ - [ ] **Step 3: Manually verify**
388
+
389
+ ```bash
390
+ cd /Users/abhishekraj/reelstack && node cli/index.js init
391
+ ```
392
+
393
+ Expected (on a machine with ffmpeg + whisper installed): both checks pass, init proceeds. State.json shows both as `"ok"`.
394
+
395
+ ```bash
396
+ cat ~/.reelstack/state.json
397
+ ```
398
+
399
+ Expected output:
400
+
401
+ ```json
402
+ {
403
+ "ffmpeg": "ok",
404
+ "whisperCli": "ok"
405
+ }
406
+ ```
407
+
408
+ - [ ] **Step 4: Commit**
409
+
410
+ ```bash
411
+ cd /Users/abhishekraj/reelstack && git add cli/init.js
412
+ git commit -m "ReelStack init — Tier 2: ffmpeg required + whisper soft, with platform-aware install
413
+
414
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
415
+ ```
416
+
417
+ ---
418
+
419
+ ## Task 4: Tier 3 — Remotion project bootstrap (cli/bootstrap.js)
420
+
421
+ **Files:**
422
+ - Create: `cli/bootstrap.js`
423
+ - Test: `test/bootstrap.test.js`
424
+
425
+ Encapsulates: detect cwd state (no-pkg / non-remotion / has-remotion), prompt buyer, run `npx create-video@latest --yes --blank <name>`, then `npm install @devinilabs/reelstack` inside the new project. Returns `{ projectDir, isNew }`.
426
+
427
+ - [ ] **Step 1: Write failing tests**
428
+
429
+ Create `test/bootstrap.test.js`:
430
+
431
+ ```javascript
432
+ "use strict";
433
+
434
+ const { test } = require("node:test");
435
+ const assert = require("node:assert/strict");
436
+ const fs = require("node:fs");
437
+ const path = require("node:path");
438
+ const os = require("node:os");
439
+
440
+ const bootstrap = require("../cli/bootstrap");
441
+
442
+ function makeTmpDir(label) {
443
+ const dir = path.join(os.tmpdir(), `reelstack-bootstrap-${label}-${process.pid}-${Date.now()}`);
444
+ fs.mkdirSync(dir, { recursive: true });
445
+ return dir;
446
+ }
447
+
448
+ test("detectCwdState returns 'no-package-json' for empty dir", () => {
449
+ const cwd = makeTmpDir("empty");
450
+ assert.equal(bootstrap.detectCwdState(cwd), "no-package-json");
451
+ });
452
+
453
+ test("detectCwdState returns 'has-remotion' when remotion in deps", () => {
454
+ const cwd = makeTmpDir("remotion-dep");
455
+ fs.writeFileSync(
456
+ path.join(cwd, "package.json"),
457
+ JSON.stringify({ dependencies: { remotion: "^4.0.0" } }),
458
+ );
459
+ assert.equal(bootstrap.detectCwdState(cwd), "has-remotion");
460
+ });
461
+
462
+ test("detectCwdState returns 'has-remotion' when remotion in devDependencies", () => {
463
+ const cwd = makeTmpDir("remotion-devdep");
464
+ fs.writeFileSync(
465
+ path.join(cwd, "package.json"),
466
+ JSON.stringify({ devDependencies: { remotion: "^4.0.0" } }),
467
+ );
468
+ assert.equal(bootstrap.detectCwdState(cwd), "has-remotion");
469
+ });
470
+
471
+ test("detectCwdState returns 'non-remotion-package' when package.json has no remotion", () => {
472
+ const cwd = makeTmpDir("non-remotion");
473
+ fs.writeFileSync(
474
+ path.join(cwd, "package.json"),
475
+ JSON.stringify({ dependencies: { lodash: "^4.0.0" } }),
476
+ );
477
+ assert.equal(bootstrap.detectCwdState(cwd), "non-remotion-package");
478
+ });
479
+
480
+ test("buildCreateVideoArgs returns --yes --blank <name>", () => {
481
+ assert.deepEqual(
482
+ bootstrap.buildCreateVideoArgs("my-reels"),
483
+ ["create-video@latest", "--yes", "--blank", "my-reels"],
484
+ );
485
+ });
486
+ ```
487
+
488
+ - [ ] **Step 2: Run tests to verify they fail**
489
+
490
+ ```bash
491
+ cd /Users/abhishekraj/reelstack && node --test test/bootstrap.test.js
492
+ ```
493
+
494
+ Expected: FAIL with "Cannot find module '../cli/bootstrap'".
495
+
496
+ - [ ] **Step 3: Implement cli/bootstrap.js**
497
+
498
+ Create `cli/bootstrap.js`:
499
+
500
+ ```javascript
501
+ /**
502
+ * Tier 3: detect Remotion project state, prompt for bootstrap, run create-video.
503
+ *
504
+ * Only the pure helpers (`detectCwdState`, `buildCreateVideoArgs`) are unit-tested.
505
+ * The full `run()` function shells out and is exercised manually + via init-flow test.
506
+ */
507
+ const fs = require("node:fs");
508
+ const path = require("node:path");
509
+ const { spawnSync } = require("node:child_process");
510
+ const { info, success, fail, warn, c, askYesNo, ask } = require("./utils");
511
+
512
+ const PKG_NAME = require("../package.json").name;
513
+
514
+ function detectCwdState(cwd) {
515
+ const pkgPath = path.join(cwd, "package.json");
516
+ if (!fs.existsSync(pkgPath)) return "no-package-json";
517
+
518
+ let pkg;
519
+ try {
520
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
521
+ } catch {
522
+ return "no-package-json";
523
+ }
524
+
525
+ const hasRemotion =
526
+ (pkg.dependencies && pkg.dependencies.remotion) ||
527
+ (pkg.devDependencies && pkg.devDependencies.remotion);
528
+
529
+ return hasRemotion ? "has-remotion" : "non-remotion-package";
530
+ }
531
+
532
+ function buildCreateVideoArgs(projectName) {
533
+ return ["create-video@latest", "--yes", "--blank", projectName];
534
+ }
535
+
536
+ function ensureReelstackDep(projectDir) {
537
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
538
+ const already =
539
+ (pkg.dependencies && pkg.dependencies[PKG_NAME]) ||
540
+ (pkg.devDependencies && pkg.devDependencies[PKG_NAME]);
541
+
542
+ if (already) {
543
+ success(`${PKG_NAME} already present.`);
544
+ return true;
545
+ }
546
+
547
+ info(`Installing ${PKG_NAME}…`);
548
+ const result = spawnSync("npm", ["install", PKG_NAME], {
549
+ stdio: "inherit",
550
+ cwd: projectDir,
551
+ });
552
+ if (result.status !== 0) {
553
+ fail(`npm install ${PKG_NAME} failed.`);
554
+ return false;
555
+ }
556
+ success(`${PKG_NAME} installed.`);
557
+ return true;
558
+ }
559
+
560
+ /**
561
+ * Run Tier 3. Returns { projectDir, isNew, skipped } or exits process on
562
+ * unrecoverable failure. `skipped: true` when buyer declines bootstrap or
563
+ * --no-bootstrap was passed.
564
+ */
565
+ async function run({ noBootstrap = false, projectName = "reelstack-project" } = {}) {
566
+ const cwd = process.cwd();
567
+ const state = detectCwdState(cwd);
568
+
569
+ if (state === "has-remotion") {
570
+ info("Remotion project detected in current directory.");
571
+ if (!ensureReelstackDep(cwd)) process.exit(1);
572
+ return { projectDir: cwd, isNew: false, skipped: false };
573
+ }
574
+
575
+ if (noBootstrap) {
576
+ warn("No Remotion project here; --no-bootstrap was passed.");
577
+ console.log(c.gray(" Re-run `reelstack init` from inside a Remotion project to wire up imports."));
578
+ return { projectDir: null, isNew: false, skipped: true };
579
+ }
580
+
581
+ if (state === "non-remotion-package") {
582
+ warn("This folder has a non-Remotion package.json.");
583
+ info("Bootstrapping Remotion in a subdirectory instead so we don't muddy your project.");
584
+ }
585
+
586
+ console.log("");
587
+ const yes = await askYesNo(`Scaffold a Remotion project at ./${projectName}/?`);
588
+ if (!yes) {
589
+ warn("Skipped Remotion bootstrap.");
590
+ console.log(c.gray(" Re-run `reelstack init` from inside a Remotion project to wire up imports."));
591
+ return { projectDir: null, isNew: false, skipped: true };
592
+ }
593
+
594
+ const targetDir = path.join(cwd, projectName);
595
+ if (fs.existsSync(targetDir)) {
596
+ fail(`./${projectName}/ already exists. Choose a different name with --name=<dir>, or delete it first.`);
597
+ process.exit(1);
598
+ }
599
+
600
+ info(`Running: npx ${buildCreateVideoArgs(projectName).join(" ")}`);
601
+ const result = spawnSync("npx", buildCreateVideoArgs(projectName), {
602
+ stdio: "inherit",
603
+ cwd,
604
+ });
605
+ if (result.status !== 0) {
606
+ fail("create-video did not exit cleanly.");
607
+ process.exit(1);
608
+ }
609
+ success("Remotion project scaffolded.");
610
+
611
+ if (!ensureReelstackDep(targetDir)) process.exit(1);
612
+
613
+ return { projectDir: targetDir, isNew: true, skipped: false };
614
+ }
615
+
616
+ module.exports = { run, detectCwdState, buildCreateVideoArgs, ensureReelstackDep };
617
+ ```
618
+
619
+ - [ ] **Step 4: Run tests, verify pass**
620
+
621
+ ```bash
622
+ cd /Users/abhishekraj/reelstack && node --test test/bootstrap.test.js
623
+ ```
624
+
625
+ Expected: all 5 tests PASS.
626
+
627
+ - [ ] **Step 5: Commit**
628
+
629
+ ```bash
630
+ cd /Users/abhishekraj/reelstack && git add cli/bootstrap.js test/bootstrap.test.js
631
+ git commit -m "ReelStack init — Tier 3: Remotion bootstrap module (cli/bootstrap.js)
632
+
633
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
634
+ ```
635
+
636
+ ---
637
+
638
+ ## Task 5: Tier 4 — Smoke test (cli/smoke.js)
639
+
640
+ **Files:**
641
+ - Create: `cli/smoke.js`
642
+ - Test: `test/smoke.test.js`
643
+
644
+ Three checks: scaffold a Demo, run `npx remotion compositions` to confirm it loads, run `ffmpeg -version` to confirm the binary executes. Each step gets its own `success`/`fail` line so the buyer can see exactly which check failed.
645
+
646
+ - [ ] **Step 1: Write failing tests**
647
+
648
+ Create `test/smoke.test.js`:
649
+
650
+ ```javascript
651
+ "use strict";
652
+
653
+ const { test } = require("node:test");
654
+ const assert = require("node:assert/strict");
655
+
656
+ const smoke = require("../cli/smoke");
657
+
658
+ test("STEPS lists three checks in order", () => {
659
+ assert.equal(smoke.STEPS.length, 3);
660
+ assert.equal(smoke.STEPS[0].name, "scaffold-demo");
661
+ assert.equal(smoke.STEPS[1].name, "remotion-compositions");
662
+ assert.equal(smoke.STEPS[2].name, "ffmpeg-version");
663
+ });
664
+
665
+ test("each STEP has a label and a run function", () => {
666
+ for (const step of smoke.STEPS) {
667
+ assert.equal(typeof step.label, "string");
668
+ assert.equal(typeof step.run, "function");
669
+ }
670
+ });
671
+
672
+ test("runAll returns { passed: true } when all steps succeed", async () => {
673
+ const fakeSpawn = () => ({ status: 0, stdout: "", stderr: "" });
674
+ const result = await smoke.runAll({
675
+ projectDir: "/tmp/fake",
676
+ spawnFn: fakeSpawn,
677
+ scaffoldFn: async () => {},
678
+ });
679
+ assert.equal(result.passed, true);
680
+ assert.equal(result.failedAt, null);
681
+ });
682
+
683
+ test("runAll returns { passed: false, failedAt } when a step throws", async () => {
684
+ const fakeSpawn = () => ({ status: 1, stdout: "", stderr: "boom" });
685
+ const result = await smoke.runAll({
686
+ projectDir: "/tmp/fake",
687
+ spawnFn: fakeSpawn,
688
+ scaffoldFn: async () => {},
689
+ });
690
+ assert.equal(result.passed, false);
691
+ assert.equal(result.failedAt, "remotion-compositions");
692
+ });
693
+ ```
694
+
695
+ - [ ] **Step 2: Run tests to verify they fail**
696
+
697
+ ```bash
698
+ cd /Users/abhishekraj/reelstack && node --test test/smoke.test.js
699
+ ```
700
+
701
+ Expected: FAIL with "Cannot find module '../cli/smoke'".
702
+
703
+ - [ ] **Step 3: Implement cli/smoke.js**
704
+
705
+ Create `cli/smoke.js`:
706
+
707
+ ```javascript
708
+ /**
709
+ * Tier 4: smoke test. Runs after Tiers 1-3 pass and proves the full stack
710
+ * actually loads. Three fast, non-destructive checks.
711
+ *
712
+ * Steps:
713
+ * 1. scaffold-demo — reelstack scaffold --family=glass --preset=graphify --name=Demo
714
+ * 2. remotion-compositions — npx remotion compositions (proves Demo registers)
715
+ * 3. ffmpeg-version — ffmpeg -version (proves binary executes)
716
+ */
717
+ const { spawnSync } = require("node:child_process");
718
+ const { info, success, fail, c } = require("./utils");
719
+ const scaffold = require("./scaffold");
720
+
721
+ const STEPS = [
722
+ {
723
+ name: "scaffold-demo",
724
+ label: "Scaffolding Demo.tsx",
725
+ async run({ projectDir, scaffoldFn }) {
726
+ const fn = scaffoldFn || scaffold.run;
727
+ await fn(["--family=glass", "--preset=graphify", "--name=Demo"], { cwd: projectDir });
728
+ return { ok: true };
729
+ },
730
+ },
731
+ {
732
+ name: "remotion-compositions",
733
+ label: "Loading Remotion compositions",
734
+ async run({ projectDir, spawnFn }) {
735
+ const fn = spawnFn || spawnSync;
736
+ const result = fn("npx", ["remotion", "compositions"], {
737
+ cwd: projectDir,
738
+ stdio: "pipe",
739
+ encoding: "utf8",
740
+ });
741
+ return result.status === 0
742
+ ? { ok: true }
743
+ : { ok: false, message: (result.stderr || result.stdout || "unknown error").toString().trim() };
744
+ },
745
+ },
746
+ {
747
+ name: "ffmpeg-version",
748
+ label: "Validating ffmpeg",
749
+ async run({ spawnFn }) {
750
+ const fn = spawnFn || spawnSync;
751
+ const result = fn("ffmpeg", ["-version"], { stdio: "pipe", encoding: "utf8" });
752
+ return result.status === 0
753
+ ? { ok: true }
754
+ : { ok: false, message: (result.stderr || result.stdout || "ffmpeg failed").toString().trim() };
755
+ },
756
+ },
757
+ ];
758
+
759
+ async function runAll(opts) {
760
+ for (const step of STEPS) {
761
+ info(step.label + "…");
762
+ let outcome;
763
+ try {
764
+ outcome = await step.run(opts);
765
+ } catch (err) {
766
+ outcome = { ok: false, message: err.message };
767
+ }
768
+ if (!outcome.ok) {
769
+ fail(`Smoke test failed at: ${step.name}`);
770
+ if (outcome.message) console.log(c.gray(" " + outcome.message));
771
+ return { passed: false, failedAt: step.name };
772
+ }
773
+ success(step.label);
774
+ }
775
+ return { passed: true, failedAt: null };
776
+ }
777
+
778
+ module.exports = { STEPS, runAll };
779
+ ```
780
+
781
+ - [ ] **Step 4: Run tests, verify pass**
782
+
783
+ ```bash
784
+ cd /Users/abhishekraj/reelstack && node --test test/smoke.test.js
785
+ ```
786
+
787
+ Expected: all 4 tests PASS.
788
+
789
+ - [ ] **Step 5: Commit**
790
+
791
+ ```bash
792
+ cd /Users/abhishekraj/reelstack && git add cli/smoke.js test/smoke.test.js
793
+ git commit -m "ReelStack init — Tier 4: smoke test module (cli/smoke.js)
794
+
795
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
796
+ ```
797
+
798
+ ---
799
+
800
+ ## Task 6: Wire init.js to compose all four tiers + parse flags
801
+
802
+ **Files:**
803
+ - Modify: `cli/init.js`
804
+ - Test: `test/init-flow.test.js`
805
+
806
+ Replace the existing `run()` body to compose Tier 1 → runtime/skill/commands install (preserved unchanged) → Tier 2 → Tier 3 → Tier 4. Parse `--name=<dir>`, `--skip-smoke`, `--no-bootstrap` from `argv`.
807
+
808
+ - [ ] **Step 1: Write failing tests**
809
+
810
+ Create `test/init-flow.test.js`:
811
+
812
+ ```javascript
813
+ "use strict";
814
+
815
+ const { test } = require("node:test");
816
+ const assert = require("node:assert/strict");
817
+
818
+ const init = require("../cli/init");
819
+
820
+ test("parseFlags defaults", () => {
821
+ const flags = init.parseFlags([]);
822
+ assert.equal(flags.skipSmoke, false);
823
+ assert.equal(flags.noBootstrap, false);
824
+ assert.equal(flags.projectName, "reelstack-project");
825
+ });
826
+
827
+ test("parseFlags --skip-smoke", () => {
828
+ const flags = init.parseFlags(["--skip-smoke"]);
829
+ assert.equal(flags.skipSmoke, true);
830
+ });
831
+
832
+ test("parseFlags --no-bootstrap", () => {
833
+ const flags = init.parseFlags(["--no-bootstrap"]);
834
+ assert.equal(flags.noBootstrap, true);
835
+ });
836
+
837
+ test("parseFlags --name=foo", () => {
838
+ const flags = init.parseFlags(["--name=foo"]);
839
+ assert.equal(flags.projectName, "foo");
840
+ });
841
+
842
+ test("parseFlags accepts combined flags", () => {
843
+ const flags = init.parseFlags(["--name=bar", "--skip-smoke", "--no-bootstrap"]);
844
+ assert.equal(flags.projectName, "bar");
845
+ assert.equal(flags.skipSmoke, true);
846
+ assert.equal(flags.noBootstrap, true);
847
+ });
848
+ ```
849
+
850
+ - [ ] **Step 2: Run tests to verify they fail**
851
+
852
+ ```bash
853
+ cd /Users/abhishekraj/reelstack && node --test test/init-flow.test.js
854
+ ```
855
+
856
+ Expected: FAIL with "init.parseFlags is not a function".
857
+
858
+ - [ ] **Step 3: Add `parseFlags()` and refactor `run()` in cli/init.js**
859
+
860
+ Add near the top of `cli/init.js`, after the constants:
861
+
862
+ ```javascript
863
+ function parseFlags(argv) {
864
+ const flags = {
865
+ skipSmoke: false,
866
+ noBootstrap: false,
867
+ projectName: "reelstack-project",
868
+ };
869
+ for (const arg of argv) {
870
+ if (arg === "--skip-smoke") flags.skipSmoke = true;
871
+ else if (arg === "--no-bootstrap") flags.noBootstrap = true;
872
+ else if (arg.startsWith("--name=")) flags.projectName = arg.slice(7);
873
+ }
874
+ return flags;
875
+ }
876
+ ```
877
+
878
+ Replace the entire `run()` function. Final version:
879
+
880
+ ```javascript
881
+ async function run(argv) {
882
+ const flags = parseFlags(argv || []);
883
+ banner();
884
+ info("Welcome. Let's get ReelStack set up.");
885
+ console.log("");
886
+
887
+ // 1. License
888
+ const licensed = await license.verifyOrPrompt();
889
+ if (!licensed) {
890
+ fail("License required. Visit https://devini.io/reelstack to purchase.");
891
+ process.exit(1);
892
+ }
893
+
894
+ // 2. Tier 1 — Node hard gate (exits on fail)
895
+ console.log("");
896
+ info("Checking dependencies…");
897
+ checkNode();
898
+
899
+ // 3. Tier 2 — System binaries (exits on ffmpeg miss + decline)
900
+ await ensureFfmpeg();
901
+ await ensureWhisper();
902
+
903
+ // 4. Install ~/.reelstack runtime (preserved from original init)
904
+ console.log("");
905
+ info(`Installing ReelStack runtime at ${REELSTACK_HOME}…`);
906
+ ensureDir(REELSTACK_HOME);
907
+ for (const sub of ["families", "templates", "utils", "docs", "cli", "reference"]) {
908
+ copyTree(path.join(REELSTACK_PKG, sub), path.join(REELSTACK_HOME, sub));
909
+ }
910
+ fs.copyFileSync(
911
+ path.join(REELSTACK_PKG, "package.json"),
912
+ path.join(REELSTACK_HOME, "package.json"),
913
+ );
914
+ success("Runtime installed.");
915
+
916
+ // 5. Install Claude skill + slash commands (preserved from original init)
917
+ info(`Installing Claude Code skill at ${path.join(CLAUDE_SKILLS_HOME, "reelstack")}…`);
918
+ ensureDir(CLAUDE_SKILLS_HOME);
919
+ copyTree(path.join(REELSTACK_PKG, "skill"), path.join(CLAUDE_SKILLS_HOME, "reelstack"));
920
+ success("Skill installed.");
921
+
922
+ info(`Installing slash commands at ${CLAUDE_COMMANDS_HOME}…`);
923
+ ensureDir(CLAUDE_COMMANDS_HOME);
924
+ const cmdsSrc = path.join(REELSTACK_PKG, "skill", "commands");
925
+ const cmdFiles = fs.readdirSync(cmdsSrc);
926
+ for (const file of cmdFiles) {
927
+ fs.copyFileSync(path.join(cmdsSrc, file), path.join(CLAUDE_COMMANDS_HOME, file));
928
+ }
929
+ success(`${cmdFiles.length} slash commands installed.`);
930
+
931
+ // 6. Tier 3 — Remotion project bootstrap
932
+ console.log("");
933
+ const tier3 = await bootstrap.run({
934
+ noBootstrap: flags.noBootstrap,
935
+ projectName: flags.projectName,
936
+ });
937
+
938
+ // 7. Tier 4 — Smoke test (only if we have a project to test against)
939
+ if (tier3.skipped) {
940
+ saveState({ smokeTest: "skipped-no-project" });
941
+ } else if (flags.skipSmoke) {
942
+ saveState({ smokeTest: "skipped" });
943
+ warn("Smoke test skipped (--skip-smoke).");
944
+ } else {
945
+ console.log("");
946
+ info("Running smoke test…");
947
+ const result = await smoke.runAll({ projectDir: tier3.projectDir });
948
+ if (!result.passed) {
949
+ saveState({ smokeTest: "failed", smokeFailedAt: result.failedAt });
950
+ fail(`ReelStack is NOT ready — smoke test failed at ${result.failedAt}.`);
951
+ process.exit(1);
952
+ }
953
+ saveState({ smokeTest: "passed" });
954
+ }
955
+
956
+ saveState({ lastInitAt: new Date().toISOString() });
957
+
958
+ // 8. Success banner
959
+ console.log("");
960
+ success("ReelStack is ready.");
961
+ console.log("");
962
+ console.log(c.gray(" Next:"));
963
+ if (tier3.isNew) {
964
+ console.log(` ${c.cyan("cd " + flags.projectName)}`);
965
+ }
966
+ console.log(` ${c.cyan("npm run dev")} ${c.gray("# open Remotion Studio")}`);
967
+ console.log(` ${c.cyan("/reelstack-glass")} ${c.gray("# scaffold a Glass Iridescent reel")}`);
968
+ console.log(` ${c.cyan("/reelstack-paper")} ${c.gray("# … or any of the other 4 family commands")}`);
969
+ console.log("");
970
+ }
971
+
972
+ module.exports = { run, parseFlags };
973
+ ```
974
+
975
+ Update imports at the top to add the new modules:
976
+
977
+ ```javascript
978
+ const license = require("./license");
979
+ const scaffold = require("./scaffold");
980
+ const bootstrap = require("./bootstrap");
981
+ const smoke = require("./smoke");
982
+ ```
983
+
984
+ Remove the old smoke-scaffold block (the old `// 7. Smoke scaffold` section at [cli/init.js:160-171](../../../cli/init.js#L160)) — Tier 4 replaces it.
985
+
986
+ - [ ] **Step 4: Run flag tests, verify pass**
987
+
988
+ ```bash
989
+ cd /Users/abhishekraj/reelstack && node --test test/init-flow.test.js
990
+ ```
991
+
992
+ Expected: all 5 flag-parsing tests PASS.
993
+
994
+ - [ ] **Step 5: Run full test suite to make sure nothing else broke**
995
+
996
+ ```bash
997
+ cd /Users/abhishekraj/reelstack && node --test test/*.test.js
998
+ ```
999
+
1000
+ Expected: all tests PASS (existing license / scaffold / lint / etc + the four new files).
1001
+
1002
+ - [ ] **Step 6: Commit**
1003
+
1004
+ ```bash
1005
+ cd /Users/abhishekraj/reelstack && git add cli/init.js test/init-flow.test.js
1006
+ git commit -m "ReelStack init — wire all four tiers + flag parsing
1007
+
1008
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1009
+ ```
1010
+
1011
+ ---
1012
+
1013
+ ## Task 7: Make /reelstack-beats fail-fast on declined whisper
1014
+
1015
+ **Files:**
1016
+ - Modify: `cli/beats.js`
1017
+
1018
+ If state.json says `whisperCli: "missing-declined"`, beats should refuse to run with a clear "you declined this during init; install it and re-run init" message instead of falling through to the existing "whisper-cli not found" error (which doesn't tell the buyer their decline is the reason).
1019
+
1020
+ - [ ] **Step 1: Add state check to top of `run()` in cli/beats.js**
1021
+
1022
+ Read the current top of `run()` ([cli/beats.js around line 50-58](../../../cli/beats.js#L50)) and add after the `which()` checks but BEFORE the existing fail messages:
1023
+
1024
+ ```javascript
1025
+ const { loadState } = require("./utils");
1026
+ // … existing requires …
1027
+
1028
+ async function run(argv) {
1029
+ // … existing argv parsing …
1030
+
1031
+ const state = loadState();
1032
+ if (state.whisperCli === "missing-declined") {
1033
+ fail("whisper-cli was declined during `reelstack init`.");
1034
+ console.log(" Install it now:");
1035
+ console.log(" brew install whisper-cpp # macOS");
1036
+ console.log(" Then re-run: reelstack init");
1037
+ process.exit(1);
1038
+ }
1039
+
1040
+ if (!which("ffmpeg")) {
1041
+ fail("ffmpeg not found. Install: brew install ffmpeg");
1042
+ process.exit(1);
1043
+ }
1044
+ // … rest unchanged …
1045
+ }
1046
+ ```
1047
+
1048
+ The exact insertion point is after argv parsing, before the existing `which("ffmpeg")` check at [cli/beats.js:52](../../../cli/beats.js#L52). Read the current file before editing to find the right line.
1049
+
1050
+ - [ ] **Step 2: Manually verify (without trashing your real state.json)**
1051
+
1052
+ ```bash
1053
+ # Backup current state
1054
+ cp ~/.reelstack/state.json ~/.reelstack/state.json.bak 2>/dev/null || true
1055
+
1056
+ # Force the declined state
1057
+ echo '{"whisperCli":"missing-declined"}' > ~/.reelstack/state.json
1058
+
1059
+ # Trigger the new fail-fast
1060
+ cd /Users/abhishekraj/reelstack && node cli/index.js beats /tmp/fake.wav 2>&1 | head -5
1061
+ ```
1062
+
1063
+ Expected: prints "whisper-cli was declined during `reelstack init`" and exits 1.
1064
+
1065
+ Restore real state:
1066
+
1067
+ ```bash
1068
+ mv ~/.reelstack/state.json.bak ~/.reelstack/state.json 2>/dev/null || rm ~/.reelstack/state.json
1069
+ ```
1070
+
1071
+ - [ ] **Step 3: Commit**
1072
+
1073
+ ```bash
1074
+ cd /Users/abhishekraj/reelstack && git add cli/beats.js
1075
+ git commit -m "ReelStack beats — fail-fast when whisper was declined during init
1076
+
1077
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1078
+ ```
1079
+
1080
+ ---
1081
+
1082
+ ## Task 8: Manual end-to-end verification
1083
+
1084
+ **Files:** none (verification only)
1085
+
1086
+ The TDD tests cover the units. Tier 3 + Tier 4 shell out to real `npx` and `ffmpeg`, so they need a manual run-through to confirm the buyer experience matches the spec.
1087
+
1088
+ - [ ] **Step 1: Test the "no Remotion project" path (the buyer's first run)**
1089
+
1090
+ ```bash
1091
+ mkdir -p /tmp/reelstack-test-run && cd /tmp/reelstack-test-run
1092
+ node /Users/abhishekraj/reelstack/cli/index.js init --name=demo-project
1093
+ ```
1094
+
1095
+ Expected:
1096
+ - License prompts (or uses cache)
1097
+ - All 3 dep checks pass
1098
+ - "No Remotion project in this directory. Scaffold one at ./demo-project/?" prompt → answer Y
1099
+ - `create-video` runs, scaffolds Remotion project
1100
+ - `npm install @devinilabs/reelstack` runs
1101
+ - Smoke test runs all 3 steps and passes
1102
+ - Final banner: "ReelStack is ready."
1103
+ - `cat ~/.reelstack/state.json` shows `smokeTest: "passed"`
1104
+
1105
+ - [ ] **Step 2: Test the "already in Remotion project" path (re-run)**
1106
+
1107
+ ```bash
1108
+ cd /tmp/reelstack-test-run/demo-project
1109
+ node /Users/abhishekraj/reelstack/cli/index.js init
1110
+ ```
1111
+
1112
+ Expected:
1113
+ - "Remotion project detected in current directory."
1114
+ - `@devinilabs/reelstack already present.`
1115
+ - Smoke test runs and passes
1116
+ - "ReelStack is ready."
1117
+
1118
+ - [ ] **Step 3: Test `--no-bootstrap` from a non-Remotion folder**
1119
+
1120
+ ```bash
1121
+ mkdir -p /tmp/reelstack-test-nobootstrap && cd /tmp/reelstack-test-nobootstrap
1122
+ node /Users/abhishekraj/reelstack/cli/index.js init --no-bootstrap
1123
+ ```
1124
+
1125
+ Expected:
1126
+ - Tiers 1 + 2 pass
1127
+ - "No Remotion project here; --no-bootstrap was passed."
1128
+ - "Re-run `reelstack init` from inside a Remotion project to wire up imports."
1129
+ - Exit 0 (success — buyer chose manual path)
1130
+ - `cat ~/.reelstack/state.json` shows `smokeTest: "skipped-no-project"`
1131
+
1132
+ - [ ] **Step 4: Test `--skip-smoke` with existing Remotion project**
1133
+
1134
+ ```bash
1135
+ cd /tmp/reelstack-test-run/demo-project
1136
+ node /Users/abhishekraj/reelstack/cli/index.js init --skip-smoke
1137
+ ```
1138
+
1139
+ Expected:
1140
+ - All tiers run except smoke
1141
+ - "Smoke test skipped (--skip-smoke)."
1142
+ - "ReelStack is ready."
1143
+ - `cat ~/.reelstack/state.json` shows `smokeTest: "skipped"`
1144
+
1145
+ - [ ] **Step 5: Cleanup**
1146
+
1147
+ ```bash
1148
+ rm -rf /tmp/reelstack-test-run /tmp/reelstack-test-nobootstrap
1149
+ ```
1150
+
1151
+ - [ ] **Step 6: Bump version + commit**
1152
+
1153
+ Edit `package.json`: bump version from `1.1.1-rc.1` to `1.2.0-rc.1` (the readiness gate is the marquee feature of v1.2 per the spec).
1154
+
1155
+ ```bash
1156
+ cd /Users/abhishekraj/reelstack && git add package.json
1157
+ git commit -m "ReelStack v1.2.0-rc.1 — init readiness gate
1158
+
1159
+ Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>"
1160
+ ```
1161
+
1162
+ ---
1163
+
1164
+ ## Done
1165
+
1166
+ After Task 8: every `/reelstack-*` command works the moment `reelstack init` exits 0, or init exits non-zero with a clear remediation. The "I paid, init said ready, but nothing works" failure mode is gone.