@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.
- package/README.md +496 -57
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +26 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/capture.d.ts +3 -2
- package/dist/commands/capture.d.ts.map +1 -1
- package/dist/commands/capture.js +234 -20
- package/dist/commands/capture.js.map +1 -1
- package/dist/commands/discover.d.ts +2 -0
- package/dist/commands/discover.d.ts.map +1 -0
- package/dist/commands/discover.js +62 -0
- package/dist/commands/discover.js.map +1 -0
- package/dist/commands/edit.d.ts.map +1 -1
- package/dist/commands/edit.js +40 -20
- package/dist/commands/edit.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +206 -102
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +33 -0
- package/dist/commands/list.js.map +1 -0
- package/dist/commands/login.d.ts +6 -0
- package/dist/commands/login.d.ts.map +1 -0
- package/dist/commands/login.js +142 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/validate.js +1 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/editor/inject-script.js +407 -33
- package/dist/editor/inject-script.js.map +1 -1
- package/dist/index.js +27 -16
- package/dist/index.js.map +1 -1
- package/dist/lib/annotate.d.ts +2 -2
- package/dist/lib/annotate.d.ts.map +1 -1
- package/dist/lib/annotate.js +35 -43
- package/dist/lib/annotate.js.map +1 -1
- package/dist/lib/auth.d.ts +18 -0
- package/dist/lib/auth.d.ts.map +1 -0
- package/dist/lib/auth.js +45 -0
- package/dist/lib/auth.js.map +1 -0
- package/dist/lib/aws-error.d.ts +7 -0
- package/dist/lib/aws-error.d.ts.map +1 -0
- package/dist/lib/aws-error.js +74 -0
- package/dist/lib/aws-error.js.map +1 -0
- package/dist/lib/canonical.d.ts +22 -0
- package/dist/lib/canonical.d.ts.map +1 -0
- package/dist/lib/canonical.js +92 -0
- package/dist/lib/canonical.js.map +1 -0
- package/dist/lib/config.d.ts +2 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +23 -13
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/crawl.d.ts +1 -1
- package/dist/lib/crawl.d.ts.map +1 -1
- package/dist/lib/crawl.js +213 -20
- package/dist/lib/crawl.js.map +1 -1
- package/dist/lib/definition.d.ts +2 -0
- package/dist/lib/definition.d.ts.map +1 -0
- package/dist/lib/definition.js +6 -0
- package/dist/lib/definition.js.map +1 -0
- package/dist/lib/diff.d.ts +52 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +41 -0
- package/dist/lib/diff.js.map +1 -0
- package/dist/lib/login-error.d.ts +10 -0
- package/dist/lib/login-error.d.ts.map +1 -0
- package/dist/lib/login-error.js +13 -0
- package/dist/lib/login-error.js.map +1 -0
- package/dist/lib/screenshot.d.ts +8 -2
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/screenshot.js +44 -61
- package/dist/lib/screenshot.js.map +1 -1
- package/dist/lib/staging.d.ts +8 -0
- package/dist/lib/staging.d.ts.map +1 -0
- package/dist/lib/staging.js +24 -0
- package/dist/lib/staging.js.map +1 -0
- package/dist/types.d.ts +5 -23
- package/dist/types.d.ts.map +1 -1
- package/package.json +18 -11
- package/dist/commands/preview.d.ts +0 -6
- package/dist/commands/preview.d.ts.map +0 -1
- package/dist/commands/preview.js +0 -33
- package/dist/commands/preview.js.map +0 -1
- package/dist/commands/run.d.ts +0 -6
- package/dist/commands/run.d.ts.map +0 -1
- package/dist/commands/run.js +0 -39
- package/dist/commands/run.js.map +0 -1
- package/dist/editor/editor/editor.html +0 -313
- package/dist/editor/editor/inject.ts +0 -385
- package/dist/editor/editor.html +0 -338
- package/dist/editor/inject-script.cjs +0 -398
- package/dist/editor/inject-script.cjs.map +0 -1
- package/dist/editor/inject-script.d.cts +0 -2
- package/dist/editor/inject-script.d.cts.map +0 -1
- package/dist/editor/inject.d.ts +0 -2
- package/dist/editor/inject.d.ts.map +0 -1
- package/dist/editor/inject.js +0 -385
- package/dist/editor/inject.js.map +0 -1
- package/dist/lib/upload.d.ts +0 -9
- package/dist/lib/upload.d.ts.map +0 -1
- package/dist/lib/upload.js +0 -43
- 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
|
|
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
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
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
|
|
25
|
+
# Interactive setup: config → login → discover pages → workflow
|
|
24
26
|
npx kagemusha init
|
|
25
27
|
|
|
26
|
-
#
|
|
27
|
-
npx kagemusha
|
|
28
|
+
# Capture, diff vs canonical, publish what changed (the everyday command)
|
|
29
|
+
npx kagemusha capture
|
|
28
30
|
|
|
29
|
-
#
|
|
30
|
-
npx kagemusha run
|
|
31
|
+
# Preview only — no canonical update, no S3 push
|
|
32
|
+
npx kagemusha capture --dry-run
|
|
31
33
|
```
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
That's it. **One verb does everything**: capture → diff → publish (skip with `--dry-run`).
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
47
|
-
| `kagemusha
|
|
48
|
-
| `kagemusha
|
|
49
|
-
| `kagemusha
|
|
50
|
-
| `kagemusha
|
|
51
|
-
| `kagemusha
|
|
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
|
-
|
|
260
|
+
For SSO / MFA cases where login can't be scripted, fall back to `KAGEMUSHA_STORAGE_STATE` (see Authentication section below).
|
|
54
261
|
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
- [
|
|
123
|
-
- [
|
|
124
|
-
- [
|
|
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 @@
|
|
|
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
|
-
|
|
3
|
+
dryRun?: boolean;
|
|
4
4
|
open?: boolean;
|
|
5
|
+
threshold?: string;
|
|
5
6
|
}
|
|
6
|
-
export declare
|
|
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":"
|
|
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"}
|