@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
package/LICENSE
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
ReelStack Commercial License v1.0
|
|
2
|
+
Copyright (c) 2026 Devini Labs. All rights reserved.
|
|
3
|
+
|
|
4
|
+
This software is licensed, not sold. Possession of this package without a
|
|
5
|
+
valid license key issued by Devini Labs does NOT grant any rights to use,
|
|
6
|
+
copy, modify, distribute, or sublicense the contents.
|
|
7
|
+
|
|
8
|
+
================================================================================
|
|
9
|
+
SUMMARY (the legal text below is authoritative)
|
|
10
|
+
================================================================================
|
|
11
|
+
|
|
12
|
+
- You may use ReelStack on a single end-user machine per license key.
|
|
13
|
+
- You may render unlimited videos for yourself, your employer, or your
|
|
14
|
+
clients with a valid license.
|
|
15
|
+
- You MAY NOT redistribute, repackage, resell, or open-source the families,
|
|
16
|
+
presets, components, templates, or CLI source contained in this package.
|
|
17
|
+
- You MAY NOT bundle ReelStack into another product offered for sale or
|
|
18
|
+
free distribution.
|
|
19
|
+
- License keys are non-transferable.
|
|
20
|
+
- Updates are included for the duration specified in your purchase.
|
|
21
|
+
|
|
22
|
+
================================================================================
|
|
23
|
+
GRANT
|
|
24
|
+
================================================================================
|
|
25
|
+
|
|
26
|
+
Subject to the terms of this License and the existence of a valid, active
|
|
27
|
+
license key issued by Devini Labs (via the payment processor at
|
|
28
|
+
https://devini.io/reelstack), Devini Labs ("Licensor") grants you
|
|
29
|
+
("Licensee") a non-exclusive, non-transferable, revocable license to:
|
|
30
|
+
|
|
31
|
+
(a) install ReelStack on Licensee's development machines;
|
|
32
|
+
(b) use the families, presets, components, templates, and CLI to scaffold,
|
|
33
|
+
compose, and render Licensee's own video content;
|
|
34
|
+
(c) ship the rendered video output to any platform Licensee chooses, with
|
|
35
|
+
no royalty or attribution required.
|
|
36
|
+
|
|
37
|
+
================================================================================
|
|
38
|
+
RESTRICTIONS
|
|
39
|
+
================================================================================
|
|
40
|
+
|
|
41
|
+
Licensee shall NOT, without prior written consent from Devini Labs:
|
|
42
|
+
|
|
43
|
+
1. Redistribute the source files contained in this package, in whole or in
|
|
44
|
+
part, in any form, public or private.
|
|
45
|
+
2. Resell or sublicense ReelStack or any portion of it.
|
|
46
|
+
3. Use ReelStack to create a competing product (template marketplace,
|
|
47
|
+
reel builder, video-as-a-service product) that exposes the same
|
|
48
|
+
primitives, presets, or templates to third parties.
|
|
49
|
+
4. Remove, modify, or obscure copyright notices, license headers, or
|
|
50
|
+
watermarks present in the source files.
|
|
51
|
+
5. Share, post, leak, or commit license keys to any public location.
|
|
52
|
+
|
|
53
|
+
================================================================================
|
|
54
|
+
TERMINATION
|
|
55
|
+
================================================================================
|
|
56
|
+
|
|
57
|
+
This License terminates automatically upon any breach by Licensee. Upon
|
|
58
|
+
termination, Licensee must delete all copies of ReelStack from their
|
|
59
|
+
systems. Rendered video output created prior to termination remains owned
|
|
60
|
+
by Licensee.
|
|
61
|
+
|
|
62
|
+
================================================================================
|
|
63
|
+
DISCLAIMER OF WARRANTY
|
|
64
|
+
================================================================================
|
|
65
|
+
|
|
66
|
+
REELSTACK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
67
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
68
|
+
FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
69
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY ARISING
|
|
70
|
+
FROM, OUT OF, OR IN CONNECTION WITH REELSTACK OR THE USE OR OTHER DEALINGS
|
|
71
|
+
IN THE SOFTWARE.
|
|
72
|
+
|
|
73
|
+
================================================================================
|
|
74
|
+
CONTACT
|
|
75
|
+
================================================================================
|
|
76
|
+
|
|
77
|
+
Licensing questions: licensing@devinilabs.com
|
|
78
|
+
Bug reports: https://github.com/devinilabs/reelstack/issues (private)
|
|
79
|
+
|
|
80
|
+
================================================================================
|
|
81
|
+
ACKNOWLEDGMENTS
|
|
82
|
+
================================================================================
|
|
83
|
+
|
|
84
|
+
ReelStack v1.1+ incorporates design-discipline rules from leonxlnx/taste-skill
|
|
85
|
+
(MIT-licensed, by @leonxlnx) and adopts UX patterns inspired by
|
|
86
|
+
alchaincyf/huashu-design (Personal Use Only, by @AlchainHust / 花叔).
|
|
87
|
+
|
|
88
|
+
The taste-skill rules are baked into ReelStack's components, palettes, and lint
|
|
89
|
+
under the terms of its MIT license. The huashu-design influence is pattern-only
|
|
90
|
+
— no code, text, or assets from huashu-design are copied into ReelStack;
|
|
91
|
+
ReelStack reimplements equivalent UX patterns (Design Direction Advisor,
|
|
92
|
+
5-dimension critique, multi-format export) against its own Remotion + CLI
|
|
93
|
+
architecture, with full credit.
|
|
94
|
+
|
|
95
|
+
ReelStack's "huashu" preset (Warm Signature family) is named for
|
|
96
|
+
@AlchainHust's project as a credit hook.
|
|
97
|
+
|
|
98
|
+
Full attribution and rule citations: docs/design-discipline.md.
|
|
99
|
+
|
|
100
|
+
================================================================================
|
|
101
|
+
REFERENCE REEL LICENSE (v1.1.1+)
|
|
102
|
+
================================================================================
|
|
103
|
+
|
|
104
|
+
ReelStack v1.1.1+ ships with reference reels at ~/.reelstack/reference/
|
|
105
|
+
containing stripped-down source from Devini Labs reels that have shipped to
|
|
106
|
+
Instagram, TikTok, and YouTube Shorts (GraphifyReel, ClaudeWatchReel,
|
|
107
|
+
HereticReel, and 9 others). Asset imports (audio, video, brand SVGs) have
|
|
108
|
+
been replaced with REFERENCE-STRIP placeholders.
|
|
109
|
+
|
|
110
|
+
THE REFERENCE LICENSE GRANTS LICENSEE:
|
|
111
|
+
(a) the right to STUDY the JSX, motion choreography, BEAT structures, and
|
|
112
|
+
scene composition of every reference reel;
|
|
113
|
+
(b) the right to ADAPT motion patterns, scene transitions, and primitive
|
|
114
|
+
compositions into Licensee's own reels;
|
|
115
|
+
(c) the right to copy SHORT excerpts (a single scene, a specific motion
|
|
116
|
+
combination, a transition recipe) into Licensee's own work.
|
|
117
|
+
|
|
118
|
+
THE REFERENCE LICENSE DOES NOT GRANT:
|
|
119
|
+
(d) the right to publish a reference reel verbatim as Licensee's own
|
|
120
|
+
template, free or paid;
|
|
121
|
+
(e) the right to redistribute the reference reels as a package, fork, or
|
|
122
|
+
derivative work without the rest of ReelStack;
|
|
123
|
+
(f) the right to remove REFERENCE-STRIP markers and ship the result as a
|
|
124
|
+
paid template;
|
|
125
|
+
(g) the right to use a reference reel's branding, copy, or visual identity
|
|
126
|
+
to misrepresent the resulting work as a Devini Labs production.
|
|
127
|
+
|
|
128
|
+
If in doubt: pattern adaptation OK. Verbatim re-publication NOT OK.
|
package/README.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# ReelStack
|
|
2
|
+
|
|
3
|
+
> Premium 9:16 Reel OS for Remotion. **5 cinematic style families. 22 production-tested presets. Audio-locked motion. IG-safe by default.**
|
|
4
|
+
|
|
5
|
+
ReelStack is the toolkit behind the Devini Labs reel aesthetic — packaged as a Claude Code skill plus a companion CLI. One slash command (`/reelstack-glass`, `/reelstack-paper`, `/reelstack-dark`, `/reelstack-warm`, `/reelstack-forbidden`) scaffolds a new 9:16 reel with the right palette, primitives, motion vocabulary, BEAT skeleton, and IG safe zones already wired up. From `init` to first preview render in under 10 minutes.
|
|
6
|
+
|
|
7
|
+
## What you get
|
|
8
|
+
|
|
9
|
+
| Family | Vibe | Presets |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| **Glass Iridescent** | Light lavender-cream + iridescent particles. Caustic blobs, sonar rings, glass cards, floating glyphs. | graphify · paperclip · gstack · lilagents · jcode · claudewatch · claudewatchcta |
|
|
12
|
+
| **Cream Paper** | Warm editorial paper + dark-green cards + Claude terracotta. | justdrop · opus · designreel · devini3d |
|
|
13
|
+
| **Dark Cinematic** | Zinc void + drifting spotlights + Claude terracotta. Premium ad-film mood. | stitch · stitch2 · codedrop · claudedispatch · notebooklm · gpt55 · resourcescta · skills |
|
|
14
|
+
| **Warm Signature** | Single-accent rule. Amber signature + emerald payoff + bento grid. | huashu · mempalace |
|
|
15
|
+
| **Forbidden** | Cream-rose paper + ember + crimson + plasma. Desaturated, mysterious. | heretic |
|
|
16
|
+
|
|
17
|
+
Each preset carries the **exact palette, BEAT structure, frame count, and motion vocabulary** of a real Devini Labs reel that has shipped to YouTube and Instagram.
|
|
18
|
+
|
|
19
|
+
## Why pay for it
|
|
20
|
+
|
|
21
|
+
- **House-style enforcement.** `reelstack lint` flags motion-floor violations, IG safe-zone breaches, hero-text overflow, and missing audio locks. Most boring AI-generated reel templates can't do this.
|
|
22
|
+
- **Audio-locked everywhere.** `reelstack beats <vo.wav>` runs whisper-cli and prints frame-accurate `BEAT` constants. No more eyeball-drift across 90-second clips.
|
|
23
|
+
- **GSAP vocabulary, ported.** Power4Out, expoOut, backOut — translated into Remotion `interpolate()` so motion language stays consistent.
|
|
24
|
+
- **Real brand assets.** `reelstack icons <brand>` pulls genuine SVGs from Iconify (logos:react, logos:next-js, etc.) instead of asking you to hand-draw.
|
|
25
|
+
- **Render presets.** `reelstack render <id> --platform=ig` ships with the right H.264, bitrate, and color flags for IG / TikTok / Shorts.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Inside an existing Remotion project — or any folder; init can bootstrap one
|
|
31
|
+
npx @devinilabs/reelstack init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`init` runs a **4-tier readiness gate** (v1.2+) so its success banner is provable. After it exits 0, every `/reelstack-*` command works — or it exits non-zero with a clear remediation.
|
|
35
|
+
|
|
36
|
+
| Tier | What it checks | If missing |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| 1 | Node ≥ 20 | exits with `nvm` / `fnm` / nodejs.org guidance |
|
|
39
|
+
| 2 | `ffmpeg` (required) + `whisper-cpp` (soft, only for `/reelstack-beats`) | macOS+Homebrew: offers to `brew install` for you. Linux / Windows / macOS-without-brew: prints the exact command for your platform and exits so you can install + retry |
|
|
40
|
+
| 3 | Remotion project + `@devinilabs/reelstack` dep | runs `npx create-video@latest --yes --blank reelstack-project` (override with `--name=…`, or skip with `--no-bootstrap`) |
|
|
41
|
+
| 4 | Smoke test — scaffolds Demo.tsx, runs `npx remotion compositions`, runs `ffmpeg -version` | exits 1 with the failing step name. Skip with `--skip-smoke`. |
|
|
42
|
+
|
|
43
|
+
The verified state is recorded at `~/.reelstack/state.json` so subcommands like `/reelstack-beats` can fail-fast with helpful messages instead of generic "not found" errors.
|
|
44
|
+
|
|
45
|
+
Don't have a license yet? Try the free preview:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @devinilabs/reelstack preview
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## First reel in 5 minutes
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
You: /reelstack-glass
|
|
55
|
+
ReelStack: Which preset? (graphify | paperclip | gstack | lilagents | jcode)
|
|
56
|
+
You: graphify
|
|
57
|
+
ReelStack: Reel name?
|
|
58
|
+
You: MyLaunch
|
|
59
|
+
ReelStack: Voiceover file? (path/.wav, or skip)
|
|
60
|
+
You: ./public/launch-vo.wav
|
|
61
|
+
|
|
62
|
+
✓ Created src/MyLaunchReel.tsx
|
|
63
|
+
✓ Registered in src/Root.tsx (1080×1920, 30fps, 1956 frames)
|
|
64
|
+
✓ Whisper transcribed → 9 BEAT constants injected
|
|
65
|
+
✓ Geist fonts pre-wired
|
|
66
|
+
✓ IG safe zones inlined
|
|
67
|
+
→ Open Remotion Studio? (Y/n)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
That's it. You're inside a real Devini Labs reel template, with your voiceover beats locked, your primitives imported, and Studio open at frame 0.
|
|
71
|
+
|
|
72
|
+
## Slash commands
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
/reelstack-glass Scaffold a Glass Iridescent reel
|
|
76
|
+
/reelstack-paper Scaffold a Cream Paper reel
|
|
77
|
+
/reelstack-dark Scaffold a Dark Cinematic reel
|
|
78
|
+
/reelstack-warm Scaffold a Warm Signature reel
|
|
79
|
+
/reelstack-forbidden Scaffold a Forbidden reel
|
|
80
|
+
/reelstack-init Set up ReelStack in a fresh Remotion project
|
|
81
|
+
/reelstack-beats Convert voiceover .wav → frame-accurate BEAT constants
|
|
82
|
+
/reelstack-capture Pull product screenshots into the reel via reel-capture
|
|
83
|
+
/reelstack-icons Pull real brand SVGs via better-icons (Iconify)
|
|
84
|
+
/reelstack-render Render with platform presets (ig | tiktok | shorts)
|
|
85
|
+
/reelstack-lint Validate motion floors, safe zones, audio lock, text fit
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
|
|
90
|
+
| Tool | macOS | Linux | Windows |
|
|
91
|
+
|---|---|---|---|
|
|
92
|
+
| **Node ≥ 20** | `nvm install 20` / [nodejs.org](https://nodejs.org/) | same | same |
|
|
93
|
+
| **Remotion 4** | auto-bootstrapped by `init`, or `npm create video@latest` | same | same |
|
|
94
|
+
| **ffmpeg** (required) | `brew install ffmpeg` (init can run this) | `apt-get install ffmpeg` · `dnf install ffmpeg` · `pacman -S ffmpeg` | `choco install ffmpeg` · `scoop install ffmpeg` |
|
|
95
|
+
| **whisper-cpp** (only `/reelstack-beats`) | `brew install whisper-cpp` (init can run this) | build from source: [github.com/ggerganov/whisper.cpp](https://github.com/ggerganov/whisper.cpp) | build from source, or `scoop install whisper.cpp` |
|
|
96
|
+
| **Claude Code** | [claude.com/claude-code](https://claude.com/claude-code) | same | same |
|
|
97
|
+
|
|
98
|
+
`init`'s Tier 2 detects your platform and either offers an auto-install (macOS+Homebrew) or prints the exact command for your package manager — so you don't need to know the table above by heart. If you're on Linux/Windows, install + re-run `init` once.
|
|
99
|
+
|
|
100
|
+
## Pricing
|
|
101
|
+
|
|
102
|
+
**₹149 INR / $3 USD** one-time on [devini.io/reelstack](https://devini.io/reelstack) — region-detected at checkout. See [devini.io/reelstack](https://devini.io/reelstack) for the live regional quote and the full docs at [devini.io/reelstack/docs](https://devini.io/reelstack/docs). Includes:
|
|
103
|
+
|
|
104
|
+
- All 5 families, all 22 presets in v1.0.
|
|
105
|
+
- All future v1.x preset additions for free.
|
|
106
|
+
- Single end-user license. (Team/agency tier coming in v1.1.)
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
ReelStack is **commercial software** licensed under the terms in [LICENSE](LICENSE). You may render unlimited content for yourself or clients with a valid license; you may not redistribute, repackage, or resell ReelStack itself.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
Made by [Devini Labs](https://github.com/devinilabs). Same hands that shipped GraphifyReel, HereticReel, JustDropReel, OpusReel, ClaudeWatchReel, NotebookLMReel, HuashuReel, and 15 more — now boxed into a skill.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Acknowledgments
|
|
119
|
+
|
|
120
|
+
ReelStack v1.1+ stands on the shoulders of two outside skills:
|
|
121
|
+
|
|
122
|
+
- **[leonxlnx/taste-skill](https://github.com/leonxlnx/taste-skill)** (MIT) — UI design-discipline rules. ReelStack bakes the master + per-family variant overlays directly into its components, palettes, and lint. Every scaffolded reel inherits the discipline by default.
|
|
123
|
+
- **[alchaincyf/huashu-design](https://github.com/alchaincyf/huashu-design)** (Personal Use Only — pattern inspiration only, no code/text copied) — productivity-UX patterns adapted to Remotion: Design Direction Advisor, 5-dim critique, multi-format render with BGM. The Warm Signature family's `huashu` preset is named for `@AlchainHust`'s project.
|
|
124
|
+
|
|
125
|
+
Full citation map at [`docs/design-discipline.md`](docs/design-discipline.md).
|
package/cli/beats.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reelstack beats <vo.wav>` — converts a voiceover into BEAT constants by
|
|
3
|
+
* running ffmpeg + whisper-cli and parsing the resulting SRT.
|
|
4
|
+
*/
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const os = require("node:os");
|
|
8
|
+
const { spawnSync } = require("node:child_process");
|
|
9
|
+
const { info, success, fail, warn, c, which, loadState } = require("./utils");
|
|
10
|
+
|
|
11
|
+
const FPS = 30;
|
|
12
|
+
|
|
13
|
+
function parseSrt(srtPath) {
|
|
14
|
+
const raw = fs.readFileSync(srtPath, "utf8");
|
|
15
|
+
const blocks = raw.split(/\r?\n\r?\n/).map((b) => b.trim()).filter(Boolean);
|
|
16
|
+
return blocks
|
|
17
|
+
.map((block) => {
|
|
18
|
+
const lines = block.split(/\r?\n/);
|
|
19
|
+
// line 0 = index, line 1 = "00:00:01,000 --> 00:00:02,400"
|
|
20
|
+
const tm = lines[1] && lines[1].match(
|
|
21
|
+
/(\d{2}):(\d{2}):(\d{2})[,.](\d{3})\s+-->\s+(\d{2}):(\d{2}):(\d{2})[,.](\d{3})/,
|
|
22
|
+
);
|
|
23
|
+
if (!tm) return null;
|
|
24
|
+
const start =
|
|
25
|
+
Number(tm[1]) * 3600 + Number(tm[2]) * 60 + Number(tm[3]) + Number(tm[4]) / 1000;
|
|
26
|
+
const text = lines.slice(2).join(" ").trim();
|
|
27
|
+
return { startSec: start, text };
|
|
28
|
+
})
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function slugifyBeatKey(text) {
|
|
33
|
+
return text
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9 ]+/g, "")
|
|
36
|
+
.trim()
|
|
37
|
+
.split(/\s+/)
|
|
38
|
+
.slice(0, 3)
|
|
39
|
+
.join("_") || "beat";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function run(argv) {
|
|
43
|
+
const voPath = argv.find((a) => !a.startsWith("--"));
|
|
44
|
+
if (!voPath) {
|
|
45
|
+
fail("Usage: reelstack beats <path/to/voiceover.wav>");
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
if (!fs.existsSync(voPath)) {
|
|
49
|
+
fail(`File not found: ${voPath}`);
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
const state = loadState();
|
|
53
|
+
if (state.whisperCli === "missing") {
|
|
54
|
+
fail("whisper-cli is not installed (init flagged this).");
|
|
55
|
+
console.log(" Install it now:");
|
|
56
|
+
console.log(" brew install whisper-cpp # macOS");
|
|
57
|
+
console.log(" Then re-run: reelstack init");
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!which("ffmpeg")) {
|
|
62
|
+
fail("ffmpeg not found. Install: brew install ffmpeg");
|
|
63
|
+
process.exit(2);
|
|
64
|
+
}
|
|
65
|
+
const whisperBin = which("whisper-cli") || which("whisper-cpp");
|
|
66
|
+
if (!whisperBin) {
|
|
67
|
+
fail("whisper-cli not found. Install: brew install whisper-cpp");
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tmpWav = path.join(os.tmpdir(), `reelstack_${process.pid}_v16k.wav`);
|
|
72
|
+
const tmpOut = path.join(os.tmpdir(), `reelstack_${process.pid}_out`);
|
|
73
|
+
const modelPath = process.env.WHISPER_MODEL || path.join(os.tmpdir(), "ggml-base.en.bin");
|
|
74
|
+
if (!fs.existsSync(modelPath)) {
|
|
75
|
+
warn(`Whisper model not found at ${modelPath}.`);
|
|
76
|
+
info(`Download: curl -L -o "${modelPath}" https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin`);
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
info("Resampling voiceover to 16kHz mono PCM…");
|
|
81
|
+
spawnSync("ffmpeg", [
|
|
82
|
+
"-y", "-i", voPath, "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", tmpWav,
|
|
83
|
+
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
84
|
+
|
|
85
|
+
info("Running whisper-cli…");
|
|
86
|
+
spawnSync(whisperBin, [
|
|
87
|
+
"-m", modelPath, "-f", tmpWav, "--output-srt", "-of", tmpOut,
|
|
88
|
+
], { stdio: ["ignore", "ignore", "inherit"] });
|
|
89
|
+
|
|
90
|
+
const srtPath = `${tmpOut}.srt`;
|
|
91
|
+
if (!fs.existsSync(srtPath)) {
|
|
92
|
+
fail(`whisper-cli did not produce ${srtPath}.`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const segments = parseSrt(srtPath);
|
|
97
|
+
if (segments.length === 0) {
|
|
98
|
+
fail("No segments parsed from SRT.");
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log("");
|
|
103
|
+
console.log(c.bold("BEAT constants (paste into your reel):"));
|
|
104
|
+
console.log("");
|
|
105
|
+
console.log("const BEATS = {");
|
|
106
|
+
const seenKeys = new Set();
|
|
107
|
+
for (const seg of segments) {
|
|
108
|
+
let key = slugifyBeatKey(seg.text);
|
|
109
|
+
let i = 1;
|
|
110
|
+
while (seenKeys.has(key)) { key = `${slugifyBeatKey(seg.text)}_${i++}`; }
|
|
111
|
+
seenKeys.add(key);
|
|
112
|
+
const frame = Math.round(seg.startSec * FPS);
|
|
113
|
+
const comment = seg.text.length > 64 ? seg.text.slice(0, 64) + "…" : seg.text;
|
|
114
|
+
console.log(` ${key.padEnd(28)} ${frame.toString().padStart(5)}, // ${seg.startSec.toFixed(2)}s — "${comment}"`);
|
|
115
|
+
}
|
|
116
|
+
console.log("} as const;");
|
|
117
|
+
console.log("");
|
|
118
|
+
warn("whisper base.en mishears tech jargon (Claude → Cloud, Remotion → Re-motion).");
|
|
119
|
+
warn("Use these timings frame-for-frame, but copy ON-SCREEN text from your script.");
|
|
120
|
+
console.log("");
|
|
121
|
+
success(`Parsed ${segments.length} segments → ${segments.length} BEAT constants.`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { run };
|
package/cli/bootstrap.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier 3: detect Remotion project state, prompt for bootstrap, run create-video.
|
|
3
|
+
*
|
|
4
|
+
* Only the pure helpers (`detectCwdState`, `buildCreateVideoArgs`) are unit-tested.
|
|
5
|
+
* The full `run()` function shells out and is exercised manually + via init-flow test.
|
|
6
|
+
*/
|
|
7
|
+
const fs = require("node:fs");
|
|
8
|
+
const path = require("node:path");
|
|
9
|
+
const { spawnSync } = require("node:child_process");
|
|
10
|
+
const { info, success, fail, warn, c, askYesNo } = require("./utils");
|
|
11
|
+
|
|
12
|
+
const PKG_NAME = require("../package.json").name;
|
|
13
|
+
|
|
14
|
+
function detectCwdState(cwd) {
|
|
15
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
16
|
+
if (!fs.existsSync(pkgPath)) return "no-package-json";
|
|
17
|
+
|
|
18
|
+
let pkg;
|
|
19
|
+
try {
|
|
20
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
21
|
+
} catch {
|
|
22
|
+
return "corrupt-package-json";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const hasRemotion =
|
|
26
|
+
(pkg.dependencies && pkg.dependencies.remotion) ||
|
|
27
|
+
(pkg.devDependencies && pkg.devDependencies.remotion);
|
|
28
|
+
|
|
29
|
+
return hasRemotion ? "has-remotion" : "non-remotion-package";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildCreateVideoArgs(projectName) {
|
|
33
|
+
return ["create-video@latest", "--yes", "--blank", projectName];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ensureReelstackDep(projectDir) {
|
|
37
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, "package.json"), "utf8"));
|
|
38
|
+
const already =
|
|
39
|
+
(pkg.dependencies && pkg.dependencies[PKG_NAME]) ||
|
|
40
|
+
(pkg.devDependencies && pkg.devDependencies[PKG_NAME]);
|
|
41
|
+
|
|
42
|
+
if (already) {
|
|
43
|
+
success(`${PKG_NAME} already present.`);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
info(`Installing ${PKG_NAME}…`);
|
|
48
|
+
const result = spawnSync("npm", ["install", PKG_NAME], {
|
|
49
|
+
stdio: "inherit",
|
|
50
|
+
cwd: projectDir,
|
|
51
|
+
});
|
|
52
|
+
if (result.status !== 0) {
|
|
53
|
+
fail(`npm install ${PKG_NAME} failed.`);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
success(`${PKG_NAME} installed.`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Run Tier 3. Returns { projectDir, isNew } or exits process on
|
|
62
|
+
* unrecoverable failure. `skipped: true` when buyer declines bootstrap or
|
|
63
|
+
* --no-bootstrap was passed.
|
|
64
|
+
*/
|
|
65
|
+
async function run({ noBootstrap = false, projectName = "reelstack-project" } = {}) {
|
|
66
|
+
const cwd = process.cwd();
|
|
67
|
+
const state = detectCwdState(cwd);
|
|
68
|
+
|
|
69
|
+
if (state === "has-remotion") {
|
|
70
|
+
info("Remotion project detected in current directory.");
|
|
71
|
+
if (!ensureReelstackDep(cwd)) process.exit(1);
|
|
72
|
+
return { projectDir: cwd, isNew: false, skipped: false };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (state === "corrupt-package-json") {
|
|
76
|
+
fail("Found a package.json in this folder, but it doesn't parse as valid JSON.");
|
|
77
|
+
console.log(c.gray(" Fix the file or run `reelstack init` from a different directory."));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (noBootstrap) {
|
|
82
|
+
warn("No Remotion project here; --no-bootstrap was passed.");
|
|
83
|
+
console.log(c.gray(" Re-run `reelstack init` from inside a Remotion project to wire up imports."));
|
|
84
|
+
return { projectDir: null, isNew: false, skipped: true };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (state === "non-remotion-package") {
|
|
88
|
+
warn("This folder has a non-Remotion package.json.");
|
|
89
|
+
info("Bootstrapping Remotion in a subdirectory instead so we don't muddy your project.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log("");
|
|
93
|
+
const yes = await askYesNo(`Scaffold a Remotion project at ./${projectName}/?`);
|
|
94
|
+
if (!yes) {
|
|
95
|
+
warn("Skipped Remotion bootstrap.");
|
|
96
|
+
console.log(c.gray(" Re-run `reelstack init` from inside a Remotion project to wire up imports."));
|
|
97
|
+
return { projectDir: null, isNew: false, skipped: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const targetDir = path.join(cwd, projectName);
|
|
101
|
+
if (fs.existsSync(targetDir)) {
|
|
102
|
+
fail(`./${projectName}/ already exists. Choose a different name with --name=<dir>, or delete it first.`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
info(`Running: npx ${buildCreateVideoArgs(projectName).join(" ")}`);
|
|
107
|
+
const result = spawnSync("npx", buildCreateVideoArgs(projectName), {
|
|
108
|
+
stdio: "inherit",
|
|
109
|
+
cwd,
|
|
110
|
+
});
|
|
111
|
+
if (result.status !== 0) {
|
|
112
|
+
fail(`create-video did not exit cleanly (status ${result.status}).`);
|
|
113
|
+
console.log(c.gray(" Check your network connection, or try running manually:"));
|
|
114
|
+
console.log(c.gray(` npx ${buildCreateVideoArgs(projectName).join(" ")}`));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
success("Remotion project scaffolded.");
|
|
118
|
+
|
|
119
|
+
if (!ensureReelstackDep(targetDir)) process.exit(1);
|
|
120
|
+
|
|
121
|
+
return { projectDir: targetDir, isNew: true, skipped: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { run, detectCwdState, buildCreateVideoArgs, ensureReelstackDep };
|
package/cli/capture.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reelstack capture <url> [--scroll] [--name=<slug>]` — delegate to the
|
|
3
|
+
* user's `reel-capture` Claude Code skill via a stub instruction. The CLI
|
|
4
|
+
* does NOT directly drive Chrome; it prints the wiring instructions so the
|
|
5
|
+
* skill can run reel-capture in the same session.
|
|
6
|
+
*/
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const { info, warn, success, fail, c } = require("./utils");
|
|
9
|
+
|
|
10
|
+
async function run(argv) {
|
|
11
|
+
const url = argv.find((a) => !a.startsWith("--"));
|
|
12
|
+
if (!url) {
|
|
13
|
+
fail("Usage: reelstack capture <url> [--scroll] [--name=<slug>]");
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
const slug = argv.find((a) => a.startsWith("--name="))?.replace("--name=", "")
|
|
17
|
+
|| new URL(url).hostname.replace(/\W+/g, "-");
|
|
18
|
+
const scroll = argv.includes("--scroll");
|
|
19
|
+
|
|
20
|
+
info(`Delegating to reel-capture skill: ${url}`);
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log(c.gray("In your Claude Code session, run:"));
|
|
23
|
+
console.log(` ${c.cyan(`/reel-capture ${url}${scroll ? " --scroll" : ""}`)}`);
|
|
24
|
+
console.log("");
|
|
25
|
+
console.log(c.gray("Output will land in:"));
|
|
26
|
+
console.log(` ${c.cyan(path.join(process.cwd(), "public", "captures", slug, scroll ? "frame_NNNN.png" : "hero.png"))}`);
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(c.gray("Then wire it into your reel:"));
|
|
29
|
+
console.log(` ${c.cyan(`<Img src={staticFile("captures/${slug}/hero.png")} />`)}`);
|
|
30
|
+
console.log("");
|
|
31
|
+
warn("ReelStack does not auto-edit your reel — wiring is your call.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { run };
|
package/cli/direction.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `reelstack direction "<brief>"` — when a buyer's brief is vague, return 3
|
|
3
|
+
* differentiated family picks with reasoning.
|
|
4
|
+
*
|
|
5
|
+
* Inspired by alchaincyf/huashu-design's Design Direction Advisor pattern
|
|
6
|
+
* (no code copied; pattern only).
|
|
7
|
+
*/
|
|
8
|
+
const { c, info, success, fail } = require("./utils");
|
|
9
|
+
|
|
10
|
+
const FAMILY_PROFILES = {
|
|
11
|
+
glass: {
|
|
12
|
+
label: "Glass Iridescent",
|
|
13
|
+
mood: "premium product reveal · underwater light · iridescent glass",
|
|
14
|
+
bestFor: ["product launch", "feature reveal", "SaaS announcement", "premium brand", "AI/ML demo"],
|
|
15
|
+
presets: ["graphify", "paperclip", "gstack", "lilagents", "jcode", "claudewatch"],
|
|
16
|
+
},
|
|
17
|
+
paper: {
|
|
18
|
+
label: "Cream Paper",
|
|
19
|
+
mood: "editorial print · warm document · card-stack hierarchy",
|
|
20
|
+
bestFor: ["thesis-driven narrative", "skill ecosystem reveal", "long-form storytelling", "design tool launch"],
|
|
21
|
+
presets: ["justdrop", "opus", "designreel", "devini3d"],
|
|
22
|
+
},
|
|
23
|
+
dark: {
|
|
24
|
+
label: "Dark Cinematic",
|
|
25
|
+
mood: "late-night ad-film · drifting spotlights · multi-brand partnership",
|
|
26
|
+
bestFor: ["partnership announcement", "integration reveal", "secret/insider drop", "dev-tool launch"],
|
|
27
|
+
presets: ["stitch", "stitch2", "codedrop", "claudedispatch", "notebooklm", "gpt55"],
|
|
28
|
+
},
|
|
29
|
+
warm: {
|
|
30
|
+
label: "Warm Signature",
|
|
31
|
+
mood: "confident product launch · single amber accent · bento grid",
|
|
32
|
+
bestFor: ["confident launch", "feature matrix reveal", "memory/learning narrative", "bento-style features"],
|
|
33
|
+
presets: ["huashu", "mempalace"],
|
|
34
|
+
},
|
|
35
|
+
forbidden: {
|
|
36
|
+
label: "Forbidden",
|
|
37
|
+
mood: "declassified document · cream-rose paper · ember/crimson/plasma",
|
|
38
|
+
bestFor: ["mysterious reveal", "unconventional tool", "open-source rebellion", "censorship-strip narrative"],
|
|
39
|
+
presets: ["heretic"],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const KEYWORD_MAP = {
|
|
44
|
+
glass: ["product", "launch", "ai", "ml", "saas", "demo", "premium", "reveal", "feature", "ship"],
|
|
45
|
+
paper: ["thesis", "essay", "skill", "ecosystem", "narrative", "design", "editorial", "story", "long"],
|
|
46
|
+
dark: ["partnership", "integration", "secret", "insider", "cinematic", "drop", "tool", "dev", "code"],
|
|
47
|
+
warm: ["confident", "matrix", "memory", "learning", "bento", "feature", "warm", "amber", "ship"],
|
|
48
|
+
forbidden:["mysterious", "declassified", "rebellion", "open source", "censorship", "alt", "uncommon"],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Score each family by counting matching keywords in the brief.
|
|
53
|
+
*/
|
|
54
|
+
function scoreFamilies(brief) {
|
|
55
|
+
const lc = brief.toLowerCase();
|
|
56
|
+
const scores = {};
|
|
57
|
+
for (const [family, keywords] of Object.entries(KEYWORD_MAP)) {
|
|
58
|
+
scores[family] = keywords.reduce((acc, kw) => acc + (lc.includes(kw) ? 1 : 0), 0);
|
|
59
|
+
}
|
|
60
|
+
return scores;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pickTop3(scores) {
|
|
64
|
+
// Sort by score desc; on tie, sort alphabetically by family key for
|
|
65
|
+
// deterministic output (no Object.entries insertion-order surprises).
|
|
66
|
+
return Object.entries(scores)
|
|
67
|
+
.sort(([famA, a], [famB, b]) => (b - a) || famA.localeCompare(famB))
|
|
68
|
+
.slice(0, 3)
|
|
69
|
+
.map(([family, score]) => ({ family, score, profile: FAMILY_PROFILES[family] }));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Pure helper: scoreFamilies + pickTop3 + zero-score fallback. Returns the
|
|
74
|
+
* 3 picks the CLI's `direction` runtime would print, without touching stdout.
|
|
75
|
+
*/
|
|
76
|
+
function directionPicks(brief) {
|
|
77
|
+
const scores = scoreFamilies(brief);
|
|
78
|
+
const allZero = Object.values(scores).every((s) => s === 0);
|
|
79
|
+
if (allZero) {
|
|
80
|
+
return ["glass", "dark", "warm"].map((family) => ({
|
|
81
|
+
family,
|
|
82
|
+
score: 0,
|
|
83
|
+
profile: FAMILY_PROFILES[family],
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return pickTop3(scores);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function run(argv) {
|
|
90
|
+
const brief = argv.filter((a) => !a.startsWith("--")).join(" ").trim();
|
|
91
|
+
if (!brief) {
|
|
92
|
+
fail("Usage: reelstack direction \"<brief>\"");
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
info(`Brief: "${brief}"`);
|
|
97
|
+
console.log("");
|
|
98
|
+
|
|
99
|
+
const top3 = directionPicks(brief);
|
|
100
|
+
|
|
101
|
+
console.log(c.bold("Three directions for your brief:"));
|
|
102
|
+
console.log("");
|
|
103
|
+
top3.forEach(({ family, profile }, i) => {
|
|
104
|
+
console.log(` ${c.cyan(`${i + 1}.`)} ${c.bold(profile.label)} — ${c.gray(profile.mood)}`);
|
|
105
|
+
console.log(` ${c.gray("best for:")} ${profile.bestFor.slice(0, 3).join(" · ")}`);
|
|
106
|
+
console.log(` ${c.gray("presets: ")} ${profile.presets.slice(0, 4).join(", ")}${profile.presets.length > 4 ? "…" : ""}`);
|
|
107
|
+
console.log(` ${c.gray("trigger: ")} /reelstack-${family}`);
|
|
108
|
+
console.log("");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
console.log(c.gray("Pick one and run the matching slash command. Or pass `--family=<x>` to scaffold directly."));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { run, scoreFamilies, pickTop3, directionPicks, FAMILY_PROFILES };
|