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 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
- # Temporary Holding Version
1
+ # demowalk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ }
@@ -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
+ }