@wasao/kagemusha 0.2.0 → 0.3.5

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 (93) hide show
  1. package/README.md +103 -453
  2. package/dist/commands/capture.js +57 -35
  3. package/dist/commands/capture.js.map +1 -1
  4. package/dist/commands/edit.d.ts +1 -1
  5. package/dist/commands/edit.d.ts.map +1 -1
  6. package/dist/commands/edit.js +46 -8
  7. package/dist/commands/edit.js.map +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +82 -83
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/commands/login.d.ts.map +1 -1
  12. package/dist/commands/login.js +14 -18
  13. package/dist/commands/login.js.map +1 -1
  14. package/dist/editor/inject-script/annotations.d.ts +11 -0
  15. package/dist/editor/inject-script/annotations.d.ts.map +1 -0
  16. package/dist/editor/inject-script/annotations.js +409 -0
  17. package/dist/editor/inject-script/annotations.js.map +1 -0
  18. package/dist/editor/inject-script/bridge.d.ts +13 -0
  19. package/dist/editor/inject-script/bridge.d.ts.map +1 -0
  20. package/dist/editor/inject-script/bridge.js +33 -0
  21. package/dist/editor/inject-script/bridge.js.map +1 -0
  22. package/dist/editor/inject-script/crop.d.ts +9 -0
  23. package/dist/editor/inject-script/crop.d.ts.map +1 -0
  24. package/dist/editor/inject-script/crop.js +236 -0
  25. package/dist/editor/inject-script/crop.js.map +1 -0
  26. package/dist/editor/inject-script/dom.d.ts +7 -0
  27. package/dist/editor/inject-script/dom.d.ts.map +1 -0
  28. package/dist/editor/inject-script/dom.js +32 -0
  29. package/dist/editor/inject-script/dom.js.map +1 -0
  30. package/dist/editor/inject-script/index.d.ts +2 -0
  31. package/dist/editor/inject-script/index.d.ts.map +1 -0
  32. package/dist/editor/inject-script/index.js +56 -0
  33. package/dist/editor/inject-script/index.js.map +1 -0
  34. package/dist/editor/inject-script/record.d.ts +5 -0
  35. package/dist/editor/inject-script/record.d.ts.map +1 -0
  36. package/dist/editor/inject-script/record.js +398 -0
  37. package/dist/editor/inject-script/record.js.map +1 -0
  38. package/dist/editor/inject-script/selector.d.ts +6 -0
  39. package/dist/editor/inject-script/selector.d.ts.map +1 -0
  40. package/dist/editor/inject-script/selector.js +112 -0
  41. package/dist/editor/inject-script/selector.js.map +1 -0
  42. package/dist/editor/inject-script/state.d.ts +27 -0
  43. package/dist/editor/inject-script/state.d.ts.map +1 -0
  44. package/dist/editor/inject-script/state.js +26 -0
  45. package/dist/editor/inject-script/state.js.map +1 -0
  46. package/dist/editor/inject-script/svg.d.ts +7 -0
  47. package/dist/editor/inject-script/svg.d.ts.map +1 -0
  48. package/dist/editor/inject-script/svg.js +39 -0
  49. package/dist/editor/inject-script/svg.js.map +1 -0
  50. package/dist/editor/inject-script/toolbar.d.ts +14 -0
  51. package/dist/editor/inject-script/toolbar.d.ts.map +1 -0
  52. package/dist/editor/inject-script/toolbar.js +240 -0
  53. package/dist/editor/inject-script/toolbar.js.map +1 -0
  54. package/dist/editor/inject-script/types.d.ts +102 -0
  55. package/dist/editor/inject-script/types.d.ts.map +1 -0
  56. package/dist/editor/inject-script/types.js +5 -0
  57. package/dist/editor/inject-script/types.js.map +1 -0
  58. package/dist/editor/inject-script.js +1248 -699
  59. package/dist/index.js +9 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/lib/canonical.d.ts +35 -3
  62. package/dist/lib/canonical.d.ts.map +1 -1
  63. package/dist/lib/canonical.js +85 -25
  64. package/dist/lib/canonical.js.map +1 -1
  65. package/dist/lib/crawl.js +1 -1
  66. package/dist/lib/crawl.js.map +1 -1
  67. package/dist/lib/diff.d.ts +23 -4
  68. package/dist/lib/diff.d.ts.map +1 -1
  69. package/dist/lib/diff.js +5 -6
  70. package/dist/lib/diff.js.map +1 -1
  71. package/dist/lib/page-ready.d.ts +18 -0
  72. package/dist/lib/page-ready.d.ts.map +1 -0
  73. package/dist/lib/page-ready.js +19 -0
  74. package/dist/lib/page-ready.js.map +1 -0
  75. package/dist/lib/playwright-launch.d.ts +4 -0
  76. package/dist/lib/playwright-launch.d.ts.map +1 -0
  77. package/dist/lib/playwright-launch.js +11 -0
  78. package/dist/lib/playwright-launch.js.map +1 -0
  79. package/dist/lib/screenshot.d.ts +4 -1
  80. package/dist/lib/screenshot.d.ts.map +1 -1
  81. package/dist/lib/screenshot.js +37 -7
  82. package/dist/lib/screenshot.js.map +1 -1
  83. package/dist/lib/staging.d.ts +0 -1
  84. package/dist/lib/staging.d.ts.map +1 -1
  85. package/dist/lib/staging.js +0 -3
  86. package/dist/lib/staging.js.map +1 -1
  87. package/dist/types.d.ts +5 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/package.json +22 -12
  90. package/templates/notify-slack.jq +30 -0
  91. package/dist/editor/inject-script.d.ts +0 -2
  92. package/dist/editor/inject-script.d.ts.map +0 -1
  93. package/dist/editor/inject-script.js.map +0 -1
package/README.md CHANGED
@@ -5,562 +5,212 @@ 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, 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?".
8
+ `kagemusha capture` walks your app with Playwright, diffs each screenshot against the canonical version on S3, and pushes only what changed. Your help articles embed a stable URL once and the image refreshes itself on every merge to main.
9
9
 
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
17
- - **GitHub Actions ready** — Runs on every merge to main
10
+ - **One verb (`capture`)** — capture diff push, all in one command
11
+ - **S3-first** — `<id>/latest.png` is the canonical, embedded directly into help articles
12
+ - **Component-level capture** — Playwright-powered, full-page / crop, pre-capture actions, element hiding
13
+ - **Visual editor** — draw rectangles, arrows, labels; pick crop range by drag (`kagemusha edit`)
14
+ - **Login once** — store `storageState` locally, or run a scripted login on every CI run
15
+ - **Slack-ready** — `reports/summary.json` ships before/after URLs so notifications include image previews
18
16
 
19
17
  ## Quick Start
20
18
 
21
19
  ```bash
22
- # Install
23
20
  npm install -D @wasao/kagemusha
24
21
 
25
22
  # Interactive setup: config → login → discover pages → workflow
26
23
  npx kagemusha init
27
24
 
28
- # Capture, diff vs canonical, publish what changed (the everyday command)
25
+ # Capture, diff vs canonical, publish what changed
29
26
  npx kagemusha capture
30
27
 
31
- # Preview only — no canonical update, no S3 push
28
+ # Preview only — no canonical update
32
29
  npx kagemusha capture --dry-run
33
30
  ```
34
31
 
35
- That's it. **One verb does everything**: capture → diff → publish (skip with `--dry-run`).
36
-
37
- ## Workflow
38
-
39
- ### 1. First-time setup — `init`
40
-
41
- ```bash
42
- npx kagemusha init
43
- ```
32
+ ## Commands
44
33
 
45
- Walks you through:
34
+ | Command | Description |
35
+ |---|---|
36
+ | `kagemusha init` | Interactive setup (config + login + discover + workflow) |
37
+ | `kagemusha login` | Refresh the saved login session (interactive or scripted) |
38
+ | `kagemusha discover` | Re-crawl the app and add newly-found pages to definitions |
39
+ | `kagemusha add <path>` | Add a single screenshot definition |
40
+ | `kagemusha list` | List all definitions |
41
+ | `kagemusha edit --id <id>` | Open the visual editor (crop range + annotations) |
42
+ | `kagemusha capture` | Capture → diff → publish (use `--dry-run` to preview) |
43
+ | `kagemusha validate` | Validate config and definition files |
46
44
 
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`
45
+ ## Configuration
52
46
 
53
- Produces:
47
+ `init` generates these files:
54
48
 
55
49
  ```
56
50
  kagemusha.config.yaml # base URL, viewport, publish destination
57
51
  .kagemusha/definitions.json # one entry per screenshot
58
- .kagemusha/auth-state.json # saved login state (git-ignored)
52
+ .kagemusha/login.mjs # optional scripted login (CI-friendly)
59
53
  .github/workflows/kagemusha.yml
60
54
  ```
61
55
 
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
56
+ `kagemusha.config.yaml`:
148
57
 
58
+ ```yaml
59
+ app:
60
+ baseUrl: https://your-app.example.com
61
+ screenshot:
62
+ defaultViewport: { width: 1440, height: 900, deviceScaleFactor: 2 }
63
+ defaultDiffThreshold: 0.005 # 0.5% pixel diff = flagged
64
+ publish:
65
+ destination: s3 # or "local" for testing
66
+ cdnBucket: your-bucket
67
+ cdnBaseUrl: https://your-bucket.s3.ap-northeast-1.amazonaws.com
149
68
  ```
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.
156
-
157
- ## Commands
158
-
159
- | Command | Description |
160
- |---------|-------------|
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
69
 
173
- `.kagemusha/definitions.json` is an array of definitions:
70
+ Definition (`.kagemusha/definitions.json`):
174
71
 
175
72
  ```json
176
73
  [
177
74
  {
178
75
  "id": "dashboard",
179
- "name": "dashboard",
180
76
  "url": "/dashboard",
181
77
  "capture": { "mode": "fullPage" },
182
78
  "hideElements": [".intercom-launcher"],
183
79
  "decorations": [
184
- {
185
- "type": "rect",
186
- "target": { "x": 32, "y": 120, "width": 310, "height": 120 },
187
- "style": { "color": "#FF0000", "strokeWidth": 2 }
188
- }
80
+ { "type": "rect", "target": { "x": 32, "y": 120, "width": 310, "height": 120 } }
189
81
  ]
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
82
  }
200
83
  ]
201
84
  ```
202
85
 
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)
86
+ Run `kagemusha edit --id dashboard` to set the crop range and add decorations visually.
225
87
 
226
- If your org already has a GitHub OIDC provider + IAM Role configured, replace the AWS env vars in the workflow with:
88
+ ## Avoiding loading-state screenshots
227
89
 
228
- \`\`\`yaml
229
- permissions:
230
- id-token: write
231
- contents: read
90
+ After `page.goto` kagemusha waits for `load` event + 3s of best-effort `networkidle` + 500ms hydration buffer. This handles most pages; SPAs with component-level skeletons may still capture mid-loading. Add a `beforeCapture` step per definition:
232
91
 
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
- \`\`\`
259
-
260
- For SSO / MFA cases where login can't be scripted, fall back to `KAGEMUSHA_STORAGE_STATE` (see Authentication section below).
261
-
262
- ### 4. AWS region
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
92
+ ```json
93
+ {
94
+ "id": "analytics-overview",
95
+ "url": "/analytics/overview",
96
+ "beforeCapture": [
97
+ { "action": "waitForSelector", "selector": "text=Overview", "timeout": 15000 },
98
+ { "action": "wait", "ms": 3000 }
99
+ ]
100
+ }
300
101
  ```
301
102
 
302
- CI uses GitHub Secrets same env names, just passed via the workflow's `env:` block (no .env needed).
103
+ Wait for a known page-specific element (page title, chart canvas, first table row, etc.), then a short buffer. Playwright's `text=` selector matches any rendered text.
303
104
 
304
- ### Option 1 (recommended): scripted auto-login
105
+ ## Authentication
305
106
 
306
- Best for **CI** and apps with simple form-based login. `kagemusha init` offers to generate a skeleton at `.kagemusha/login.mjs`:
107
+ If your app needs login, `init` generates a `.kagemusha/login.mjs` skeleton:
307
108
 
308
109
  ```js
309
110
  /** @param {import('playwright-chromium').Page} page */
310
111
  export const login = async (page) => {
311
112
  await page.goto("/login");
312
- await page.fill('input[name="email"]', process.env.EMAIL ?? "");
313
- await page.fill('input[name="password"]', process.env.PASSWORD ?? "");
113
+ // Pick env names that fit your project (NOT EMAIL/PASSWORD, NOT KAGEMUSHA_*).
114
+ await page.fill('input[name="email"]', process.env.MY_APP_EMAIL ?? "");
115
+ await page.fill('input[name="password"]', process.env.MY_APP_PASSWORD ?? "");
314
116
  await page.click('button[type="submit"]');
315
117
  await page.waitForURL((url) => !url.pathname.startsWith("/login"));
316
118
  };
317
119
  ```
318
120
 
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.
320
-
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
- };
343
- ```
121
+ `kagemusha capture` auto-runs this on first invocation when no saved session exists, so CI just needs `npx kagemusha capture` no separate login step. `baseURL` is set from your config, so relative paths work.
344
122
 
345
- ### Option 2: manual login + storageState (fallback for SSO / MFA)
123
+ For SSO / MFA / OAuth where scripting is impossible: run `kagemusha login` locally to save `.kagemusha/auth-state.json`, then `base64 -i .kagemusha/auth-state.json | pbcopy` and store the result as a `KAGEMUSHA_STORAGE_STATE` GitHub Secret. The generated workflow shows the restore step (commented out).
346
124
 
347
- When scripting login is impossible (SSO, OAuth provider, MFA):
125
+ ## Deploying to GitHub Actions
348
126
 
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`)
127
+ `init` generates `.github/workflows/kagemusha.yml`. Required secrets:
353
128
 
354
- ## CI pipeline
129
+ - `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` — IAM with `s3:GetObject` and `s3:PutObject` on your bucket, or use OIDC with `aws-actions/configure-aws-credentials@v4`
130
+ - Login credentials (named whatever your `login.mjs` reads)
131
+ - `SLACK_WEBHOOK_URL` — optional, see Notifications below
355
132
 
356
- A typical CI flow (uses scripted login from Option 1):
133
+ Region is auto-detected from `publish.cdnBaseUrl` (`*.s3.<region>.amazonaws.com`), so no `AWS_REGION` env needed.
357
134
 
358
- ```yaml
359
- name: Kagemusha
360
- on:
361
- pull_request:
362
- types: [closed]
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
370
-
371
- jobs:
372
- update-screenshots:
373
- if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch'
374
- runs-on: ubuntu-latest
375
- steps:
376
- - uses: actions/checkout@v4
377
- - uses: actions/setup-node@v4
378
- with:
379
- node-version: 20
380
- - run: npm ci
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
386
- env:
387
- AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
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
- ```
135
+ The workflow triggers on `push: main` and runs `kagemusha capture` automatically.
400
136
 
401
137
  ## Notifications
402
138
 
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.
139
+ `kagemusha capture` writes `reports/summary.json` with before/after URLs (when destination is S3 and a real push happened):
408
140
 
409
141
  ```json
410
142
  {
411
143
  "schemaVersion": "1",
412
- "timestamp": "2026-05-08T12:34:56.789Z",
144
+ "timestamp": "2026-05-15T12:34:56.789Z",
413
145
  "dryRun": false,
414
- "canonical": "https://wevox-help-pages.s3.ap-northeast-1.amazonaws.com",
146
+ "canonical": "https://your-bucket.s3.ap-northeast-1.amazonaws.com",
415
147
  "counts": { "changed": 1, "unchanged": 5, "new": 2, "missing": 0 },
416
148
  "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" },
149
+ {
150
+ "id": "engagements-overview",
151
+ "status": "changed",
152
+ "reason": "pixel-diff",
153
+ "diffPercentage": 2.34,
154
+ "urls": {
155
+ "before": "https://.../engagements-overview/previous.png",
156
+ "after": "https://.../engagements-overview/latest.png"
157
+ }
158
+ },
159
+ { "id": "new-page", "status": "new", "urls": { "after": "https://.../new-page/latest.png" } },
420
160
  { "id": "dashboard", "status": "unchanged" }
421
161
  ]
422
162
  }
423
163
  ```
424
164
 
425
- ### Example: Slack (changed/new only)
165
+ The schema is **part of kagemusha's public API** — additive changes stay on `schemaVersion: "1"`, removals/renames bump it. kagemusha intentionally does not publish a pre-rendered diff image; consumers compare `before` vs `after` raw images (= Slack auto-unfurls both side by side).
426
166
 
427
- In your `.github/workflows/kagemusha.yml`:
167
+ Slack notification (the generated workflow includes this; just set `SLACK_WEBHOOK_URL`):
428
168
 
429
169
  ```yaml
430
- - run: npx kagemusha capture
431
-
432
170
  - 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
171
  env:
455
172
  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
173
  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 }}
174
+ [ -n "$SLACK_WEBHOOK_URL" ] || exit 0
175
+ jq -c -f .kagemusha/notify-slack.jq reports/summary.json | while IFS= read -r payload; do
176
+ [ -z "$payload" ] && continue
177
+ curl -sS -X POST "$SLACK_WEBHOOK_URL" \
178
+ -H 'Content-Type: application/json' \
179
+ --data "$payload"
180
+ done
474
181
  ```
475
182
 
476
- ### Example: GitHub PR comment
183
+ One message per changed/new screenshot — Slack unfurls the `before`/`after` URLs per-message, so each post shows its own image previews instead of a single message that just lists URLs as plain text.
477
184
 
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
- });
497
- ```
498
-
499
- ### Local debugging
500
-
501
- `reports/summary.json` is written every run, including local. Inspect with:
185
+ The message format is defined in `.kagemusha/notify-slack.jq` (generated by `kagemusha init`). Each line of jq output becomes one Slack `chat.postMessage` body, so you can customize freely (add `blocks`, override `channel`, etc). Test locally:
502
186
 
503
187
  ```bash
504
- cat reports/summary.json | jq '.counts'
505
- cat reports/summary.json | jq '.results[] | select(.status == "changed")'
188
+ jq -c -f .kagemusha/notify-slack.jq reports/summary.json
506
189
  ```
507
190
 
191
+ The same `summary.json` works for Discord (swap `text` → `content`) or PR comments via `actions/github-script` — copy the jq file and tweak.
192
+
508
193
  ## Positioning
509
194
 
510
195
  **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
196
 
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
-
517
- ## Try it locally
518
-
519
- ```bash
520
- cd example
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
525
- ```
526
-
527
- ## Roadmap
528
-
529
- - [x] Screenshot capture with Playwright
530
- - [x] Annotations (rect, arrow, label)
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")
197
+ kagemusha is for **post-merge auto-update of help center screenshots**, where stable embeddable URLs matter more than per-commit baseline correctness.
541
198
 
542
199
  ## Releasing
543
200
 
544
- Release-driven publish via GitHub Actions. Steps for maintainers:
201
+ Automated via [release-please](https://github.com/googleapis/release-please).
545
202
 
546
- 1. Bump `version` in `package.json` and the banner in `src/index.ts` (PR + merge to main)
547
- 2. On GitHub: **ReleasesDraft 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
- ```
203
+ 1. **Write PRs with [Conventional Commits](https://www.conventionalcommits.org/) titles**:
204
+ - `feat: ...`minor bump (= 0.2.0 → 0.3.0)
205
+ - `fix: ...` → patch bump (= 0.2.0 0.2.1)
206
+ - `feat!: ...` or `BREAKING CHANGE:` in body → major (= 1.0.0+)
207
+ - `chore: ...` / `docs: ...` no version bump
208
+ - Squash merge propagates the PR title as the commit on `main`.
209
+ 2. **release-please opens a "Release PR" automatically** whenever there's anything to release. It bumps `package.json`, updates `.release-please-manifest.json`, and regenerates `CHANGELOG.md`.
210
+ 3. **Merge the Release PR** → release-please tags `v0.X.Y` and publishes a GitHub Release.
211
+ 4. **`release.yml` triggers on `release: published`** → builds and runs `pnpm publish --provenance` to npm. Requires the `NPM_TOKEN` secret.
562
212
 
563
- Required secret on the repo: `NPM_TOKEN` (Automation token from npmjs.com needs write access to the `@wasao` scope).
213
+ The `kagemusha` CLI reads its version from `package.json` at runtime, so release-please only needs to touch one file.
564
214
 
565
215
  ## License
566
216