demowalk 0.0.0-stage → 1.0.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/CLAUDE.md +67 -0
- package/LICENSE +21 -0
- package/README.md +213 -2
- package/docs/style-rules.md +61 -0
- package/package.json +57 -4
- package/scripts/init.mjs +153 -0
- package/src/assets.ts +17 -0
- package/src/capture.ts +352 -0
- package/src/index.ts +15 -0
- package/src/overlay.ts +315 -0
- package/src/run.ts +146 -0
- package/src/stitch.ts +106 -0
- package/src/tts.ts +49 -0
- package/src/types.ts +188 -0
- package/templates/demo.config.ts +28 -0
- package/templates/journeys/example.ts +39 -0
- package/templates/run.ts +8 -0
- package/templates/tsconfig.json +13 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Authoring journeys with Claude
|
|
2
|
+
|
|
3
|
+
This file is the instruction set for any Claude session asked to create or
|
|
4
|
+
edit a walkthrough video in a host repo. The human gives only a **start
|
|
5
|
+
point and an end point** ("start from login, end at partner listing") —
|
|
6
|
+
everything else below is YOUR job, not theirs.
|
|
7
|
+
|
|
8
|
+
## The contract
|
|
9
|
+
|
|
10
|
+
1. **Read `docs/style-rules.md` in this package first** — the 19 visual
|
|
11
|
+
style rules bind every journey. Do not re-learn them from review rounds;
|
|
12
|
+
start compliant.
|
|
13
|
+
2. **The human never lists what to blur.** The engine blurs all dynamic data
|
|
14
|
+
universally; you add only generic patterns (label→value pairs on detail
|
|
15
|
+
pages, checkbox-name siblings, selection-card text). If they want
|
|
16
|
+
something readable, they'll say so → use `keepVisible` (one value) or
|
|
17
|
+
`noUniversalRedact` (whole frame, rare).
|
|
18
|
+
3. **Journeys create nothing** (rule 18): fill real forms, ring the final
|
|
19
|
+
create/submit button, narrate the completion — never click it. Show the
|
|
20
|
+
result on an existing record with everything blurred.
|
|
21
|
+
|
|
22
|
+
## How to auto-write a journey from "start X, end Y"
|
|
23
|
+
|
|
24
|
+
1. **Mine the host repo — you have full access:**
|
|
25
|
+
- `docs/<module>.md` — each module's doc explains what every screen,
|
|
26
|
+
field and button actually does. This is your narration source of truth.
|
|
27
|
+
- `docs/user-guide/guide.config.json` + `mockups.mjs` — the sections,
|
|
28
|
+
step order and per-screen elements the team already documents for
|
|
29
|
+
users. A journey should cover the same ground in the same order.
|
|
30
|
+
- The screen's source (`src/screen/<Module>/`) — for exact field labels,
|
|
31
|
+
placeholders (your selectors), conditional sections, and validations.
|
|
32
|
+
2. **Cover EVERYTHING on the path.** Walk the real screen top-to-bottom and
|
|
33
|
+
give every field/section its moment — name, then subdomain, then type,
|
|
34
|
+
then each conditional section as it appears. Missing a field the screen
|
|
35
|
+
shows is a review bounce; the human should never have to say "you skipped
|
|
36
|
+
X".
|
|
37
|
+
3. **Detect the product's own tour/onboarding.** Search the host for
|
|
38
|
+
ProductTour / onboarding flags (e.g. `has_completed_onboarding`). Where
|
|
39
|
+
the product draws its own tour, trigger it (patch the flag in the demo
|
|
40
|
+
browser's API response — never in the app) and use `protectOnly` frames:
|
|
41
|
+
the product's backdrop is the only dim in the whole video (rule 4).
|
|
42
|
+
4. **Narration voice:** present tense, "you/your", one or two sentences per
|
|
43
|
+
frame, say what + why, end transition steps with the click to make
|
|
44
|
+
("Click Next", "click Continue"). Opening frame = welcome + what the
|
|
45
|
+
video covers; closing frame = one-line recap + "thanks for watching".
|
|
46
|
+
5. **Selectors:** accessible (labels, placeholders, roles) — never
|
|
47
|
+
page-wide text or nth() guesses (rule 6); scope through section titles
|
|
48
|
+
and siblings. Measurement flakiness is already engine-handled.
|
|
49
|
+
6. **Emails/data:** all typed values come from the journey's `data` object —
|
|
50
|
+
fictional names, plus-tagged owned mailboxes if an address could ever
|
|
51
|
+
receive mail (rule 19), fictional addresses/phones (555-01xx).
|
|
52
|
+
|
|
53
|
+
## Workflow with the human
|
|
54
|
+
|
|
55
|
+
1. They give start + end (+ anything special to keep visible).
|
|
56
|
+
2. You write `demo/journeys/<id>.ts` + run once; the storyboard
|
|
57
|
+
`demo/journeys/<id>.md` regenerates automatically — share it for review
|
|
58
|
+
of the narration before they even watch the video.
|
|
59
|
+
3. They watch, send screenshots; you fix. Engine-level issues get fixed in
|
|
60
|
+
THIS package (every portal inherits); journey issues in the journey file.
|
|
61
|
+
|
|
62
|
+
## Debugging a failed run
|
|
63
|
+
|
|
64
|
+
The run stops loudly with the step id, the page URL, any on-screen toast
|
|
65
|
+
text and a debug frame path. That is the diagnosis — read it before touching
|
|
66
|
+
code. A "spotlight target not found" after a product change is the tool
|
|
67
|
+
working as designed: update that one step and re-run.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 360 Solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,214 @@
|
|
|
1
|
-
#
|
|
1
|
+
# demowalk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Self-regenerating product demo videos.** One command rebuilds a narrated
|
|
4
|
+
walkthrough **from your live app**: Playwright drives the real UI, every
|
|
5
|
+
frame is styled in your own branding (spotlight rings, captions, and
|
|
6
|
+
skeleton-style redaction of all dynamic data), narration is voiced, and
|
|
7
|
+
ffmpeg stitches the final MP4.
|
|
8
|
+
|
|
9
|
+
When the product changes, you re-run the command — nothing is recorded by
|
|
10
|
+
hand, so nothing can silently go stale.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm run demo:video # → demo/out/example/example.mp4
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- **Always current** — a UI change means a re-run, not a re-shoot.
|
|
17
|
+
- **Privacy-safe by construction** — every filled input, table cell and
|
|
18
|
+
record value is redacted on every frame, by the engine. Raw captures are
|
|
19
|
+
deleted after every render.
|
|
20
|
+
- **Your branding, not ours** — ring, chip and redaction colors come from
|
|
21
|
+
each project's config; the engine ships neutral fallbacks only.
|
|
22
|
+
- **Fails loudly, never lies** — a missing element stops the run with the
|
|
23
|
+
step name, URL and a debug frame. A wrong video cannot be produced silently.
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+
Your repo keeps two inputs; demowalk turns them into a video in four stages:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
demo/journeys/<id>.ts demo/demo.config.ts
|
|
31
|
+
steps + narration text baseUrl · auth hook · brand colors
|
|
32
|
+
│ │
|
|
33
|
+
└──────────────────┬───────────────────┘
|
|
34
|
+
▼
|
|
35
|
+
┌─────────────────────┐
|
|
36
|
+
your app ◄── │ 1 CAPTURE │ signs in with your e2e fixture,
|
|
37
|
+
(localhost) │ Playwright │ walks the steps, shoots 2× retina
|
|
38
|
+
└──────────┬──────────┘ frames + element geometry
|
|
39
|
+
▼
|
|
40
|
+
┌─────────────────────┐
|
|
41
|
+
│ 2 OVERLAY │ redacts every dynamic value into
|
|
42
|
+
│ sharp │ skeleton bars; draws rings, chips
|
|
43
|
+
└──────────┬──────────┘ and captions in YOUR colors
|
|
44
|
+
▼
|
|
45
|
+
┌─────────────────────┐
|
|
46
|
+
│ 3 NARRATE │ voices each step's narration
|
|
47
|
+
│ say → ffmpeg │ into one audio clip
|
|
48
|
+
└──────────┬──────────┘
|
|
49
|
+
▼
|
|
50
|
+
┌─────────────────────┐
|
|
51
|
+
│ 4 STITCH │ per-step segments pinned to the
|
|
52
|
+
│ ffmpeg │ audio length → one seamless MP4
|
|
53
|
+
└──────────┬──────────┘
|
|
54
|
+
▼
|
|
55
|
+
demo/out/<id>/<id>.mp4
|
|
56
|
+
+ demo/journeys/<id>.md (storyboard, regenerated each run)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The engine, starter templates and setup CLI live in this package; journeys
|
|
60
|
+
(one `.ts` file per video) live in your repo.
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
| Required | Why |
|
|
65
|
+
|---|---|
|
|
66
|
+
| Node 18+ and `tsx` | runs the pipeline |
|
|
67
|
+
| `@playwright/test` + Chromium | the capture engine (peer dependency) |
|
|
68
|
+
| ffmpeg on the machine | narration conversion + stitching |
|
|
69
|
+
| **macOS** (for now) | narration uses the built-in `say`; download a natural voice once via System Settings → Accessibility → Spoken Content → Manage Voices (e.g. "Tom (Enhanced)") |
|
|
70
|
+
| A sign-in function | the only app-specific code — repos with e2e tests reuse their auth fixture |
|
|
71
|
+
| The app running locally | something to film |
|
|
72
|
+
|
|
73
|
+
Docs, an e2e suite and test-ids are **not** required — they only make
|
|
74
|
+
journeys easier to author or redaction richer when present.
|
|
75
|
+
|
|
76
|
+
## Quick start
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# 0. ffmpeg (once per machine — check first: `ffmpeg -version`).
|
|
80
|
+
# The 2-minute static build beats compiling via brew:
|
|
81
|
+
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip \
|
|
82
|
+
&& unzip -o /tmp/ffmpeg.zip -d /tmp \
|
|
83
|
+
&& sudo mv /tmp/ffmpeg /usr/local/bin/ffmpeg \
|
|
84
|
+
&& sudo xattr -d com.apple.quarantine /usr/local/bin/ffmpeg 2>/dev/null; ffmpeg -version
|
|
85
|
+
|
|
86
|
+
# 1. in your app's repo:
|
|
87
|
+
npm install --save-dev tsx
|
|
88
|
+
npm install --save-dev demowalk
|
|
89
|
+
# (straight from GitHub instead: npm i -D demowalk@github:360solutions-dev/demowalk
|
|
90
|
+
# from a local clone: npm i -D --install-links file:../demowalk
|
|
91
|
+
# --install-links matters — a symlinked file: install duplicates
|
|
92
|
+
# Playwright's types and breaks tsc)
|
|
93
|
+
|
|
94
|
+
# 2. only if the repo doesn't already use Playwright:
|
|
95
|
+
npm install --save-dev @playwright/test && npx playwright install chromium
|
|
96
|
+
|
|
97
|
+
# 3. let demowalk set itself up (scripts + templates + gitignore, idempotent):
|
|
98
|
+
npx demowalk-init
|
|
99
|
+
|
|
100
|
+
# 4. review demo/demo.config.ts — init AUTO-WIRES it when the repo has the
|
|
101
|
+
# common e2e convention (tests/global-setup.ts + tests/fixtures/
|
|
102
|
+
# auth.fixture.ts), detecting the dev port and theme colors too.
|
|
103
|
+
# Otherwise wire the marked spots by hand (the env-loading setup import
|
|
104
|
+
# comes FIRST — the auth fixture reads env when it is imported).
|
|
105
|
+
|
|
106
|
+
# 5. smoke test — with the app + backend running locally:
|
|
107
|
+
npm run demo:video
|
|
108
|
+
open demo/out/example/example.mp4
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Your repo keeps **three small files**:
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
demo/
|
|
115
|
+
├── demo.config.ts # THE app-specific file: baseUrl, auth hook, branding
|
|
116
|
+
├── run.ts # 3-line shim
|
|
117
|
+
└── journeys/<id>.ts # the videos — one file per video
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Day-to-day:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm run demo:video -- --journey=<id> # build one video
|
|
124
|
+
npm run demo:video -- --from=overlay # re-style an existing capture (also: tts, stitch)
|
|
125
|
+
npm run demo:video -- --keep-frames # keep raw frames while iterating on styling
|
|
126
|
+
npm run demo:update # pull a newer engine into this repo
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Creating a journey
|
|
130
|
+
|
|
131
|
+
Journeys are **authored, not configured** — usually by an AI coding
|
|
132
|
+
assistant: give it the **start and end point** ("start from login, end at the
|
|
133
|
+
created record") and it writes `demo/journeys/<id>.ts` following
|
|
134
|
+
[`CLAUDE.md`](./CLAUDE.md) — mining your docs for narration, covering every
|
|
135
|
+
field on the path, and applying the
|
|
136
|
+
[visual style rules](./docs/style-rules.md) that bind every journey.
|
|
137
|
+
|
|
138
|
+
Every run regenerates a storyboard (`demo/journeys/<id>.md`) with each step's
|
|
139
|
+
narration as text — review the script without watching the video.
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
demowalk has **no .env of its own**: real configuration lives in
|
|
144
|
+
`demo/demo.config.ts` (typed, committed). Env vars are per-run overrides only:
|
|
145
|
+
|
|
146
|
+
| Variable | Meaning | Default |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `BASE_URL` | Override the base URL for one run | from config |
|
|
149
|
+
| `DEMO_VOICE` | Override the macOS `say` voice | from config |
|
|
150
|
+
| `DEMO_VOICE_RATE` | Words per minute | from config / 165 |
|
|
151
|
+
| `DEMO_ALLOW_REMOTE` | `1` permits a non-localhost base URL (explicit opt-in) | off |
|
|
152
|
+
|
|
153
|
+
**Auth env belongs to the host, not to demowalk** — the pipeline signs in
|
|
154
|
+
through your repo's own e2e fixture, so whatever your e2e suite needs is all
|
|
155
|
+
it needs. If `npm run e2e` works on a machine, `npm run demo:video` has
|
|
156
|
+
everything besides ffmpeg and the running local app.
|
|
157
|
+
|
|
158
|
+
Branding is per project — each config passes its own theme's values:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import type { DemoConfig } from "demowalk";
|
|
162
|
+
import globalSetup from "../tests/global-setup";
|
|
163
|
+
import { createAuthContext } from "../tests/fixtures/auth.fixture";
|
|
164
|
+
|
|
165
|
+
const config: DemoConfig = {
|
|
166
|
+
baseUrl: process.env.BASE_URL ?? "http://localhost:3000",
|
|
167
|
+
prepare: () => globalSetup(), // host e2e setup: env + preflight
|
|
168
|
+
createAuthContext, // the host's own signed-in context
|
|
169
|
+
ringColor: "#2F6FEB", // spotlight ring (theme primary)
|
|
170
|
+
chipColor: "#1F2937", // chips + caption (theme secondary)
|
|
171
|
+
redactionTint: "#E2E5E9", // skeleton grey (theme placeholder tint)
|
|
172
|
+
voice: "Tom (Enhanced)",
|
|
173
|
+
};
|
|
174
|
+
export default config;
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Privacy posture
|
|
178
|
+
|
|
179
|
+
Designed for products whose screens hold sensitive data (including
|
|
180
|
+
regulated/health data):
|
|
181
|
+
|
|
182
|
+
- **Localhost-only by default** — `DEMO_ALLOW_REMOTE=1` is an explicit opt-in.
|
|
183
|
+
- **Journeys create nothing and send nothing** — the final create/submit is
|
|
184
|
+
ringed and narrated, never clicked.
|
|
185
|
+
- **All dynamic data is redacted by the engine** on every frame — no step
|
|
186
|
+
opts in, no frame can be forgotten; raw captures never persist.
|
|
187
|
+
- Output folders are gitignored; distribute videos only through your own
|
|
188
|
+
storage behind your product's auth.
|
|
189
|
+
|
|
190
|
+
The full frame-styling contract lives in
|
|
191
|
+
[docs/style-rules.md](./docs/style-rules.md).
|
|
192
|
+
|
|
193
|
+
## Updating
|
|
194
|
+
|
|
195
|
+
In a host repo: `npm run demo:update` (set up by `demowalk-init`). Journeys
|
|
196
|
+
and config stay untouched; only the engine updates.
|
|
197
|
+
|
|
198
|
+
## Contributing
|
|
199
|
+
|
|
200
|
+
PRs welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for setup, code
|
|
201
|
+
layout, style and the PR workflow.
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
git clone https://github.com/360solutions-dev/demowalk.git
|
|
205
|
+
cd demowalk && npm install && npm run typecheck # no build step
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT. See [LICENSE](./LICENSE).
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
*Made for teams tired of re-recording demo videos every sprint.*
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Visual style rules
|
|
2
|
+
|
|
3
|
+
These rules bind **every** journey. They were set by frame-by-frame product
|
|
4
|
+
review; journey authors — human or AI (see [`CLAUDE.md`](../CLAUDE.md)) —
|
|
5
|
+
follow all of them. A behavior change that affects frames must update these
|
|
6
|
+
rules in the same PR.
|
|
7
|
+
|
|
8
|
+
1. **Rings + label chips are for MAIN CONCEPTS only**: buttons the viewer
|
|
9
|
+
must click, navigation items, wizard steppers, dialogs, a module's key
|
|
10
|
+
introduction. **Never on a plain section heading** — captions narrate those.
|
|
11
|
+
2. **Form fields get NO ring and NO chip** — the app's own focus border is
|
|
12
|
+
the highlight. Fill fields ONE PER FRAME, narrated one at a time.
|
|
13
|
+
3. **Selection cards get NO ring** — the app's selected-state border is the
|
|
14
|
+
highlight.
|
|
15
|
+
4. **No dim overlay ever** — the only dimming is the product's own tour
|
|
16
|
+
backdrop (`protectOnly` frames show the product's callout alone).
|
|
17
|
+
5. **Redact values only, skeleton-soft**: row contents not tables, numbers
|
|
18
|
+
not stat cards, typed text inside fields not whole fields, card text not
|
|
19
|
+
cards, nav icon+label not the whole sidebar. Never a whole panel/screen.
|
|
20
|
+
6. **Ring geometry**: wrap the WHOLE element group via scoped
|
|
21
|
+
ancestors/siblings. **Never union with page-wide locators** (a page-wide
|
|
22
|
+
`input.last()` once grabbed a below-the-fold checkbox; a page-wide text
|
|
23
|
+
match once ringed a sidebar nav item instead of the section).
|
|
24
|
+
7. **The current screen's own nav items stay readable** (URL-driven); a
|
|
25
|
+
navigation step `protect`s its subject.
|
|
26
|
+
8. **After an in-app tour finishes, scroll back to the top.**
|
|
27
|
+
9. **Every typed form value is redacted** (engine-enforced); `keepVisible` is
|
|
28
|
+
the explicit opt-out for the value a step itself teaches.
|
|
29
|
+
10. **No frame ever shows a toast** (engine-enforced).
|
|
30
|
+
11. **A "record created" listing frame** rings the WHOLE row with ALL values
|
|
31
|
+
redacted — the story is "your record appears here", never the data. The
|
|
32
|
+
quick-actions (⋮) menu gets its own `protect`ed step.
|
|
33
|
+
12. **Detail/review screens redact EVERY printed value** (generic
|
|
34
|
+
label→value patterns; names in breadcrumbs and headings included) —
|
|
35
|
+
labels and section titles stay readable.
|
|
36
|
+
13. **App UI text is never redacted** — field labels, checkbox labels, button
|
|
37
|
+
text, column headers. Redaction is for DATA only.
|
|
38
|
+
14. **The opening frame of a journey is clean** — welcome narration over the
|
|
39
|
+
untouched screen; the first highlight arrives with the first action.
|
|
40
|
+
15. **A step without a spotlight scrolls explicitly** to what its narration
|
|
41
|
+
describes.
|
|
42
|
+
16. **Raw frames are deleted after every successful render**
|
|
43
|
+
(engine-enforced; `--keep-frames` only while styling).
|
|
44
|
+
17. **Dynamic data is redacted UNIVERSALLY, in the engine** — step `redact`s
|
|
45
|
+
only ADD patterns. If a value shows, fix the engine or a generic pattern,
|
|
46
|
+
never a one-off.
|
|
47
|
+
18. **Journeys create NOTHING**: never click the final create/submit — with
|
|
48
|
+
all rows redacted, a ringed existing record tells the identical story. No
|
|
49
|
+
records, no emails, no cleanup, no orphan users.
|
|
50
|
+
19. **Any email a flow WOULD send** must target a plus-tagged mailbox the
|
|
51
|
+
team owns — never a fictional-but-real-looking domain.
|
|
52
|
+
|
|
53
|
+
## Step-level escape hatches
|
|
54
|
+
|
|
55
|
+
- `redact` — ADD generic redaction patterns for one step.
|
|
56
|
+
- `keepVisible` — "don't redact this one thing — it's the value the step
|
|
57
|
+
teaches."
|
|
58
|
+
- `noUniversalRedact` — whole frame clean; rare and deliberate.
|
|
59
|
+
- `protect` / `protectOnly` — re-paste an element sharp over the redactions
|
|
60
|
+
(`protectOnly`: and show nothing else highlighted). See `src/types.ts` for
|
|
61
|
+
the full API.
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "demowalk",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Self-regenerating product demo videos: Playwright walks your live app, every frame is branded and redacted, narration is voiced, ffmpeg stitches the MP4. One command, always up to date.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"demo",
|
|
7
|
+
"video",
|
|
8
|
+
"walkthrough",
|
|
9
|
+
"playwright",
|
|
10
|
+
"screen-recording",
|
|
11
|
+
"product-tour",
|
|
12
|
+
"documentation",
|
|
13
|
+
"tts",
|
|
14
|
+
"ffmpeg"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/360solutions-dev/demowalk#readme",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/360solutions-dev/demowalk.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/360solutions-dev/demowalk/issues"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"author": "360 Solutions",
|
|
26
|
+
"type": "module",
|
|
27
|
+
"main": "src/index.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": "./src/index.ts"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"src",
|
|
33
|
+
"templates",
|
|
34
|
+
"scripts",
|
|
35
|
+
"docs",
|
|
36
|
+
"README.md",
|
|
37
|
+
"CLAUDE.md"
|
|
38
|
+
],
|
|
39
|
+
"bin": {
|
|
40
|
+
"demowalk-init": "./scripts/init.mjs"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"typecheck": "tsc --noEmit"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"node": ">=18"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@playwright/test": ">=1.40.0"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"sharp": "^0.35.5"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@playwright/test": "^1.49.0",
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"typescript": "^5.6.0"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/scripts/init.mjs
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-command host setup: `npx demowalk-init` (run from the host repo root
|
|
4
|
+
* after installing the kit). Idempotent — safe to run again any time.
|
|
5
|
+
*
|
|
6
|
+
* 1. adds the demo:video / demo:update scripts to the host package.json
|
|
7
|
+
* 2. copies the starter templates to demo/ (only if demo/ doesn't exist)
|
|
8
|
+
* 3. adds demo/out/ to .gitignore
|
|
9
|
+
* 4. prints exactly what is left to do by hand (wire auth + branding)
|
|
10
|
+
*
|
|
11
|
+
* Exists because a manual install kept missing step 3 ("Missing script:
|
|
12
|
+
* demo:video") — the kit now does every mechanical step itself.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from "fs";
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
|
|
18
|
+
const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const hostRoot = process.cwd();
|
|
20
|
+
|
|
21
|
+
const pkgPath = path.join(hostRoot, "package.json");
|
|
22
|
+
if (!fs.existsSync(pkgPath)) {
|
|
23
|
+
console.error("[demowalk-init] run this from the host repo root (no package.json here).");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 1. scripts
|
|
28
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
29
|
+
pkg.scripts = pkg.scripts ?? {};
|
|
30
|
+
pkg.scripts["demo:video"] = "tsx demo/run.ts";
|
|
31
|
+
// demo:update matches HOW this host installed demowalk: a local folder install
|
|
32
|
+
// keeps the forced --install-links copy; a registry/GitHub install just
|
|
33
|
+
// reinstalls the spec. (The rm -rf is what defeats npm's "up to date" cache.)
|
|
34
|
+
const spec =
|
|
35
|
+
pkg.devDependencies?.demowalk ?? pkg.dependencies?.demowalk ?? "file:../demowalk";
|
|
36
|
+
pkg.scripts["demo:update"] = spec.startsWith("file:")
|
|
37
|
+
? `rm -rf node_modules/demowalk && npm install --save-dev --install-links ${spec}`
|
|
38
|
+
: "rm -rf node_modules/demowalk && npm install";
|
|
39
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
40
|
+
console.log("✓ scripts: demo:video, demo:update");
|
|
41
|
+
|
|
42
|
+
if (!pkg.devDependencies?.tsx && !pkg.dependencies?.tsx) {
|
|
43
|
+
console.log("⚠ tsx is not installed — run: npm install --save-dev tsx");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 1.5 AUTO-WIRE demo.config.ts when the host follows the common e2e
|
|
47
|
+
// convention (tests/global-setup.ts default export + tests/fixtures/
|
|
48
|
+
// auth.fixture.ts exporting createAuthContext). Detection also reads the
|
|
49
|
+
// dev port from the dev script and the theme's primary/secondary colours —
|
|
50
|
+
// so on a conventional repo, init produces a config that RUNS, with zero
|
|
51
|
+
// manual wiring. Anything undetectable keeps a safe default + a TODO.
|
|
52
|
+
function detectConfig() {
|
|
53
|
+
const read = (rel) => {
|
|
54
|
+
try {
|
|
55
|
+
return fs.readFileSync(path.join(hostRoot, rel), "utf-8");
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const setup = read("tests/global-setup.ts");
|
|
61
|
+
const fixture = read("tests/fixtures/auth.fixture.ts");
|
|
62
|
+
if (!setup || !/export default/.test(setup)) return null;
|
|
63
|
+
if (!fixture || !/export (async )?function createAuthContext/.test(fixture)) return null;
|
|
64
|
+
|
|
65
|
+
const port = /-p\s+(\d+)/.exec(pkg.scripts?.dev ?? "")?.[1] ?? "3000";
|
|
66
|
+
const theme = read("src/theme/index.ts") ?? "";
|
|
67
|
+
const primary = /primary:\s*['"](#[0-9a-fA-F]{6})/.exec(theme)?.[1];
|
|
68
|
+
const secondary = /secondary:\s*['"](#[0-9a-fA-F]{6})/.exec(theme)?.[1];
|
|
69
|
+
|
|
70
|
+
return `import type { DemoConfig } from "demowalk";
|
|
71
|
+
|
|
72
|
+
// ⚠️ Import ORDER matters: the global setup loads the e2e env on import,
|
|
73
|
+
// and the auth fixture reads that env at ITS import — setup must come first.
|
|
74
|
+
import globalSetup from "../tests/global-setup";
|
|
75
|
+
import { createAuthContext } from "../tests/fixtures/auth.fixture";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* AUTO-WIRED by demowalk-init from this repo's own conventions
|
|
79
|
+
* (e2e fixture, dev port, theme colours). Review the values once; adjust
|
|
80
|
+
* anything the detection got wrong.
|
|
81
|
+
*/
|
|
82
|
+
const config: DemoConfig = {
|
|
83
|
+
baseUrl: process.env.BASE_URL ?? "http://localhost:${port}",
|
|
84
|
+
prepare: () => globalSetup(),
|
|
85
|
+
createAuthContext,
|
|
86
|
+
ringColor: "${primary ?? "#2F6FEB"}",${primary ? " // from src/theme primary" : " // TODO: your theme primary"}
|
|
87
|
+
chipColor: "${secondary ?? "#1F2937"}",${secondary ? " // from src/theme secondary" : " // TODO: your theme secondary"}
|
|
88
|
+
redactionTint: "#E2E5E9", // skeleton grey — adjust to your theme's tint
|
|
89
|
+
voice: "Tom (Enhanced)",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default config;
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let autoWired = false;
|
|
97
|
+
const configPath = path.join(hostRoot, "demo", "demo.config.ts");
|
|
98
|
+
const existingConfig = fs.existsSync(configPath) ? fs.readFileSync(configPath, "utf-8") : null;
|
|
99
|
+
const isUnwiredPlaceholder = existingConfig?.includes("wire your portal's e2e auth fixture here");
|
|
100
|
+
if (!existingConfig || isUnwiredPlaceholder) {
|
|
101
|
+
const wired = detectConfig();
|
|
102
|
+
if (wired) {
|
|
103
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
104
|
+
fs.writeFileSync(configPath, wired);
|
|
105
|
+
autoWired = true;
|
|
106
|
+
console.log("✓ demo/demo.config.ts AUTO-WIRED from this repo's e2e fixture, dev port and theme");
|
|
107
|
+
} else if (isUnwiredPlaceholder) {
|
|
108
|
+
console.log("⚠ demo/demo.config.ts is still the placeholder and no e2e convention was detected — wire it by hand");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// (a config the team already edited is never touched)
|
|
112
|
+
|
|
113
|
+
// 2. templates → demo/, FILE-level idempotent: each missing file is copied,
|
|
114
|
+
// each existing file is left untouched (a discarded install can leave empty
|
|
115
|
+
// folders behind — "the folder exists" proves nothing about its contents).
|
|
116
|
+
const demoDir = path.join(hostRoot, "demo");
|
|
117
|
+
const templatesDir = path.join(kitRoot, "templates");
|
|
118
|
+
let copied = 0;
|
|
119
|
+
for (const rel of fs.readdirSync(templatesDir, { recursive: true })) {
|
|
120
|
+
const src = path.join(templatesDir, String(rel));
|
|
121
|
+
if (fs.statSync(src).isDirectory()) continue;
|
|
122
|
+
const dest = path.join(demoDir, String(rel));
|
|
123
|
+
if (fs.existsSync(dest)) continue;
|
|
124
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
125
|
+
fs.copyFileSync(src, dest);
|
|
126
|
+
copied++;
|
|
127
|
+
console.log(`✓ demo/${rel} created from template`);
|
|
128
|
+
}
|
|
129
|
+
if (copied === 0) console.log("✓ demo/ complete — nothing to copy");
|
|
130
|
+
|
|
131
|
+
// 3. gitignore
|
|
132
|
+
const giPath = path.join(hostRoot, ".gitignore");
|
|
133
|
+
const gi = fs.existsSync(giPath) ? fs.readFileSync(giPath, "utf-8") : "";
|
|
134
|
+
if (!gi.split("\n").some((l) => l.trim() === "demo/out/")) {
|
|
135
|
+
fs.writeFileSync(giPath, gi.replace(/\n?$/, "\n") + "demo/out/\n");
|
|
136
|
+
console.log("✓ .gitignore: demo/out/");
|
|
137
|
+
} else {
|
|
138
|
+
console.log("✓ .gitignore already covers demo/out/");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 4. what's left (the genuinely portal-specific part)
|
|
142
|
+
const configStep = autoWired
|
|
143
|
+
? ` 1. Review demo/demo.config.ts (auto-wired — adjust anything detection
|
|
144
|
+
got wrong) and make sure the e2e env file it relies on exists with the
|
|
145
|
+
test user's credentials.`
|
|
146
|
+
: ` 1. Wire demo/demo.config.ts — your auth fixture (env-loading setup
|
|
147
|
+
import FIRST), your baseUrl, YOUR theme colours.`;
|
|
148
|
+
console.log(`
|
|
149
|
+
Left to do by hand (the portal-specific part):
|
|
150
|
+
${configStep}
|
|
151
|
+
2. Check ffmpeg: ffmpeg -version
|
|
152
|
+
3. Run the smoke test: npm run demo:video (runs the example)
|
|
153
|
+
`);
|
package/src/assets.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
import sharp from "sharp";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Render an SVG string to a PNG on disk if it doesn't exist yet — for
|
|
8
|
+
* journey assets like a fictional demo logo, generated rather than committed
|
|
9
|
+
* (no binaries in repos, and the asset regenerates with everything else).
|
|
10
|
+
*/
|
|
11
|
+
export async function ensureSvgAsset(outPath: string, svg: string): Promise<string> {
|
|
12
|
+
if (!fs.existsSync(outPath)) {
|
|
13
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
14
|
+
await sharp(Buffer.from(svg)).png().toFile(outPath);
|
|
15
|
+
}
|
|
16
|
+
return outPath;
|
|
17
|
+
}
|