reelson 0.7.0 → 0.7.2
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/CHANGELOG.md +26 -0
- package/README.md +80 -32
- package/bin/reelson.js +89 -34
- package/bin/reelson.ts +86 -34
- package/docs/prompting.md +2 -2
- package/package.json +5 -3
- package/skills/reelson-compose/scripts/project.js +5 -5
- package/skills/reelson-compose/scripts/project.ts +5 -5
- package/skills/reelson-compose/studio/studio.js +1 -1
- package/skills/reelson-record/SKILL.md +4 -3
- package/skills/reelson-record/scripts/config.js +15 -0
- package/skills/reelson-record/scripts/config.ts +17 -0
- package/docs/demo.webp +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to reelson. The format follows [Keep a Changelog](https://ke
|
|
|
4
4
|
versions follow [Semantic Versioning](https://semver.org/) (before 1.0, a minor version may change
|
|
5
5
|
`video.json` or `demo.config.json`; the notes say how to update).
|
|
6
6
|
|
|
7
|
+
## 0.7.2 — 2026-09-23
|
|
8
|
+
|
|
9
|
+
The first release published by CI (staged, then approved on npm).
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- The README clip is re-rendered by CI on every push to main and served from the rolling `demo`
|
|
13
|
+
pre-release, so the npm package no longer ships it (2.6 MB → about 0.3 MB).
|
|
14
|
+
- Hardened publishing: actions pinned to commit SHAs (Dependabot keeps them current, npm updates
|
|
15
|
+
after a 7-day cooldown), npm pinned, no install scripts or cache in the publish job, no token left
|
|
16
|
+
in the checkout, and the publish runs in the `npm` environment (approval before each release).
|
|
17
|
+
CI only **stages** a release (`npm stage publish`); it goes live once approved on npm with 2FA.
|
|
18
|
+
- README: a pipeline diagram that renders the same in every font, commands with their notes above them.
|
|
19
|
+
|
|
20
|
+
## 0.7.1 — 2026-09-23
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- **Works with any agent that reads Agent Skills**, not only Claude Code: `reelson install` links the
|
|
24
|
+
skills into `.agents/skills/` (Codex and others) and links `.claude/skills/` to them (nothing to do
|
|
25
|
+
when the whole `.claude/skills` folder already links to `.agents/skills`); `--global` does the same
|
|
26
|
+
in the home folder. New `demo.config.json` `$schema` paths and scenario imports go through
|
|
27
|
+
`.agents/skills`; existing `.claude/skills` paths keep working. The `.gitignore` hint adds the links.
|
|
28
|
+
- **`reelson install` asks where** when neither `--global` nor a folder is given: every project
|
|
29
|
+
(the default: `~/.agents/skills` + `~/.claude/skills`), this project, or another folder. `-y` takes
|
|
30
|
+
the default; without a terminal it never asks. (0.7.0 installed into the current folder.)
|
|
31
|
+
- Agent-neutral wording in the README, the skills and the prompting guide.
|
|
32
|
+
|
|
7
33
|
## 0.7.0 — 2026-09-23
|
|
8
34
|
|
|
9
35
|
The first release on npm.
|
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# reelson
|
|
2
2
|
|
|
3
3
|
Turn a prompt into a finished, branded demo video of a web app. Two
|
|
4
|
-
[
|
|
4
|
+
[Agent Skills](https://agentskills.io) plus a `reelson` CLI, for Claude Code, Codex or any agent
|
|
5
|
+
that reads `SKILL.md` skills:
|
|
5
6
|
|
|
6
7
|
| Skill | Does | Output |
|
|
7
8
|
|------------------------------------------|-----------------------------------------------------------------------------|------------------------------------------|
|
|
@@ -11,16 +12,24 @@ Turn a prompt into a finished, branded demo video of a web app. Two
|
|
|
11
12
|
The same prompt re-creates the video after a UI change: a scenario re-records in ~20 s,
|
|
12
13
|
headless, with identical pacing, and callouts/zooms follow their markers and clicks.
|
|
13
14
|
|
|
14
|
-

|
|
15
16
|
|
|
16
|
-
<sub>The [example](examples/todo-add-item/scenario.ts), rendered by CI on every
|
|
17
|
-
|
|
17
|
+
<sub>The [TodoMVC example](examples/todo-add-item/scenario.ts), recorded and rendered by CI on every
|
|
18
|
+
push to main.</sub>
|
|
18
19
|
|
|
19
20
|
```
|
|
20
|
-
scenario.ts
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
scenario.ts
|
|
22
|
+
| reelson record
|
|
23
|
+
v
|
|
24
|
+
recording.mp4 + markers.json
|
|
25
|
+
| reelson build <-- video.json (title, trim, callouts, zooms)
|
|
26
|
+
| <-- demo.config.json (brand, logo, language, music)
|
|
27
|
+
| <-- template + sections
|
|
28
|
+
v
|
|
29
|
+
video/ a HyperFrames project
|
|
30
|
+
| reelson render
|
|
31
|
+
v
|
|
32
|
+
video/renders/<slug>.mp4 + .gif, portrait, square, .srt/.vtt captions
|
|
24
33
|
```
|
|
25
34
|
|
|
26
35
|
## Requirements
|
|
@@ -34,24 +43,33 @@ video.json (title, trim, callouts, zooms) ┴──reelson build──▶ video/
|
|
|
34
43
|
|
|
35
44
|
```bash
|
|
36
45
|
npm install -g reelson
|
|
37
|
-
reelson install
|
|
46
|
+
reelson install # asks: every project (default), this project, or another folder
|
|
38
47
|
```
|
|
39
48
|
|
|
40
|
-
`reelson install` downloads Playwright's Chromium
|
|
41
|
-
|
|
42
|
-
|
|
49
|
+
`reelson install` downloads Playwright's Chromium and links both skills into `.agents/skills/`
|
|
50
|
+
(Codex and the other agents that read Agent Skills) and `.claude/skills/` (Claude Code, linked to
|
|
51
|
+
the first — or nothing to do when the whole folder already links to `.agents/skills`):
|
|
52
|
+
|
|
53
|
+
- **every project**: in `~/.agents/skills` and `~/.claude/skills` (`reelson install --global`);
|
|
54
|
+
run `reelson init` in a project for its `demo.config.json`;
|
|
55
|
+
- **one project**: in `my-app/.agents/skills` and `my-app/.claude/skills`
|
|
56
|
+
(`reelson install ~/code/my-app`); also creates `my-app/demo.config.json` and prints the
|
|
57
|
+
`.gitignore` lines.
|
|
58
|
+
|
|
59
|
+
`-y` takes the default without asking; without a terminal (CI, scripts) it never asks. The links
|
|
60
|
+
point at the installed package, so `npm update -g reelson` updates every project.
|
|
43
61
|
|
|
44
62
|
To work on reelson itself, install from a checkout instead: the `reelson` command then runs the
|
|
45
63
|
TypeScript sources directly and `git pull` updates every project.
|
|
46
64
|
|
|
47
65
|
```bash
|
|
48
66
|
git clone git@github.com:reelson/reelson.git ~/reelson
|
|
49
|
-
~/reelson/install.sh
|
|
67
|
+
~/reelson/install.sh # npm link + reelson install (same question; or --global / <project-dir>)
|
|
50
68
|
```
|
|
51
69
|
|
|
52
70
|
reelson was called reelkit before 0.7. Re-run `reelson install` (or `install.sh`) for each project: it drops the old
|
|
53
71
|
`reelkit` command and `reelkit-*` skill links. Then point scenario imports and `$schema` paths
|
|
54
|
-
at `.claude/skills/reelson
|
|
72
|
+
at `.agents/skills/reelson-*` (`.claude/skills/reelson-*` works too).
|
|
55
73
|
|
|
56
74
|
## Configure per project — `demo.config.json`
|
|
57
75
|
|
|
@@ -87,7 +105,7 @@ typo is an error with a "did you mean" hint).
|
|
|
87
105
|
|
|
88
106
|
## Use
|
|
89
107
|
|
|
90
|
-
Ask
|
|
108
|
+
Ask your agent in the project, e.g.:
|
|
91
109
|
|
|
92
110
|
> Make a demo video of searching a customer. Admin area on https://app.test as the admin.
|
|
93
111
|
> Steps: open Customers, type a name in the table search, hover the match. Title "Find a
|
|
@@ -97,18 +115,30 @@ More prompts in [docs/prompting.md](docs/prompting.md); the rules every video fo
|
|
|
97
115
|
[docs/style-guide.md](docs/style-guide.md). By hand:
|
|
98
116
|
|
|
99
117
|
```bash
|
|
100
|
-
|
|
101
|
-
reelson
|
|
102
|
-
|
|
103
|
-
reelson
|
|
104
|
-
#
|
|
105
|
-
reelson
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
reelson
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
reelson
|
|
118
|
+
# tools + cursor/footage sync on this machine
|
|
119
|
+
reelson doctor
|
|
120
|
+
# a scenario stub
|
|
121
|
+
reelson new customers-search --url https://app.test
|
|
122
|
+
# --mobile / --square: the takes for --portrait / --square; --all-takes: all three
|
|
123
|
+
reelson record customers-search [--headed]
|
|
124
|
+
# creates video.json on the first run; then edit it: callout wording,
|
|
125
|
+
# { "clicks": [2, 3], "scale": 1.8 } zooms, trim ("auto" or a marker)
|
|
126
|
+
reelson build customers-search --title "Find a customer"
|
|
127
|
+
# "voice": true in video.json: speak the callouts
|
|
128
|
+
# (voice.provider: openai, elevenlabs, piper, command)
|
|
129
|
+
reelson voice customers-search
|
|
130
|
+
# schemas, zoom timing, hyperframes lint
|
|
131
|
+
reelson check customers-search
|
|
132
|
+
# after an app change: every demo still records and fits
|
|
133
|
+
reelson verify --all
|
|
134
|
+
# preview + edit on a layer timeline (saves video.json)
|
|
135
|
+
reelson studio customers-search
|
|
136
|
+
# templates and intro/recap/outro sections
|
|
137
|
+
reelson templates
|
|
138
|
+
# the MP4 + .srt/.vtt captions; --gif, --square, --portrait, --all-formats, --draft
|
|
139
|
+
reelson render customers-search
|
|
140
|
+
# every demo; skips the unchanged ones
|
|
141
|
+
reelson render --all
|
|
112
142
|
```
|
|
113
143
|
|
|
114
144
|
Per video, commit `scenario.ts`, `markers.json` and `video.json`; everything else is generated.
|
|
@@ -155,10 +185,28 @@ Node will not run TypeScript from under `node_modules`, so the package ships `.j
|
|
|
155
185
|
each `.ts` (`npm run build`; `npm pack` / `npm publish` build first and clean up after).
|
|
156
186
|
`bin/run.js` picks the compiled CLI when installed from npm and the sources in a checkout.
|
|
157
187
|
|
|
158
|
-
1.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
the
|
|
188
|
+
1. Add the release to CHANGELOG.md, then `npm version patch` (or `minor` / `major`: bumps
|
|
189
|
+
package.json, commits, tags `vX.Y.Z`) and `git push --follow-tags`.
|
|
190
|
+
2. [.github/workflows/publish.yml](.github/workflows/publish.yml) checks the tag matches
|
|
191
|
+
package.json, runs the checks and **stages** the release with npm trusted publishing (OIDC: no
|
|
192
|
+
npm token exists anywhere; with provenance). It runs in the `npm` environment, so it first
|
|
193
|
+
waits for a required reviewer on GitHub.
|
|
194
|
+
3. Approve it on npm with 2FA: `npm stage list reelson`, then `npm stage approve <id>` (or on
|
|
195
|
+
npmjs.com). Until then nobody can install it.
|
|
196
|
+
|
|
197
|
+
One-time setup:
|
|
198
|
+
|
|
199
|
+
- npmjs.com → the package → Settings → **Trusted publisher**: GitHub Actions, `reelson` /
|
|
200
|
+
`reelson`, workflow `publish.yml`, environment `npm`, **"Allow npm publish" off** (it may only
|
|
201
|
+
stage). Under **Publishing access**, pick "Require two-factor authentication and disallow
|
|
202
|
+
bypass 2fa tokens".
|
|
203
|
+
- GitHub → Settings → **Environments** → `npm`: add yourself as a required reviewer and allow
|
|
204
|
+
only `v*` tags to deploy. Settings → Rules → **Rulesets**: restrict creating `v*` tags to
|
|
205
|
+
yourself. (Both need a public repository, or a paid plan for a private one.)
|
|
206
|
+
|
|
207
|
+
The workflows pin every action to a commit SHA ([Dependabot](.github/dependabot.yml) proposes
|
|
208
|
+
updates weekly, npm packages after a 7-day cooldown), install dependencies without install
|
|
209
|
+
scripts when publishing, and never leave a token in the checkout.
|
|
162
210
|
|
|
163
211
|
The very first release has to be published by hand (trusted publishing needs the package to
|
|
164
212
|
exist): `npm login && npm publish`. Check the contents first with `npm pack --dry-run`.
|
|
@@ -171,7 +219,7 @@ skills/
|
|
|
171
219
|
reelson-record/ SKILL.md, scripts/ (record, scenario, cursor-overlay, config, validate), schemas/
|
|
172
220
|
reelson-compose/ SKILL.md, scripts/ (build, check, timeline, zooms, composition, project, hyperframes),
|
|
173
221
|
schemas/, templates/<name>/ (stages), sections/<slot>/<name>/
|
|
174
|
-
docs/ style-guide.md, prompting.md
|
|
222
|
+
docs/ style-guide.md, prompting.md
|
|
175
223
|
examples/ demo.config.json + todo-add-item/ (scenario, markers, video.json)
|
|
176
224
|
test/ unit + golden tests, fixtures
|
|
177
225
|
music/ local-only tracks (git-ignored; licences are per project)
|
package/bin/reelson.js
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* demo.config.json (walking up from the working directory).
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
10
|
import { homedir } from 'node:os';
|
|
11
|
+
import { createInterface } from 'node:readline/promises';
|
|
11
12
|
import { basename, relative, resolve } from 'node:path';
|
|
12
13
|
import { parseArgs, parseEnv } from 'node:util';
|
|
13
|
-
import { CONFIG_SCHEMA_PATH, ConfigError, fromRoot, loadConfig } from "../skills/reelson-record/scripts/config.js";
|
|
14
|
+
import { CONFIG_SCHEMA_PATH, ConfigError, fromRoot, loadConfig, viaProjectSkills } from "../skills/reelson-record/scripts/config.js";
|
|
14
15
|
import { doctor } from "../skills/reelson-record/scripts/doctor.js";
|
|
15
16
|
import { build, plan } from "../skills/reelson-compose/scripts/build.js";
|
|
16
17
|
import { captionCues, toSrt, toVtt } from "../skills/reelson-compose/scripts/captions.js";
|
|
@@ -26,11 +27,13 @@ const HELP = `reelson — scripted walkthroughs → branded demo videos
|
|
|
26
27
|
|
|
27
28
|
Usage: reelson <command> [options]
|
|
28
29
|
|
|
29
|
-
install [<project-dir>
|
|
30
|
+
install [<project-dir> | --global] [-y] [--no-browser]
|
|
30
31
|
link the reelson-record + reelson-compose skills into
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
.agents/skills/ (Codex and other agents) and .claude/skills/
|
|
33
|
+
(Claude Code): --global in ~/ for every project, or in a
|
|
34
|
+
project (which also gets a demo.config.json); asks which when
|
|
35
|
+
neither is given (-y, or no terminal: --global). Downloads
|
|
36
|
+
Playwright's Chromium (--no-browser: skip it)
|
|
34
37
|
init create demo.config.json in this directory
|
|
35
38
|
doctor check the tools and that the cursor layer lines up here
|
|
36
39
|
new <slug> [--url <origin>] start <videosDir>/<slug>/scenario.ts
|
|
@@ -119,7 +122,7 @@ async function run(cmd, argv) {
|
|
|
119
122
|
console.log(JSON.parse(readFileSync(resolve(KIT_ROOT, 'package.json'), 'utf8')).version);
|
|
120
123
|
return 0;
|
|
121
124
|
case 'install':
|
|
122
|
-
return install(argv);
|
|
125
|
+
return await install(argv);
|
|
123
126
|
case 'init':
|
|
124
127
|
return init();
|
|
125
128
|
case 'doctor':
|
|
@@ -176,22 +179,25 @@ function one(positionals, usage) {
|
|
|
176
179
|
return positionals[0];
|
|
177
180
|
}
|
|
178
181
|
/**
|
|
179
|
-
* `reelson install`: links the skills into a project (or
|
|
182
|
+
* `reelson install`: links the skills into a project (or the home folder) and prepares the machine. The links
|
|
180
183
|
* point at this installation, so `npm update -g reelson` (or `git pull` in a checkout) updates every project.
|
|
181
184
|
*/
|
|
182
|
-
function install(argv) {
|
|
185
|
+
async function install(argv) {
|
|
183
186
|
const skillNames = ['reelson-record', 'reelson-compose'];
|
|
184
187
|
// Names used before 0.7 (demo-* before that): their links are dropped when they point at a reelson install.
|
|
185
188
|
const oldNames = ['demo-record', 'demo-video', 'reelkit-record', 'reelkit-compose'];
|
|
186
189
|
const { values, positionals } = parseArgs({
|
|
187
190
|
args: argv,
|
|
188
191
|
allowPositionals: true,
|
|
189
|
-
options: { global: { type: 'boolean' }, 'no-browser': { type: 'boolean' } },
|
|
192
|
+
options: { global: { type: 'boolean' }, yes: { type: 'boolean', short: 'y' }, 'no-browser': { type: 'boolean' } },
|
|
190
193
|
});
|
|
191
194
|
if (positionals.length > 1 || (values.global && positionals.length)) {
|
|
192
|
-
throw new ReelsonError('usage: reelson install [<project-dir> | --global] [--no-browser]');
|
|
195
|
+
throw new ReelsonError('usage: reelson install [<project-dir> | --global] [-y] [--no-browser]');
|
|
193
196
|
}
|
|
194
|
-
|
|
197
|
+
// Asked only when neither --global nor a folder is given and someone is at the terminal.
|
|
198
|
+
const where = values.global ? null : positionals.length ? resolve(positionals[0]) : values.yes || !process.stdin.isTTY ? null : await askWhere();
|
|
199
|
+
const global = where === null;
|
|
200
|
+
const target = where ?? homedir();
|
|
195
201
|
if (!existsSync(target)) {
|
|
196
202
|
throw new ReelsonError(`${target} does not exist`);
|
|
197
203
|
}
|
|
@@ -206,43 +212,94 @@ function install(argv) {
|
|
|
206
212
|
throw new ReelsonError('could not install Chromium — run `npx playwright install chromium`');
|
|
207
213
|
}
|
|
208
214
|
}
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
// .agents/skills links to this installation; .claude/skills links to .agents/skills (unless the
|
|
216
|
+
// whole folder already is a link to it), so both agents see the same skills.
|
|
217
|
+
const agents = resolve(target, '.agents/skills');
|
|
218
|
+
const claude = resolve(target, '.claude/skills');
|
|
219
|
+
mkdirSync(agents, { recursive: true });
|
|
220
|
+
mkdirSync(claude, { recursive: true });
|
|
221
|
+
const shared = realpathSync(agents) === realpathSync(claude);
|
|
211
222
|
const isLink = (path) => lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink() ?? false;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
223
|
+
const link = (path, to) => {
|
|
224
|
+
if (existsSync(path) && !isLink(path)) {
|
|
225
|
+
throw new ReelsonError(`${path} is a folder, not a link to reelson — move it away and run install again`);
|
|
226
|
+
}
|
|
227
|
+
if (isLink(path)) {
|
|
228
|
+
rmSync(path);
|
|
229
|
+
}
|
|
230
|
+
symlinkSync(to, path, 'dir');
|
|
231
|
+
console.log(` linked ${path} → ${to}`);
|
|
232
|
+
};
|
|
233
|
+
for (const dir of shared ? [agents] : [agents, claude]) {
|
|
234
|
+
for (const old of oldNames) {
|
|
235
|
+
const path = resolve(dir, old);
|
|
236
|
+
if (isLink(path) && (resolve(dir, readlinkSync(path)).startsWith(`${KIT_ROOT}/`) || /[\\/]skills[\\/](reelkit|reelson)-/.test(readlinkSync(path)))) {
|
|
237
|
+
rmSync(path);
|
|
238
|
+
console.log(` removed old link ${path}`);
|
|
239
|
+
}
|
|
217
240
|
}
|
|
218
241
|
}
|
|
219
242
|
for (const skill of skillNames) {
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
243
|
+
link(resolve(agents, skill), resolve(KIT_ROOT, 'skills', skill));
|
|
244
|
+
if (!shared) {
|
|
245
|
+
link(resolve(claude, skill), relative(claude, resolve(agents, skill)));
|
|
223
246
|
}
|
|
224
|
-
if (isLink(link)) {
|
|
225
|
-
rmSync(link);
|
|
226
|
-
}
|
|
227
|
-
symlinkSync(resolve(KIT_ROOT, 'skills', skill), link, 'dir');
|
|
228
|
-
console.log(` linked ${link}`);
|
|
229
247
|
}
|
|
230
|
-
if (!
|
|
248
|
+
if (!global) {
|
|
231
249
|
if (!existsSync(resolve(target, 'demo.config.json'))) {
|
|
232
250
|
init(target);
|
|
233
251
|
}
|
|
234
252
|
const videos = loadConfig(target).videosDir.replace(/^\.?\/+|\/+$/g, '');
|
|
235
253
|
console.log(`
|
|
236
|
-
Add to ${resolve(target, '.gitignore')}:
|
|
254
|
+
Add to ${resolve(target, '.gitignore')} (the skill links point at this machine's reelson):
|
|
237
255
|
|
|
256
|
+
/.agents/skills/reelson-*
|
|
257
|
+
/.claude/skills/reelson-*
|
|
238
258
|
/${videos}/**/recording*.mp4
|
|
239
259
|
/${videos}/**/.raw*/
|
|
240
260
|
/${videos}/**/video/
|
|
241
261
|
/${videos}/**/*.openscreen`);
|
|
242
262
|
}
|
|
243
|
-
console.log('\nDone. Try: reelson doctor');
|
|
263
|
+
console.log(global ? '\nDone — every project sees the skills. In a project: reelson init, then reelson doctor' : '\nDone. Try: reelson doctor');
|
|
244
264
|
return 0;
|
|
245
265
|
}
|
|
266
|
+
/** `reelson install`'s question: every project (the default), this one, or another folder → null for global, else the project. */
|
|
267
|
+
async function askWhere() {
|
|
268
|
+
const home = (path) => (path.startsWith(homedir()) ? `~${path.slice(homedir().length)}` : path);
|
|
269
|
+
const here = process.cwd();
|
|
270
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
271
|
+
try {
|
|
272
|
+
console.log(`Where should reelson install its skills?
|
|
273
|
+
1. Every project (default): ~/.agents/skills, linked from ~/.claude/skills
|
|
274
|
+
2. This project: ${home(here)}/.agents/skills, linked from .claude/skills
|
|
275
|
+
3. Another project…`);
|
|
276
|
+
for (;;) {
|
|
277
|
+
const answer = (await rl.question('Choose [1]: ')).trim();
|
|
278
|
+
if (answer === '' || answer === '1')
|
|
279
|
+
return null;
|
|
280
|
+
if (answer === '2')
|
|
281
|
+
return here;
|
|
282
|
+
if (answer === '3') {
|
|
283
|
+
const dir = (await rl.question('Project folder: ')).trim().replace(/^~(?=$|\/)/, homedir());
|
|
284
|
+
if (dir && existsSync(dir))
|
|
285
|
+
return resolve(dir);
|
|
286
|
+
console.log(` no folder ${dir || '(empty)'}`);
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
console.log(' type 1, 2 or 3 (Enter: 1)');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
// Ctrl+D (or Ctrl+C) at the question: stop quietly, nothing linked yet.
|
|
294
|
+
if (error.name === 'AbortError') {
|
|
295
|
+
throw new ReelsonError('install cancelled — nothing was changed');
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
rl.close();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
246
303
|
function init(dir = process.cwd()) {
|
|
247
304
|
const target = resolve(dir, 'demo.config.json');
|
|
248
305
|
if (existsSync(target)) {
|
|
@@ -251,8 +308,7 @@ function init(dir = process.cwd()) {
|
|
|
251
308
|
const example = JSON.parse(readFileSync(resolve(KIT_ROOT, 'demo.config.example.json'), 'utf8'));
|
|
252
309
|
delete example.$comment;
|
|
253
310
|
delete example.$schema;
|
|
254
|
-
const
|
|
255
|
-
const schema = existsSync(viaProject) ? viaProject : CONFIG_SCHEMA_PATH;
|
|
311
|
+
const schema = viaProjectSkills(dir, 'reelson-record/schemas/demo.config.schema.json') ?? CONFIG_SCHEMA_PATH;
|
|
256
312
|
let schemaRef = relative(dir, schema);
|
|
257
313
|
if (!/^\.{1,2}\//.test(schemaRef) && !schemaRef.startsWith('/')) {
|
|
258
314
|
schemaRef = `./${schemaRef}`;
|
|
@@ -275,8 +331,7 @@ function create(argv) {
|
|
|
275
331
|
throw new ReelsonError(`${scenario} already exists`);
|
|
276
332
|
}
|
|
277
333
|
mkdirSync(dir, { recursive: true });
|
|
278
|
-
const
|
|
279
|
-
const typesFile = existsSync(viaProject) ? viaProject : resolve(KIT_ROOT, 'skills/reelson-record/scripts/scenario.ts');
|
|
334
|
+
const typesFile = viaProjectSkills(cfg.root, 'reelson-record/scripts/scenario.ts') ?? resolve(KIT_ROOT, 'skills/reelson-record/scripts/scenario.ts');
|
|
280
335
|
let importPath = relative(dir, typesFile);
|
|
281
336
|
if (!/^\.{1,2}\//.test(importPath)) {
|
|
282
337
|
importPath = `./${importPath}`;
|
package/bin/reelson.ts
CHANGED
|
@@ -5,12 +5,13 @@
|
|
|
5
5
|
* demo.config.json (walking up from the working directory).
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from 'node:child_process'
|
|
8
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
8
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
|
|
9
9
|
import { createRequire } from 'node:module'
|
|
10
10
|
import { homedir } from 'node:os'
|
|
11
|
+
import { createInterface } from 'node:readline/promises'
|
|
11
12
|
import { basename, relative, resolve } from 'node:path'
|
|
12
13
|
import { parseArgs, parseEnv } from 'node:util'
|
|
13
|
-
import { CONFIG_SCHEMA_PATH, ConfigError, fromRoot, loadConfig, type LoadedConfig } from '../skills/reelson-record/scripts/config.ts'
|
|
14
|
+
import { CONFIG_SCHEMA_PATH, ConfigError, fromRoot, loadConfig, viaProjectSkills, type LoadedConfig } from '../skills/reelson-record/scripts/config.ts'
|
|
14
15
|
import { doctor } from '../skills/reelson-record/scripts/doctor.ts'
|
|
15
16
|
import { build, plan, type BuildOptions } from '../skills/reelson-compose/scripts/build.ts'
|
|
16
17
|
import { captionCues, toSrt, toVtt } from '../skills/reelson-compose/scripts/captions.ts'
|
|
@@ -27,11 +28,13 @@ const HELP = `reelson — scripted walkthroughs → branded demo videos
|
|
|
27
28
|
|
|
28
29
|
Usage: reelson <command> [options]
|
|
29
30
|
|
|
30
|
-
install [<project-dir>
|
|
31
|
+
install [<project-dir> | --global] [-y] [--no-browser]
|
|
31
32
|
link the reelson-record + reelson-compose skills into
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
.agents/skills/ (Codex and other agents) and .claude/skills/
|
|
34
|
+
(Claude Code): --global in ~/ for every project, or in a
|
|
35
|
+
project (which also gets a demo.config.json); asks which when
|
|
36
|
+
neither is given (-y, or no terminal: --global). Downloads
|
|
37
|
+
Playwright's Chromium (--no-browser: skip it)
|
|
35
38
|
init create demo.config.json in this directory
|
|
36
39
|
doctor check the tools and that the cursor layer lines up here
|
|
37
40
|
new <slug> [--url <origin>] start <videosDir>/<slug>/scenario.ts
|
|
@@ -122,7 +125,7 @@ async function run(cmd: string | undefined, argv: string[]): Promise<number> {
|
|
|
122
125
|
console.log(JSON.parse(readFileSync(resolve(KIT_ROOT, 'package.json'), 'utf8')).version)
|
|
123
126
|
return 0
|
|
124
127
|
case 'install':
|
|
125
|
-
return install(argv)
|
|
128
|
+
return await install(argv)
|
|
126
129
|
case 'init':
|
|
127
130
|
return init()
|
|
128
131
|
case 'doctor':
|
|
@@ -183,22 +186,25 @@ function one(positionals: string[], usage: string): string {
|
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
/**
|
|
186
|
-
* `reelson install`: links the skills into a project (or
|
|
189
|
+
* `reelson install`: links the skills into a project (or the home folder) and prepares the machine. The links
|
|
187
190
|
* point at this installation, so `npm update -g reelson` (or `git pull` in a checkout) updates every project.
|
|
188
191
|
*/
|
|
189
|
-
function install(argv: string[]): number {
|
|
192
|
+
async function install(argv: string[]): Promise<number> {
|
|
190
193
|
const skillNames = ['reelson-record', 'reelson-compose']
|
|
191
194
|
// Names used before 0.7 (demo-* before that): their links are dropped when they point at a reelson install.
|
|
192
195
|
const oldNames = ['demo-record', 'demo-video', 'reelkit-record', 'reelkit-compose']
|
|
193
196
|
const { values, positionals } = parseArgs({
|
|
194
197
|
args: argv,
|
|
195
198
|
allowPositionals: true,
|
|
196
|
-
options: { global: { type: 'boolean' }, 'no-browser': { type: 'boolean' } },
|
|
199
|
+
options: { global: { type: 'boolean' }, yes: { type: 'boolean', short: 'y' }, 'no-browser': { type: 'boolean' } },
|
|
197
200
|
})
|
|
198
201
|
if (positionals.length > 1 || (values.global && positionals.length)) {
|
|
199
|
-
throw new ReelsonError('usage: reelson install [<project-dir> | --global] [--no-browser]')
|
|
202
|
+
throw new ReelsonError('usage: reelson install [<project-dir> | --global] [-y] [--no-browser]')
|
|
200
203
|
}
|
|
201
|
-
|
|
204
|
+
// Asked only when neither --global nor a folder is given and someone is at the terminal.
|
|
205
|
+
const where = values.global ? null : positionals.length ? resolve(positionals[0]) : values.yes || !process.stdin.isTTY ? null : await askWhere()
|
|
206
|
+
const global = where === null
|
|
207
|
+
const target = where ?? homedir()
|
|
202
208
|
if (!existsSync(target)) {
|
|
203
209
|
throw new ReelsonError(`${target} does not exist`)
|
|
204
210
|
}
|
|
@@ -214,45 +220,92 @@ function install(argv: string[]): number {
|
|
|
214
220
|
}
|
|
215
221
|
}
|
|
216
222
|
|
|
217
|
-
|
|
218
|
-
|
|
223
|
+
// .agents/skills links to this installation; .claude/skills links to .agents/skills (unless the
|
|
224
|
+
// whole folder already is a link to it), so both agents see the same skills.
|
|
225
|
+
const agents = resolve(target, '.agents/skills')
|
|
226
|
+
const claude = resolve(target, '.claude/skills')
|
|
227
|
+
mkdirSync(agents, { recursive: true })
|
|
228
|
+
mkdirSync(claude, { recursive: true })
|
|
229
|
+
const shared = realpathSync(agents) === realpathSync(claude)
|
|
219
230
|
const isLink = (path: string): boolean => lstatSync(path, { throwIfNoEntry: false })?.isSymbolicLink() ?? false
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
231
|
+
const link = (path: string, to: string): void => {
|
|
232
|
+
if (existsSync(path) && !isLink(path)) {
|
|
233
|
+
throw new ReelsonError(`${path} is a folder, not a link to reelson — move it away and run install again`)
|
|
234
|
+
}
|
|
235
|
+
if (isLink(path)) {
|
|
236
|
+
rmSync(path)
|
|
237
|
+
}
|
|
238
|
+
symlinkSync(to, path, 'dir')
|
|
239
|
+
console.log(` linked ${path} → ${to}`)
|
|
240
|
+
}
|
|
241
|
+
for (const dir of shared ? [agents] : [agents, claude]) {
|
|
242
|
+
for (const old of oldNames) {
|
|
243
|
+
const path = resolve(dir, old)
|
|
244
|
+
if (isLink(path) && (resolve(dir, readlinkSync(path)).startsWith(`${KIT_ROOT}/`) || /[\\/]skills[\\/](reelkit|reelson)-/.test(readlinkSync(path)))) {
|
|
245
|
+
rmSync(path)
|
|
246
|
+
console.log(` removed old link ${path}`)
|
|
247
|
+
}
|
|
225
248
|
}
|
|
226
249
|
}
|
|
227
250
|
for (const skill of skillNames) {
|
|
228
|
-
|
|
229
|
-
if (
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
if (isLink(link)) {
|
|
233
|
-
rmSync(link)
|
|
251
|
+
link(resolve(agents, skill), resolve(KIT_ROOT, 'skills', skill))
|
|
252
|
+
if (!shared) {
|
|
253
|
+
link(resolve(claude, skill), relative(claude, resolve(agents, skill)))
|
|
234
254
|
}
|
|
235
|
-
symlinkSync(resolve(KIT_ROOT, 'skills', skill), link, 'dir')
|
|
236
|
-
console.log(` linked ${link}`)
|
|
237
255
|
}
|
|
238
256
|
|
|
239
|
-
if (!
|
|
257
|
+
if (!global) {
|
|
240
258
|
if (!existsSync(resolve(target, 'demo.config.json'))) {
|
|
241
259
|
init(target)
|
|
242
260
|
}
|
|
243
261
|
const videos = loadConfig(target).videosDir.replace(/^\.?\/+|\/+$/g, '')
|
|
244
262
|
console.log(`
|
|
245
|
-
Add to ${resolve(target, '.gitignore')}:
|
|
263
|
+
Add to ${resolve(target, '.gitignore')} (the skill links point at this machine's reelson):
|
|
246
264
|
|
|
265
|
+
/.agents/skills/reelson-*
|
|
266
|
+
/.claude/skills/reelson-*
|
|
247
267
|
/${videos}/**/recording*.mp4
|
|
248
268
|
/${videos}/**/.raw*/
|
|
249
269
|
/${videos}/**/video/
|
|
250
270
|
/${videos}/**/*.openscreen`)
|
|
251
271
|
}
|
|
252
|
-
console.log('\nDone. Try: reelson doctor')
|
|
272
|
+
console.log(global ? '\nDone — every project sees the skills. In a project: reelson init, then reelson doctor' : '\nDone. Try: reelson doctor')
|
|
253
273
|
return 0
|
|
254
274
|
}
|
|
255
275
|
|
|
276
|
+
/** `reelson install`'s question: every project (the default), this one, or another folder → null for global, else the project. */
|
|
277
|
+
async function askWhere(): Promise<string | null> {
|
|
278
|
+
const home = (path: string): string => (path.startsWith(homedir()) ? `~${path.slice(homedir().length)}` : path)
|
|
279
|
+
const here = process.cwd()
|
|
280
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
281
|
+
try {
|
|
282
|
+
console.log(`Where should reelson install its skills?
|
|
283
|
+
1. Every project (default): ~/.agents/skills, linked from ~/.claude/skills
|
|
284
|
+
2. This project: ${home(here)}/.agents/skills, linked from .claude/skills
|
|
285
|
+
3. Another project…`)
|
|
286
|
+
for (;;) {
|
|
287
|
+
const answer = (await rl.question('Choose [1]: ')).trim()
|
|
288
|
+
if (answer === '' || answer === '1') return null
|
|
289
|
+
if (answer === '2') return here
|
|
290
|
+
if (answer === '3') {
|
|
291
|
+
const dir = (await rl.question('Project folder: ')).trim().replace(/^~(?=$|\/)/, homedir())
|
|
292
|
+
if (dir && existsSync(dir)) return resolve(dir)
|
|
293
|
+
console.log(` no folder ${dir || '(empty)'}`)
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
console.log(' type 1, 2 or 3 (Enter: 1)')
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
// Ctrl+D (or Ctrl+C) at the question: stop quietly, nothing linked yet.
|
|
300
|
+
if ((error as Error).name === 'AbortError') {
|
|
301
|
+
throw new ReelsonError('install cancelled — nothing was changed')
|
|
302
|
+
}
|
|
303
|
+
throw error
|
|
304
|
+
} finally {
|
|
305
|
+
rl.close()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
256
309
|
function init(dir = process.cwd()): number {
|
|
257
310
|
const target = resolve(dir, 'demo.config.json')
|
|
258
311
|
if (existsSync(target)) {
|
|
@@ -261,8 +314,7 @@ function init(dir = process.cwd()): number {
|
|
|
261
314
|
const example = JSON.parse(readFileSync(resolve(KIT_ROOT, 'demo.config.example.json'), 'utf8'))
|
|
262
315
|
delete example.$comment
|
|
263
316
|
delete example.$schema
|
|
264
|
-
const
|
|
265
|
-
const schema = existsSync(viaProject) ? viaProject : CONFIG_SCHEMA_PATH
|
|
317
|
+
const schema = viaProjectSkills(dir, 'reelson-record/schemas/demo.config.schema.json') ?? CONFIG_SCHEMA_PATH
|
|
266
318
|
let schemaRef = relative(dir, schema)
|
|
267
319
|
if (!/^\.{1,2}\//.test(schemaRef) && !schemaRef.startsWith('/')) {
|
|
268
320
|
schemaRef = `./${schemaRef}`
|
|
@@ -286,8 +338,8 @@ function create(argv: string[]): number {
|
|
|
286
338
|
throw new ReelsonError(`${scenario} already exists`)
|
|
287
339
|
}
|
|
288
340
|
mkdirSync(dir, { recursive: true })
|
|
289
|
-
const
|
|
290
|
-
|
|
341
|
+
const typesFile =
|
|
342
|
+
viaProjectSkills(cfg.root, 'reelson-record/scripts/scenario.ts') ?? resolve(KIT_ROOT, 'skills/reelson-record/scripts/scenario.ts')
|
|
291
343
|
let importPath = relative(dir, typesFile)
|
|
292
344
|
if (!/^\.{1,2}\//.test(importPath)) {
|
|
293
345
|
importPath = `./${importPath}`
|
package/docs/prompting.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# How to ask for a video
|
|
2
2
|
|
|
3
3
|
The prompt is the spec. A complete prompt has six parts; the more you give, the fewer guesses
|
|
4
|
-
|
|
4
|
+
the agent makes and the fewer re-renders you need.
|
|
5
5
|
|
|
6
6
|
```
|
|
7
7
|
Make a demo video of <FEATURE>.
|
|
@@ -14,7 +14,7 @@ Emphasis: zoom on <what> during step <n> only where the UI is small
|
|
|
14
14
|
Output: mp4 (default) / also gif slug: <kebab-case>
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The agent writes the scenario, records, verifies frames, builds the composition, words the
|
|
18
18
|
callouts in video.json, checks, renders, and reports the file path with a few extracted frames.
|
|
19
19
|
|
|
20
20
|
## Example prompts
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reelson",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Claude Code
|
|
5
|
+
"description": "Agent Skills (Claude Code, Codex, …) + CLI that turn a scripted Playwright walkthrough into a branded HyperFrames demo video.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"demo-video",
|
|
8
8
|
"screencast",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"claude-code",
|
|
12
12
|
"skills",
|
|
13
13
|
"product-demo",
|
|
14
|
-
"video"
|
|
14
|
+
"video",
|
|
15
|
+
"agent-skills",
|
|
16
|
+
"codex"
|
|
15
17
|
],
|
|
16
18
|
"license": "MIT",
|
|
17
19
|
"author": "Ion Caliman <icaliman92@gmail.com>",
|
|
@@ -6,7 +6,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
7
7
|
import { basename, dirname, extname, relative, resolve } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { fromRoot } from "../../reelson-record/scripts/config.js";
|
|
9
|
+
import { fromRoot, viaProjectSkills } from "../../reelson-record/scripts/config.js";
|
|
10
10
|
import { closest, loadSchema, validate } from "../../reelson-record/scripts/validate.js";
|
|
11
11
|
import { SECTION_TIMING, SLOTS, STAGE_TIMING, } from "./timeline.js";
|
|
12
12
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
@@ -81,12 +81,12 @@ export function validateVideoSpec(raw, where) {
|
|
|
81
81
|
return raw;
|
|
82
82
|
}
|
|
83
83
|
/**
|
|
84
|
-
* `$schema` for a new video.json: through the project's .
|
|
85
|
-
* when
|
|
84
|
+
* `$schema` for a new video.json: through the project's skill link (.agents/skills or
|
|
85
|
+
* .claude/skills) when it points at this reelson (portable), else straight to the kit.
|
|
86
86
|
*/
|
|
87
87
|
export function videoSchemaRef(demoDir, config) {
|
|
88
|
-
const viaProject =
|
|
89
|
-
const target =
|
|
88
|
+
const viaProject = viaProjectSkills(config.root, 'reelson-compose/schemas/video.schema.json');
|
|
89
|
+
const target = viaProject && realpathSync(viaProject) === realpathSync(VIDEO_SCHEMA_PATH)
|
|
90
90
|
? viaProject
|
|
91
91
|
: VIDEO_SCHEMA_PATH;
|
|
92
92
|
return relative(demoDir, target);
|
|
@@ -6,7 +6,7 @@ import { spawnSync } from 'node:child_process'
|
|
|
6
6
|
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs'
|
|
7
7
|
import { basename, dirname, extname, relative, resolve } from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
|
-
import { fromRoot, type LoadedConfig } from '../../reelson-record/scripts/config.ts'
|
|
9
|
+
import { fromRoot, viaProjectSkills, type LoadedConfig } from '../../reelson-record/scripts/config.ts'
|
|
10
10
|
import { closest, loadSchema, validate } from '../../reelson-record/scripts/validate.ts'
|
|
11
11
|
import type { SectionSource } from './composition.ts'
|
|
12
12
|
import {
|
|
@@ -102,13 +102,13 @@ export function validateVideoSpec(raw: unknown, where: string): VideoSpec {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
* `$schema` for a new video.json: through the project's .
|
|
106
|
-
* when
|
|
105
|
+
* `$schema` for a new video.json: through the project's skill link (.agents/skills or
|
|
106
|
+
* .claude/skills) when it points at this reelson (portable), else straight to the kit.
|
|
107
107
|
*/
|
|
108
108
|
export function videoSchemaRef(demoDir: string, config: LoadedConfig): string {
|
|
109
|
-
const viaProject =
|
|
109
|
+
const viaProject = viaProjectSkills(config.root, 'reelson-compose/schemas/video.schema.json')
|
|
110
110
|
const target =
|
|
111
|
-
|
|
111
|
+
viaProject && realpathSync(viaProject) === realpathSync(VIDEO_SCHEMA_PATH)
|
|
112
112
|
? viaProject
|
|
113
113
|
: VIDEO_SCHEMA_PATH
|
|
114
114
|
|
|
@@ -33,7 +33,7 @@ async function load() {
|
|
|
33
33
|
if (!body.data) return
|
|
34
34
|
const first = !data
|
|
35
35
|
data = body.data
|
|
36
|
-
// Undo writes back a whole earlier video.json; after a change from elsewhere (
|
|
36
|
+
// Undo writes back a whole earlier video.json; after a change from elsewhere (the agent, an
|
|
37
37
|
// editor) that would silently revert it, so the history starts over.
|
|
38
38
|
if (!first && body.revision !== revision && (history.undo.length || history.redo.length)) {
|
|
39
39
|
history.undo = []
|
|
@@ -75,8 +75,9 @@ Common dev overlays (Laravel Debugbar, Vite/Next.js/webpack error overlays) are
|
|
|
75
75
|
## Prerequisites
|
|
76
76
|
|
|
77
77
|
- The app running and reachable at the scenario's `baseURL`.
|
|
78
|
-
- reelson installed once per machine (`npm install -g reelson`, then `reelson install`
|
|
79
|
-
project: Playwright's Chromium and the skill
|
|
78
|
+
- reelson installed once per machine (`npm install -g reelson`, then `reelson install --global`
|
|
79
|
+
for every project or `reelson install <project>` for one: Playwright's Chromium and the skill
|
|
80
|
+
links; a kit checkout's `install.sh` does both). Pass one of them: without either it asks the user.
|
|
80
81
|
- `ffmpeg` on PATH (`brew install ffmpeg`). Node 22.18+ (TypeScript runs directly).
|
|
81
82
|
- `reelson doctor` checks the tools and films a local test page to confirm the cursor layer
|
|
82
83
|
lines up with the footage on this machine (offset and px error; run it on a new machine).
|
|
@@ -85,7 +86,7 @@ Common dev overlays (Laravel Debugbar, Vite/Next.js/webpack error overlays) are
|
|
|
85
86
|
|
|
86
87
|
1. **Pick the origin, panel and account.** Look for existing e2e helpers in the project
|
|
87
88
|
(login, table/modal locators) and reuse them from the scenario — a scenario is mostly an
|
|
88
|
-
e2e test with a cursor. Check the project's CLAUDE.md / e2e docs for seeded accounts.
|
|
89
|
+
e2e test with a cursor. Check the project's AGENTS.md / CLAUDE.md / e2e docs for seeded accounts.
|
|
89
90
|
|
|
90
91
|
2. **Write `<videosDir>/<slug>/scenario.ts`.** `reelson new <slug> --url <origin>` creates a
|
|
91
92
|
stub with the right type import; the kit's `examples/todo-add-item/scenario.ts` is a full
|
|
@@ -113,6 +113,21 @@ export function countLabel(count, label, language) {
|
|
|
113
113
|
return `${count} ${label[category] ?? label.other}`;
|
|
114
114
|
}
|
|
115
115
|
/** Resolves a path from the config against the project root. */
|
|
116
|
+
/**
|
|
117
|
+
* Where a project links the skills, in order: .agents/skills (Codex and the other agents that read
|
|
118
|
+
* Agent Skills), then .claude/skills (Claude Code; `reelson install` links it to the first).
|
|
119
|
+
*/
|
|
120
|
+
export const SKILL_DIRS = ['.agents/skills', '.claude/skills'];
|
|
121
|
+
/** `<skill>/<path>` inside the project's first skill folder that has it (a portable path to commit), else null. */
|
|
122
|
+
export function viaProjectSkills(root, path) {
|
|
123
|
+
for (const dir of SKILL_DIRS) {
|
|
124
|
+
const candidate = resolve(root, dir, path);
|
|
125
|
+
if (existsSync(candidate)) {
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
116
131
|
export function fromRoot(config, path) {
|
|
117
132
|
return isAbsolute(path) ? path : resolve(config.root, path);
|
|
118
133
|
}
|
|
@@ -225,6 +225,23 @@ export function countLabel(count: number, label: Label, language: string): strin
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
/** Resolves a path from the config against the project root. */
|
|
228
|
+
/**
|
|
229
|
+
* Where a project links the skills, in order: .agents/skills (Codex and the other agents that read
|
|
230
|
+
* Agent Skills), then .claude/skills (Claude Code; `reelson install` links it to the first).
|
|
231
|
+
*/
|
|
232
|
+
export const SKILL_DIRS = ['.agents/skills', '.claude/skills']
|
|
233
|
+
|
|
234
|
+
/** `<skill>/<path>` inside the project's first skill folder that has it (a portable path to commit), else null. */
|
|
235
|
+
export function viaProjectSkills(root: string, path: string): string | null {
|
|
236
|
+
for (const dir of SKILL_DIRS) {
|
|
237
|
+
const candidate = resolve(root, dir, path)
|
|
238
|
+
if (existsSync(candidate)) {
|
|
239
|
+
return candidate
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return null
|
|
243
|
+
}
|
|
244
|
+
|
|
228
245
|
export function fromRoot(config: LoadedConfig, path: string): string {
|
|
229
246
|
return isAbsolute(path) ? path : resolve(config.root, path)
|
|
230
247
|
}
|
package/docs/demo.webp
DELETED
|
Binary file
|