@wasao/kagemusha 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +496 -57
  2. package/dist/commands/add.d.ts +6 -0
  3. package/dist/commands/add.d.ts.map +1 -0
  4. package/dist/commands/add.js +26 -0
  5. package/dist/commands/add.js.map +1 -0
  6. package/dist/commands/capture.d.ts +3 -2
  7. package/dist/commands/capture.d.ts.map +1 -1
  8. package/dist/commands/capture.js +234 -20
  9. package/dist/commands/capture.js.map +1 -1
  10. package/dist/commands/discover.d.ts +2 -0
  11. package/dist/commands/discover.d.ts.map +1 -0
  12. package/dist/commands/discover.js +62 -0
  13. package/dist/commands/discover.js.map +1 -0
  14. package/dist/commands/edit.d.ts.map +1 -1
  15. package/dist/commands/edit.js +40 -20
  16. package/dist/commands/edit.js.map +1 -1
  17. package/dist/commands/init.d.ts +1 -1
  18. package/dist/commands/init.d.ts.map +1 -1
  19. package/dist/commands/init.js +206 -102
  20. package/dist/commands/init.js.map +1 -1
  21. package/dist/commands/list.d.ts +2 -0
  22. package/dist/commands/list.d.ts.map +1 -0
  23. package/dist/commands/list.js +33 -0
  24. package/dist/commands/list.js.map +1 -0
  25. package/dist/commands/login.d.ts +6 -0
  26. package/dist/commands/login.d.ts.map +1 -0
  27. package/dist/commands/login.js +142 -0
  28. package/dist/commands/login.js.map +1 -0
  29. package/dist/commands/validate.js +1 -1
  30. package/dist/commands/validate.js.map +1 -1
  31. package/dist/editor/inject-script.js +407 -33
  32. package/dist/editor/inject-script.js.map +1 -1
  33. package/dist/index.js +27 -16
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/annotate.d.ts +2 -2
  36. package/dist/lib/annotate.d.ts.map +1 -1
  37. package/dist/lib/annotate.js +35 -43
  38. package/dist/lib/annotate.js.map +1 -1
  39. package/dist/lib/auth.d.ts +18 -0
  40. package/dist/lib/auth.d.ts.map +1 -0
  41. package/dist/lib/auth.js +45 -0
  42. package/dist/lib/auth.js.map +1 -0
  43. package/dist/lib/aws-error.d.ts +7 -0
  44. package/dist/lib/aws-error.d.ts.map +1 -0
  45. package/dist/lib/aws-error.js +74 -0
  46. package/dist/lib/aws-error.js.map +1 -0
  47. package/dist/lib/canonical.d.ts +22 -0
  48. package/dist/lib/canonical.d.ts.map +1 -0
  49. package/dist/lib/canonical.js +92 -0
  50. package/dist/lib/canonical.js.map +1 -0
  51. package/dist/lib/config.d.ts +2 -0
  52. package/dist/lib/config.d.ts.map +1 -1
  53. package/dist/lib/config.js +23 -13
  54. package/dist/lib/config.js.map +1 -1
  55. package/dist/lib/crawl.d.ts +1 -1
  56. package/dist/lib/crawl.d.ts.map +1 -1
  57. package/dist/lib/crawl.js +213 -20
  58. package/dist/lib/crawl.js.map +1 -1
  59. package/dist/lib/definition.d.ts +2 -0
  60. package/dist/lib/definition.d.ts.map +1 -0
  61. package/dist/lib/definition.js +6 -0
  62. package/dist/lib/definition.js.map +1 -0
  63. package/dist/lib/diff.d.ts +52 -0
  64. package/dist/lib/diff.d.ts.map +1 -0
  65. package/dist/lib/diff.js +41 -0
  66. package/dist/lib/diff.js.map +1 -0
  67. package/dist/lib/login-error.d.ts +10 -0
  68. package/dist/lib/login-error.d.ts.map +1 -0
  69. package/dist/lib/login-error.js +13 -0
  70. package/dist/lib/login-error.js.map +1 -0
  71. package/dist/lib/screenshot.d.ts +8 -2
  72. package/dist/lib/screenshot.d.ts.map +1 -1
  73. package/dist/lib/screenshot.js +44 -61
  74. package/dist/lib/screenshot.js.map +1 -1
  75. package/dist/lib/staging.d.ts +8 -0
  76. package/dist/lib/staging.d.ts.map +1 -0
  77. package/dist/lib/staging.js +24 -0
  78. package/dist/lib/staging.js.map +1 -0
  79. package/dist/types.d.ts +5 -23
  80. package/dist/types.d.ts.map +1 -1
  81. package/package.json +18 -11
  82. package/dist/commands/preview.d.ts +0 -6
  83. package/dist/commands/preview.d.ts.map +0 -1
  84. package/dist/commands/preview.js +0 -33
  85. package/dist/commands/preview.js.map +0 -1
  86. package/dist/commands/run.d.ts +0 -6
  87. package/dist/commands/run.d.ts.map +0 -1
  88. package/dist/commands/run.js +0 -39
  89. package/dist/commands/run.js.map +0 -1
  90. package/dist/editor/editor/editor.html +0 -313
  91. package/dist/editor/editor/inject.ts +0 -385
  92. package/dist/editor/editor.html +0 -338
  93. package/dist/editor/inject-script.cjs +0 -398
  94. package/dist/editor/inject-script.cjs.map +0 -1
  95. package/dist/editor/inject-script.d.cts +0 -2
  96. package/dist/editor/inject-script.d.cts.map +0 -1
  97. package/dist/editor/inject.d.ts +0 -2
  98. package/dist/editor/inject.d.ts.map +0 -1
  99. package/dist/editor/inject.js +0 -385
  100. package/dist/editor/inject.js.map +0 -1
  101. package/dist/lib/upload.d.ts +0 -9
  102. package/dist/lib/upload.d.ts.map +0 -1
  103. package/dist/lib/upload.js +0 -43
  104. package/dist/lib/upload.js.map +0 -1
package/README.md CHANGED
@@ -5,13 +5,15 @@ The shadow warrior for your documentation.
5
5
 
6
6
  ## What it does
7
7
 
8
- When you push code, Kagemusha automatically captures screenshots of your app and uploads them to S3. No more manually taking screenshots and updating help articles.
8
+ When you push code, Kagemusha automatically captures screenshots of your app, detects which screenshots changed visually, and uploads the fresh ones to S3 with stable URLs. No more "is this help article screenshot still up-to-date?".
9
9
 
10
- - **Auto-discover pages** — Crawls your app to find pages, select which ones to capture
11
- - **Playwright-powered** — Supports login, click actions, element selection, and full-page capture
12
- - **Annotations** — Add arrows, rectangles, and labels to screenshots
13
- - **S3 upload** — Screenshots are uploaded with stable URLs for embedding in help centers
14
- - **Local mode** — Save screenshots locally for review before uploading
10
+ - **Auto-discover pages** — Crawls your app (SPA routes included) and lets you pick which ones to capture
11
+ - **Login once** — Logs in via browser and reuses the session (`storageState`) for all captures
12
+ - **Playwright-powered** — Full-page / crop capture, pre-capture actions, element hiding
13
+ - **Visual regression** — One command captures, diffs against canonical (S3 or local), and publishes only what changed via [pixelmatch](https://github.com/mapbox/pixelmatch)
14
+ - **Visual editor** — Draw rectangles, arrows, and labels; pick crop range by drag
15
+ - **S3-first** — Stable URLs you can embed in help articles once and never touch again. S3 IS the canonical truth — git stays clean
16
+ - **Local mode** — Optional output dir for testing; never committed to git
15
17
  - **GitHub Actions ready** — Runs on every merge to main
16
18
 
17
19
  ## Quick Start
@@ -20,65 +22,338 @@ When you push code, Kagemusha automatically captures screenshots of your app and
20
22
  # Install
21
23
  npm install -D @wasao/kagemusha
22
24
 
23
- # Interactive setup (generates config, discovers pages, creates workflow)
25
+ # Interactive setup: config login → discover pages workflow
24
26
  npx kagemusha init
25
27
 
26
- # Preview screenshots locally
27
- npx kagemusha preview
28
+ # Capture, diff vs canonical, publish what changed (the everyday command)
29
+ npx kagemusha capture
28
30
 
29
- # Run full pipeline (capture upload)
30
- npx kagemusha run
31
+ # Preview only no canonical update, no S3 push
32
+ npx kagemusha capture --dry-run
31
33
  ```
32
34
 
33
- ## How it works
35
+ That's it. **One verb does everything**: capture → diff → publish (skip with `--dry-run`).
34
36
 
35
- 1. `npx kagemusha init` scans your app and lets you pick which pages to screenshot
36
- 2. Config and definition files are generated in your repo
37
- 3. On every merge to main, GitHub Actions runs `npx kagemusha run`
38
- 4. Screenshots are captured and uploaded to S3
39
- 5. Help center articles reference the S3 URLs — images stay up to date automatically
37
+ ## Workflow
38
+
39
+ ### 1. First-time setup `init`
40
+
41
+ ```bash
42
+ npx kagemusha init
43
+ ```
44
+
45
+ Walks you through:
46
+
47
+ 1. **Config** — target base URL and save destination (local / S3)
48
+ 2. **Login** — if the app requires auth, opens a browser so you can sign in manually. The session is saved to `.kagemusha/auth-state.json` and reused afterwards.
49
+ 3. **Discover** — crawls the app (clicks nav links + BFS on `<a>`) and shows a checklist of found pages
50
+ 4. **Workflow** — optionally generates `.github/workflows/kagemusha.yml`
51
+ 5. **Gitignore** — adds `outputDir/`, `.kagemusha/.staging/`, `reports/`, auth files to `.gitignore`
52
+
53
+ Produces:
54
+
55
+ ```
56
+ kagemusha.config.yaml # base URL, viewport, publish destination
57
+ .kagemusha/definitions.json # one entry per screenshot
58
+ .kagemusha/auth-state.json # saved login state (git-ignored)
59
+ .github/workflows/kagemusha.yml
60
+ ```
61
+
62
+ ### 2. Adding pages later
63
+
64
+ | Command | Use when |
65
+ |---------|----------|
66
+ | `npx kagemusha discover` | Re-crawl and pick up newly added routes |
67
+ | `npx kagemusha add <path>` | Add a single page manually, e.g. `npx kagemusha add /settings` |
68
+ | `npx kagemusha add <path> --id custom-id` | Add a second variant of the same page with a custom ID |
69
+ | `npx kagemusha login` | Refresh the login session (run if you get redirected to the login page during capture) |
70
+ | `npx kagemusha list` | Inspect the current definitions, grouped by URL |
71
+
72
+ One page can have multiple screenshots — use `add` with `--id` to stack states (e.g. `dashboard-empty`, `dashboard-with-data`).
73
+
74
+ ### 3. Editing capture range + annotations — `edit`
75
+
76
+ ```bash
77
+ npx kagemusha edit --id dashboard
78
+ ```
79
+
80
+ Opens the real page in a Playwright browser with a toolbar overlay. Two groups of tools:
81
+
82
+ **Capture** — what area to screenshot
83
+ - **📷 Full** — capture the whole page (default)
84
+ - **✂️ Crop** — drag a rectangle on the page; re-drag to replace
85
+
86
+ **Annotate** — decorations drawn on top of the captured image
87
+ - **▭ Rect / → Arrow / T Label** — drag or click to place; drag existing ones to move; Delete to remove
88
+
89
+ Hit **💾 Save** — both capture range and decorations are written back to `.kagemusha/definitions.json`. The same editor restores everything on next open, so adjusting is iterative.
90
+
91
+ ### 4. Capture — the only verb you need
92
+
93
+ ```bash
94
+ npx kagemusha capture # capture, diff, push changed/new to canonical
95
+ npx kagemusha capture --dry-run # preview only, no canonical update
96
+ npx kagemusha capture --ids a,b # only those IDs
97
+ npx kagemusha capture --threshold 0.001 # 0.1% pixel diff = flagged
98
+ npx kagemusha capture --open # open changed/new results in default viewer
99
+ ```
100
+
101
+ What happens:
102
+
103
+ 1. Captures fresh screenshots (with annotations) into `.kagemusha/.staging/` (internal, git-ignored)
104
+ 2. **Pulls canonical** from the configured destination:
105
+ - `s3` mode: downloads `<id>/latest.png` from S3 into your local `outputDir/` (= the working mirror)
106
+ - `local` mode: reads `outputDir/<id>.png` directly
107
+ 3. Diffs each staging file against canonical using [pixelmatch](https://github.com/mapbox/pixelmatch)
108
+ 4. Writes diff visualizations to `reports/diff/<id>.diff.png` for changed files
109
+ 5. **Default**: for changed/new files only, pushes staging → S3 (or copies into local `outputDir/`). Unchanged files are left alone (history snapshots `<id>/<timestamp>.png` keep prior versions for rollback)
110
+ 6. **With `--dry-run`**: nothing is published — exit code 1 if any pixel-diff is over threshold (CI gate use case)
111
+
112
+ Output (default — push happened):
113
+
114
+ ```
115
+ 🥷 Kagemusha — Capture
116
+ canonical: https://kagemusha.example.com
117
+
118
+ 📸 Capturing 3 screenshot(s) to staging...
119
+
120
+ ✓ engagements-overview
121
+ ~ admin-groups (2.34%) → updated
122
+ ↳ reports/diff/admin-groups.diff.png
123
+ + new-page (added to canonical)
124
+
125
+ changed: 1 / unchanged: 1 / new: 1
126
+ ```
127
+
128
+ Output (`--dry-run`):
129
+
130
+ ```
131
+ 🥷 Kagemusha — Capture (dry-run)
132
+ canonical: https://kagemusha.example.com
133
+
134
+ ✓ engagements-overview
135
+ ~ admin-groups (2.34%) → would update
136
+ + new-page (would be added)
137
+
138
+ changed: 1 / unchanged: 1 / new: 1
139
+
140
+ Drop --dry-run to update canonical (https://kagemusha.example.com).
141
+ ```
142
+
143
+ **Single canonical, kept out of git:**
144
+
145
+ - For `s3` destination: S3 IS the truth. Local `outputDir/` is just a download mirror, git-ignored
146
+ - For `local` destination: `outputDir/` is the truth, also git-ignored — use it for local testing only
147
+ - No `baselines/` directory; the canonical store (S3 or outputDir) IS the baseline. S3 history snapshots are kept as `<id>/<timestamp>.png` for rollback
148
+
149
+ ```
150
+ <outputDir>/ # local working mirror (git-ignored)
151
+ .kagemusha/.staging/ # internal capture staging (git-ignored)
152
+ reports/diff/ # diff visualizations (git-ignored)
153
+ ```
154
+
155
+ `init` adds these to `.gitignore` automatically.
40
156
 
41
157
  ## Commands
42
158
 
43
159
  | Command | Description |
44
160
  |---------|-------------|
45
- | `kagemusha init` | Interactive setup |
46
- | `kagemusha run` | Full pipeline: capture + upload |
47
- | `kagemusha capture` | Capture screenshots only |
48
- | `kagemusha preview` | Preview in browser |
49
- | `kagemusha validate` | Validate config files |
50
- | `kagemusha compare` | VRT diff detection (coming soon) |
51
- | `kagemusha publish` | Publish to Intercom/Zendesk (coming soon) |
161
+ | `kagemusha init` | Interactive setup (config + login + discover + workflow) |
162
+ | `kagemusha login` | Open browser and save login session |
163
+ | `kagemusha discover` | Re-crawl the app and add newly found pages |
164
+ | `kagemusha add <path>` | Add a single screenshot definition |
165
+ | `kagemusha list` | List all definitions, grouped by URL |
166
+ | `kagemusha edit --id <id>` | Open the visual editor (capture range + annotations) |
167
+ | `kagemusha capture` | Capture, diff vs canonical, push changed/new (use `--dry-run` to preview) |
168
+ | `kagemusha validate` | Validate config and definition files |
169
+ | `kagemusha publish` | Publish to Intercom / Zendesk (coming soon) |
170
+
171
+ ## Definition example
172
+
173
+ `.kagemusha/definitions.json` is an array of definitions:
174
+
175
+ ```json
176
+ [
177
+ {
178
+ "id": "dashboard",
179
+ "name": "dashboard",
180
+ "url": "/dashboard",
181
+ "capture": { "mode": "fullPage" },
182
+ "hideElements": [".intercom-launcher"],
183
+ "decorations": [
184
+ {
185
+ "type": "rect",
186
+ "target": { "x": 32, "y": 120, "width": 310, "height": 120 },
187
+ "style": { "color": "#FF0000", "strokeWidth": 2 }
188
+ }
189
+ ]
190
+ },
191
+ {
192
+ "id": "dashboard-hero",
193
+ "url": "/dashboard",
194
+ "capture": {
195
+ "mode": "crop",
196
+ "crop": { "start": { "x": 0, "y": 0 }, "end": { "x": 1280, "y": 400 } }
197
+ },
198
+ "decorations": []
199
+ }
200
+ ]
201
+ ```
202
+
203
+ You normally won't edit this by hand — `discover` / `add` / `edit` write it for you.
204
+
205
+ ## Deploying to GitHub Actions
206
+
207
+ To run kagemusha in CI, you need to set up secrets and verify your workflow file.
208
+
209
+ ### 1. Workflow file
210
+
211
+ `kagemusha init` generates `.github/workflows/kagemusha.yml`. If you've already initialized, verify it exists. For monorepos where kagemusha config lives in a subdirectory, add `defaults.run.working-directory: <subdir>` to the job.
212
+
213
+ ### 2. AWS credentials (for S3 publish)
214
+
215
+ The IAM identity (user or role) needs `s3:GetObject` / `s3:PutObject` on your bucket. Pick one auth strategy:
216
+
217
+ **A. Long-lived access keys** (simpler)
218
+
219
+ \`\`\`bash
220
+ gh secret set AWS_ACCESS_KEY_ID --body "AKIA..."
221
+ gh secret set AWS_SECRET_ACCESS_KEY --body "..."
222
+ \`\`\`
223
+
224
+ **B. OIDC** (recommended for orgs with established trust policies)
225
+
226
+ If your org already has a GitHub OIDC provider + IAM Role configured, replace the AWS env vars in the workflow with:
227
+
228
+ \`\`\`yaml
229
+ permissions:
230
+ id-token: write
231
+ contents: read
232
+
233
+ steps:
234
+ - uses: aws-actions/configure-aws-credentials@v4
235
+ with:
236
+ role-to-assume: arn:aws:iam::<account-id>:role/<role-name>
237
+ aws-region: ap-northeast-1
238
+ \`\`\`
239
+
240
+ No keys in secrets.
241
+
242
+ ### 3. Login credentials (if your app needs auth)
243
+
244
+ If you wrote a `.kagemusha/login.mjs` that reads `process.env.EMAIL` / `PASSWORD`:
245
+
246
+ \`\`\`bash
247
+ gh secret set EMAIL --body "ci-bot@example.com"
248
+ gh secret set PASSWORD --body "..."
249
+ \`\`\`
250
+
251
+ Pass them to the workflow step (already templated in `kagemusha init`):
252
+
253
+ \`\`\`yaml
254
+ - run: npx kagemusha capture
255
+ env:
256
+ EMAIL: \${{ secrets.EMAIL }}
257
+ PASSWORD: \${{ secrets.PASSWORD }}
258
+ \`\`\`
52
259
 
53
- ## Config
260
+ For SSO / MFA cases where login can't be scripted, fall back to `KAGEMUSHA_STORAGE_STATE` (see Authentication section below).
54
261
 
55
- `kagemusha init` generates these files:
262
+ ### 4. AWS region
56
263
 
264
+ Auto-detected from `publish.cdnBaseUrl` (e.g. `https://bucket.s3.ap-northeast-1.amazonaws.com` → `ap-northeast-1`). No explicit `AWS_REGION` env needed.
265
+
266
+ ### 5. First-time test
267
+
268
+ Run the workflow manually before relying on the auto trigger:
269
+
270
+ \`\`\`bash
271
+ gh workflow run "Kagemusha - Screenshot Update"
272
+ gh run watch
273
+ \`\`\`
274
+
275
+ If it fails, check:
276
+ - `gh run view --log-failed` — see step-level errors
277
+ - AWS auth: workflow logs should show `Configure AWS credentials` succeeding
278
+ - Login: friendly errors from `aws-error.ts` (`✗ AWS authentication failed`)
279
+ - Login script: errors from `.kagemusha/login.mjs` will surface in the capture step
280
+
281
+ ## Authentication for login-required apps
282
+
283
+ There are two ways to handle login:
284
+
285
+ ### Local dev: passing env vars
286
+
287
+ kagemusha doesn't auto-load `.env`. Pick whichever fits your project:
288
+
289
+ ```bash
290
+ # Direct shell export
291
+ export EMAIL=demo@example.com
292
+ export PASSWORD=local-dev-password
293
+ npx kagemusha capture
294
+
295
+ # Or wrap with dotenv-cli (works with your existing .env)
296
+ npx dotenv -e .env -- kagemusha capture
297
+
298
+ # Or Node 20.6+ built-in
299
+ node --env-file=.env $(which kagemusha) capture
57
300
  ```
58
- kagemusha.config.yaml # App URL, auth, save destination
59
- .kagemusha/definitions/*.json # One file per screenshot
301
+
302
+ CI uses GitHub Secrets — same env names, just passed via the workflow's `env:` block (no .env needed).
303
+
304
+ ### Option 1 (recommended): scripted auto-login
305
+
306
+ Best for **CI** and apps with simple form-based login. `kagemusha init` offers to generate a skeleton at `.kagemusha/login.mjs`:
307
+
308
+ ```js
309
+ /** @param {import('playwright-chromium').Page} page */
310
+ export const login = async (page) => {
311
+ await page.goto("/login");
312
+ await page.fill('input[name="email"]', process.env.EMAIL ?? "");
313
+ await page.fill('input[name="password"]', process.env.PASSWORD ?? "");
314
+ await page.click('button[type="submit"]');
315
+ await page.waitForURL((url) => !url.pathname.startsWith("/login"));
316
+ };
60
317
  ```
61
318
 
62
- ### Screenshot definition example
319
+ Edit the selectors / wait condition for your app. `kagemusha capture` auto-runs this on first invocation when no saved session exists, so CI just needs `kagemusha capture` (no separate login step). `baseURL` is set from `kagemusha.config.yaml`, so relative paths work.
63
320
 
64
- ```json
65
- {
66
- "id": "dashboard",
67
- "name": "dashboard",
68
- "url": "/dashboard",
69
- "capture": { "mode": "fullPage" },
70
- "hideElements": [".intercom-launcher"],
71
- "decorations": [
72
- {
73
- "type": "rect",
74
- "target": { "x": 32, "y": 120, "width": 310, "height": 120 },
75
- "style": { "color": "#FF0000", "strokeWidth": 2 }
76
- }
77
- ]
78
- }
321
+ More variations:
322
+
323
+ ```js
324
+ // HTTP Basic Auth — no script needed, set in kagemusha.config.yaml:
325
+ // auth:
326
+ // httpCredentials: { username: ..., password: ... }
327
+ // (coming soon)
328
+
329
+ // Token-based:
330
+ export const login = async (page) => {
331
+ await page.context().setExtraHTTPHeaders({
332
+ Authorization: \`Bearer \${process.env.TOKEN}\`,
333
+ });
334
+ };
335
+
336
+ // Multi-step (e.g. email → check inbox → magic link):
337
+ export const login = async (page) => {
338
+ await page.goto("/login");
339
+ await page.fill('input[name="email"]', process.env.EMAIL);
340
+ await page.click('button[type="submit"]');
341
+ // ...fetch the link from a test mailbox API, then navigate to it...
342
+ };
79
343
  ```
80
344
 
81
- ## GitHub Actions
345
+ ### Option 2: manual login + storageState (fallback for SSO / MFA)
346
+
347
+ When scripting login is impossible (SSO, OAuth provider, MFA):
348
+
349
+ 1. Run `kagemusha login` locally — opens a browser, you sign in manually
350
+ 2. Session is saved to `.kagemusha/auth-state.json`
351
+ 3. For CI: `base64 -i .kagemusha/auth-state.json | pbcopy` and save as `KAGEMUSHA_STORAGE_STATE` secret
352
+ 4. CI workflow restores it before capture (commented snippet in the generated `kagemusha.yml`)
353
+
354
+ ## CI pipeline
355
+
356
+ A typical CI flow (uses scripted login from Option 1):
82
357
 
83
358
  ```yaml
84
359
  name: Kagemusha
@@ -86,10 +361,16 @@ on:
86
361
  pull_request:
87
362
  types: [closed]
88
363
  branches: [main]
364
+ workflow_dispatch:
365
+
366
+ # Serialize runs so two merges can't race the S3 canonical
367
+ concurrency:
368
+ group: kagemusha
369
+ cancel-in-progress: false
89
370
 
90
371
  jobs:
91
- screenshots:
92
- if: github.event.pull_request.merged == true
372
+ update-screenshots:
373
+ if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
93
374
  runs-on: ubuntu-latest
94
375
  steps:
95
376
  - uses: actions/checkout@v4
@@ -97,31 +378,189 @@ jobs:
97
378
  with:
98
379
  node-version: 20
99
380
  - run: npm ci
100
- - run: npx kagemusha run
381
+ - run: npx playwright install chromium
382
+
383
+ # capture auto-runs .kagemusha/login.mjs (if present) on first invocation,
384
+ # then pulls canonical from S3, diffs, pushes only what changed.
385
+ - run: npx kagemusha capture
101
386
  env:
102
387
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
103
388
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
389
+ EMAIL: ${{ secrets.EMAIL }}
390
+ PASSWORD: ${{ secrets.PASSWORD }}
391
+
392
+ # Optional: keep diff visualizations as artifacts for later review
393
+ - if: always()
394
+ uses: actions/upload-artifact@v4
395
+ with:
396
+ name: kagemusha-diffs
397
+ path: reports/diff/
398
+ if-no-files-found: ignore
399
+ ```
400
+
401
+ ## Notifications
402
+
403
+ `kagemusha capture` writes a structured `reports/summary.json` after every run. CI (or any tool) reads it and decides what to notify. There's intentionally no built-in Slack/Discord adapter — `jq` + a webhook is enough, and you keep full control of the message.
404
+
405
+ ### Public API: `reports/summary.json`
406
+
407
+ Schema versioned and **part of kagemusha's public API** — additive changes go in minor releases, removals/renames in majors.
408
+
409
+ ```json
410
+ {
411
+ "schemaVersion": "1",
412
+ "timestamp": "2026-05-08T12:34:56.789Z",
413
+ "dryRun": false,
414
+ "canonical": "https://wevox-help-pages.s3.ap-northeast-1.amazonaws.com",
415
+ "counts": { "changed": 1, "unchanged": 5, "new": 2, "missing": 0 },
416
+ "results": [
417
+ { "id": "engagements-overview", "status": "changed", "reason": "pixel-diff", "diffPercentage": 2.34, "diffPath": "reports/diff/engagements-overview.diff.png" },
418
+ { "id": "admin-groups", "status": "changed", "reason": "layout-diff", "canonical": { "width": 1280, "height": 720 }, "staging": { "width": 1280, "height": 880 } },
419
+ { "id": "new-page", "status": "new" },
420
+ { "id": "dashboard", "status": "unchanged" }
421
+ ]
422
+ }
423
+ ```
424
+
425
+ ### Example: Slack (changed/new only)
426
+
427
+ In your `.github/workflows/kagemusha.yml`:
428
+
429
+ ```yaml
430
+ - run: npx kagemusha capture
431
+
432
+ - name: Slack notify
433
+ if: always()
434
+ run: |
435
+ [ -f reports/summary.json ] || exit 0
436
+ BODY=$(jq -r '
437
+ [.results[] | select(.status == "changed" or .status == "new")] as $items
438
+ | if ($items | length) == 0 then empty
439
+ else
440
+ "📸 *kagemusha*: \($items | length) screenshot(s)\n" +
441
+ ($items | map(
442
+ if .status == "changed" then
443
+ "~ \(.id) (\(.diffPercentage // .reason))"
444
+ else
445
+ "+ \(.id) (new)"
446
+ end
447
+ ) | join("\n"))
448
+ end
449
+ ' reports/summary.json)
450
+ [ -n "$BODY" ] || exit 0
451
+ curl -X POST "$SLACK_WEBHOOK_URL" \
452
+ -H 'Content-Type: application/json' \
453
+ -d "$(jq -n --arg t "$BODY" '{text: $t}')"
454
+ env:
455
+ SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
456
+ ```
457
+
458
+ `[ -n "$BODY" ] || exit 0` makes the step a no-op when nothing changed. The generated workflow template includes this commented out — uncomment + set the secret to enable.
459
+
460
+ ### Example: Discord (same payload, different key)
461
+
462
+ ```yaml
463
+ - name: Discord notify
464
+ if: always()
465
+ run: |
466
+ [ -f reports/summary.json ] || exit 0
467
+ BODY=$(jq -r '...' reports/summary.json) # same as Slack
468
+ [ -n "$BODY" ] || exit 0
469
+ curl -X POST "$DISCORD_WEBHOOK_URL" \
470
+ -H 'Content-Type: application/json' \
471
+ -d "$(jq -n --arg c "$BODY" '{content: $c}')"
472
+ env:
473
+ DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
474
+ ```
475
+
476
+ ### Example: GitHub PR comment
477
+
478
+ ```yaml
479
+ - name: PR comment
480
+ if: github.event_name == 'pull_request'
481
+ uses: actions/github-script@v7
482
+ with:
483
+ script: |
484
+ const fs = require('fs');
485
+ if (!fs.existsSync('reports/summary.json')) return;
486
+ const r = JSON.parse(fs.readFileSync('reports/summary.json', 'utf8'));
487
+ const items = r.results.filter(x => x.status === 'changed' || x.status === 'new');
488
+ if (items.length === 0) return;
489
+ const body = `## 📸 kagemusha\n\n` + items.map(x =>
490
+ x.status === 'changed' ? `- ~ \`${x.id}\` (${x.diffPercentage?.toFixed(2) ?? x.reason}%)` : `- + \`${x.id}\` (new)`
491
+ ).join('\n');
492
+ await github.rest.issues.createComment({
493
+ ...context.repo,
494
+ issue_number: context.issue.number,
495
+ body,
496
+ });
104
497
  ```
105
498
 
499
+ ### Local debugging
500
+
501
+ `reports/summary.json` is written every run, including local. Inspect with:
502
+
503
+ ```bash
504
+ cat reports/summary.json | jq '.counts'
505
+ cat reports/summary.json | jq '.results[] | select(.status == "changed")'
506
+ ```
507
+
508
+ ## Positioning
509
+
510
+ **kagemusha is NOT a PR-gating VRT tool.** For per-commit baselines and PR diff review with hosted HTML reports, use [reg-suit](https://github.com/reg-viz/reg-suit), [Chromatic](https://www.chromatic.com/), or [Percy](https://percy.io/).
511
+
512
+ kagemusha is for **post-merge auto-update of help center screenshots**, where:
513
+ - Stable embeddable URLs matter more than per-commit baseline correctness
514
+ - One config + one command should cover capture, diff, and publish
515
+ - The canonical IS the served asset — no separate baseline-vs-published split
516
+
106
517
  ## Try it locally
107
518
 
108
519
  ```bash
109
520
  cd example
110
- bun install
111
- bun run serve # Start sample app
112
- bunx kagemusha init # Set up kagemusha
113
- bunx kagemusha preview # See screenshots
521
+ pnpm install # or `npm install`, or `bun install`
522
+ pnpm run serve # Start sample app
523
+ npx kagemusha init # Set up kagemusha
524
+ npx kagemusha capture --open
114
525
  ```
115
526
 
116
527
  ## Roadmap
117
528
 
118
529
  - [x] Screenshot capture with Playwright
119
530
  - [x] Annotations (rect, arrow, label)
120
- - [x] S3 upload
121
- - [x] Auto-discover pages
122
- - [ ] Visual regression testing (VRT)
123
- - [ ] Intercom / Zendesk integration
124
- - [ ] AI-powered text updates
531
+ - [x] S3 upload with stable URLs
532
+ - [x] Auto-discover pages (SPA-aware BFS crawl)
533
+ - [x] Login via browser (`storageState`) + scripted CI auto-login (`.kagemusha/login.mjs`)
534
+ - [x] Visual editor for capture range (fullPage / crop)
535
+ - [x] **Visual regression — unified `capture` command (publish by default, `--dry-run` to preview)**
536
+ - [ ] HTML diff report (side-by-side, hosted as CI artifact)
537
+ - [ ] Stabilization helpers (clock freezing, animation off, mask regions)
538
+ - [ ] Slack / PR notifications with affected article IDs
539
+ - [ ] Intercom / Zendesk auto-patching
540
+ - [ ] LLM-powered diff descriptions ("what changed in plain English")
541
+
542
+ ## Releasing
543
+
544
+ Release-driven publish via GitHub Actions. Steps for maintainers:
545
+
546
+ 1. Bump `version` in `package.json` and the banner in `src/index.ts` (PR + merge to main)
547
+ 2. On GitHub: **Releases → Draft a new release**
548
+ - Tag: `v0.2.0` (must match `package.json` version, prefixed with `v`)
549
+ - Target: `main`
550
+ - Click **Generate release notes**, edit if needed
551
+ - Publish
552
+ 3. The `Release` workflow fires on `release: { types: [published] }`:
553
+ - Verifies the tag matches `package.json`
554
+ - Runs `pnpm build` + `biome check`
555
+ - `pnpm publish --provenance` to npm (uses `NPM_TOKEN` secret)
556
+
557
+ Or via CLI:
558
+
559
+ ```bash
560
+ gh release create v0.2.0 --generate-notes
561
+ ```
562
+
563
+ Required secret on the repo: `NPM_TOKEN` (Automation token from npmjs.com — needs write access to the `@wasao` scope).
125
564
 
126
565
  ## License
127
566
 
@@ -0,0 +1,6 @@
1
+ interface AddOptions {
2
+ id?: string;
3
+ }
4
+ export declare const addCommand: (pagePath: string, options: AddOptions) => Promise<void>;
5
+ export {};
6
+ //# sourceMappingURL=add.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAQA,UAAU,UAAU;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,eAAO,MAAM,UAAU,GACtB,UAAU,MAAM,EAChB,SAAS,UAAU,KACjB,OAAO,CAAC,IAAI,CAyBd,CAAC"}
@@ -0,0 +1,26 @@
1
+ import chalk from "chalk";
2
+ import { findProjectRoot, loadDefinitions, saveDefinitions, } from "../lib/config.js";
3
+ import { deriveIdFromPath } from "../lib/definition.js";
4
+ export const addCommand = async (pagePath, options) => {
5
+ const projectRoot = findProjectRoot();
6
+ const definitions = loadDefinitions(projectRoot);
7
+ const baseId = options.id ?? deriveIdFromPath(pagePath);
8
+ // If ID already exists, append a suffix
9
+ let id = baseId;
10
+ let suffix = 2;
11
+ while (definitions.some((d) => d.id === id)) {
12
+ id = `${baseId}-${suffix}`;
13
+ suffix++;
14
+ }
15
+ definitions.push({
16
+ id,
17
+ name: id,
18
+ url: pagePath,
19
+ capture: { mode: "fullPage" },
20
+ hideElements: [],
21
+ decorations: [],
22
+ });
23
+ saveDefinitions(definitions, projectRoot);
24
+ console.log(chalk.green(`\n✅ Added ${id}\n`));
25
+ };
26
+ //# sourceMappingURL=add.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"add.js","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EACN,eAAe,EACf,eAAe,EACf,eAAe,GACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAMxD,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAC9B,QAAgB,EAChB,OAAmB,EACH,EAAE;IAClB,MAAM,WAAW,GAAG,eAAe,EAAE,CAAC;IACtC,MAAM,WAAW,GAAG,eAAe,CAAC,WAAW,CAAC,CAAC;IAEjD,MAAM,MAAM,GAAG,OAAO,CAAC,EAAE,IAAI,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAExD,wCAAwC;IACxC,IAAI,EAAE,GAAG,MAAM,CAAC;IAChB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAC7C,EAAE,GAAG,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,EAAE,CAAC;IACV,CAAC;IAED,WAAW,CAAC,IAAI,CAAC;QAChB,EAAE;QACF,IAAI,EAAE,EAAE;QACR,GAAG,EAAE,QAAQ;QACb,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE;QAC7B,YAAY,EAAE,EAAE;QAChB,WAAW,EAAE,EAAE;KACf,CAAC,CAAC;IAEH,eAAe,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAAC;AAC/C,CAAC,CAAC"}
@@ -1,8 +1,9 @@
1
1
  interface CaptureOptions {
2
2
  ids?: string;
3
- all?: boolean;
3
+ dryRun?: boolean;
4
4
  open?: boolean;
5
+ threshold?: string;
5
6
  }
6
- export declare function captureCommand(options: CaptureOptions): Promise<void>;
7
+ export declare const captureCommand: (options: CaptureOptions) => Promise<void>;
7
8
  export {};
8
9
  //# sourceMappingURL=capture.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/commands/capture.ts"],"names":[],"mappings":"AAKA,UAAU,cAAc;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,IAAI,CAAC,EAAE,OAAO,CAAC;CACf;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAkD3E"}
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../../src/commands/capture.ts"],"names":[],"mappings":"AAsBA,UAAU,cAAc;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AA+CD,eAAO,MAAM,cAAc,GAC1B,SAAS,cAAc,KACrB,OAAO,CAAC,IAAI,CAUd,CAAC"}