@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.
- package/LICENSE +128 -0
- package/README.md +125 -0
- package/cli/beats.js +124 -0
- package/cli/bootstrap.js +124 -0
- package/cli/capture.js +34 -0
- package/cli/direction.js +114 -0
- package/cli/icons.js +49 -0
- package/cli/index.js +101 -0
- package/cli/init.js +253 -0
- package/cli/license.js +168 -0
- package/cli/lint.js +865 -0
- package/cli/preview.js +59 -0
- package/cli/render.js +404 -0
- package/cli/scaffold.js +239 -0
- package/cli/smoke.js +76 -0
- package/cli/update.js +26 -0
- package/cli/utils.js +184 -0
- package/docs/buyers-guide.md +220 -0
- package/docs/design-discipline.md +130 -0
- package/docs/family-galleries/dark.md +95 -0
- package/docs/family-galleries/forbidden.md +78 -0
- package/docs/family-galleries/glass.md +98 -0
- package/docs/family-galleries/paper.md +82 -0
- package/docs/family-galleries/warm.md +86 -0
- package/docs/superpowers/plans/2026-05-09-reelstack-init-readiness-gate.md +1166 -0
- package/docs/superpowers/specs/2026-05-09-reelstack-init-readiness-gate-design.md +233 -0
- package/families/dark/components/DriftingSpotlights.tsx +59 -0
- package/families/dark/components/FilmGrain.tsx +44 -0
- package/families/dark/components/ForestCard.tsx +43 -0
- package/families/dark/components/GridBackground.tsx +29 -0
- package/families/dark/components/RadialVignette.tsx +21 -0
- package/families/dark/components/Scanlines.tsx +35 -0
- package/families/dark/components/SegmentOpacity.ts +37 -0
- package/families/dark/components/index.ts +13 -0
- package/families/dark/index.ts +31 -0
- package/families/dark/palette.ts +98 -0
- package/families/dark/presets/claudedispatch.ts +46 -0
- package/families/dark/presets/codedrop.ts +37 -0
- package/families/dark/presets/gpt55.ts +54 -0
- package/families/dark/presets/notebooklm.ts +50 -0
- package/families/dark/presets/resourcescta.ts +35 -0
- package/families/dark/presets/skills.ts +40 -0
- package/families/dark/presets/stitch.ts +46 -0
- package/families/dark/presets/stitch2.ts +43 -0
- package/families/dark/typography.ts +16 -0
- package/families/forbidden/components/ForbiddenCausticBlobs.tsx +52 -0
- package/families/forbidden/components/NewsprintTexture.tsx +28 -0
- package/families/forbidden/components/TintedShadow.tsx +36 -0
- package/families/forbidden/components/index.ts +38 -0
- package/families/forbidden/index.ts +17 -0
- package/families/forbidden/palette.ts +88 -0
- package/families/forbidden/presets/heretic.ts +44 -0
- package/families/forbidden/typography.ts +18 -0
- package/families/glass/components/BreakdownCard.tsx +158 -0
- package/families/glass/components/CausticBlobs.tsx +49 -0
- package/families/glass/components/Counter.tsx +72 -0
- package/families/glass/components/EyebrowPill.tsx +59 -0
- package/families/glass/components/FilmStrip.tsx +202 -0
- package/families/glass/components/FloatingGlyphs.tsx +78 -0
- package/families/glass/components/GlassCard.tsx +58 -0
- package/families/glass/components/GlassCardBezel.tsx +45 -0
- package/families/glass/components/HairlineGrid.tsx +30 -0
- package/families/glass/components/IridescentRing.tsx +114 -0
- package/families/glass/components/IridescentText.tsx +98 -0
- package/families/glass/components/LightBeam.tsx +46 -0
- package/families/glass/components/ParticleBurst.tsx +62 -0
- package/families/glass/components/SonarRings.tsx +81 -0
- package/families/glass/components/StaggeredWords.tsx +74 -0
- package/families/glass/components/index.ts +20 -0
- package/families/glass/index.ts +31 -0
- package/families/glass/palette.ts +93 -0
- package/families/glass/presets/claudewatch.ts +64 -0
- package/families/glass/presets/claudewatchcta.ts +43 -0
- package/families/glass/presets/graphify.ts +45 -0
- package/families/glass/presets/gstack.ts +48 -0
- package/families/glass/presets/jcode.ts +50 -0
- package/families/glass/presets/lilagents.ts +52 -0
- package/families/glass/presets/paperclip.ts +43 -0
- package/families/glass/typography.ts +15 -0
- package/families/index.ts +49 -0
- package/families/paper/components/CardSpring.tsx +42 -0
- package/families/paper/components/CreamGrid.tsx +26 -0
- package/families/paper/components/EditorialSerifText.tsx +51 -0
- package/families/paper/components/GreenAccentCard.tsx +10 -0
- package/families/paper/components/PaperShadow.tsx +30 -0
- package/families/paper/components/ScaleBlurText.tsx +40 -0
- package/families/paper/components/index.ts +11 -0
- package/families/paper/index.ts +23 -0
- package/families/paper/palette.ts +102 -0
- package/families/paper/presets/designreel.ts +32 -0
- package/families/paper/presets/devini3d.ts +45 -0
- package/families/paper/presets/justdrop.ts +39 -0
- package/families/paper/presets/opus.ts +48 -0
- package/families/paper/typography.ts +17 -0
- package/families/warm/components/AccentGlow.tsx +60 -0
- package/families/warm/components/BentoCell.tsx +56 -0
- package/families/warm/components/BentoGrid.tsx +30 -0
- package/families/warm/components/FilmGrain.tsx +36 -0
- package/families/warm/components/ScaleBlurCounter.tsx +71 -0
- package/families/warm/components/WarmSurface.tsx +35 -0
- package/families/warm/components/index.ts +11 -0
- package/families/warm/index.ts +19 -0
- package/families/warm/palette.ts +81 -0
- package/families/warm/presets/huashu.ts +49 -0
- package/families/warm/presets/mempalace.ts +51 -0
- package/families/warm/typography.ts +17 -0
- package/package.json +85 -0
- package/reference/dark/claudedispatch.tsx +2441 -0
- package/reference/dark/notebooklm.tsx +2316 -0
- package/reference/dark/stitch.tsx +3040 -0
- package/reference/forbidden/heretic.tsx +2636 -0
- package/reference/glass/claudewatch.tsx +3827 -0
- package/reference/glass/graphify.tsx +2418 -0
- package/reference/glass/paperclip.tsx +2218 -0
- package/reference/paper/designreel.tsx +883 -0
- package/reference/paper/justdrop.tsx +1898 -0
- package/reference/paper/opus.tsx +1770 -0
- package/reference/warm/huashu.tsx +3413 -0
- package/reference/warm/mempalace.tsx +2909 -0
- package/skill/SKILL.md +229 -0
- package/skill/commands/reelstack-beats.md +20 -0
- package/skill/commands/reelstack-capture.md +24 -0
- package/skill/commands/reelstack-critique.md +15 -0
- package/skill/commands/reelstack-dark.md +40 -0
- package/skill/commands/reelstack-direction.md +17 -0
- package/skill/commands/reelstack-forbidden.md +25 -0
- package/skill/commands/reelstack-glass.md +39 -0
- package/skill/commands/reelstack-icons.md +22 -0
- package/skill/commands/reelstack-init.md +17 -0
- package/skill/commands/reelstack-lint.md +22 -0
- package/skill/commands/reelstack-paper.md +36 -0
- package/skill/commands/reelstack-render.md +20 -0
- package/skill/commands/reelstack-warm.md +36 -0
- package/templates/dark/template.tsx +115 -0
- package/templates/forbidden/template.tsx +111 -0
- package/templates/glass/template.tsx +201 -0
- package/templates/paper/template.tsx +133 -0
- package/templates/warm/template.tsx +210 -0
- package/utils/ai-purple-blocklist.ts +13 -0
- package/utils/banned-fonts.ts +11 -0
- package/utils/cubic-bezier.ts +36 -0
- package/utils/easing.ts +84 -0
- package/utils/grid.ts +13 -0
- package/utils/render-presets.json +56 -0
- 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.
|