@wasao/kagemusha 0.1.1 → 0.3.4
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 +168 -79
- 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 +256 -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 +1 -1
- package/dist/commands/edit.d.ts.map +1 -1
- package/dist/commands/edit.js +82 -26
- 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 +240 -105
- 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 +131 -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/annotations.d.ts +11 -0
- package/dist/editor/inject-script/annotations.d.ts.map +1 -0
- package/dist/editor/inject-script/annotations.js +409 -0
- package/dist/editor/inject-script/annotations.js.map +1 -0
- package/dist/editor/inject-script/bridge.d.ts +13 -0
- package/dist/editor/inject-script/bridge.d.ts.map +1 -0
- package/dist/editor/inject-script/bridge.js +33 -0
- package/dist/editor/inject-script/bridge.js.map +1 -0
- package/dist/editor/inject-script/crop.d.ts +9 -0
- package/dist/editor/inject-script/crop.d.ts.map +1 -0
- package/dist/editor/inject-script/crop.js +236 -0
- package/dist/editor/inject-script/crop.js.map +1 -0
- package/dist/editor/inject-script/dom.d.ts +7 -0
- package/dist/editor/inject-script/dom.d.ts.map +1 -0
- package/dist/editor/inject-script/dom.js +32 -0
- package/dist/editor/inject-script/dom.js.map +1 -0
- package/dist/editor/inject-script/index.d.ts +2 -0
- package/dist/editor/inject-script/index.d.ts.map +1 -0
- package/dist/editor/inject-script/index.js +56 -0
- package/dist/editor/inject-script/index.js.map +1 -0
- package/dist/editor/inject-script/record.d.ts +5 -0
- package/dist/editor/inject-script/record.d.ts.map +1 -0
- package/dist/editor/inject-script/record.js +398 -0
- package/dist/editor/inject-script/record.js.map +1 -0
- package/dist/editor/inject-script/selector.d.ts +6 -0
- package/dist/editor/inject-script/selector.d.ts.map +1 -0
- package/dist/editor/inject-script/selector.js +112 -0
- package/dist/editor/inject-script/selector.js.map +1 -0
- package/dist/editor/inject-script/state.d.ts +27 -0
- package/dist/editor/inject-script/state.d.ts.map +1 -0
- package/dist/editor/inject-script/state.js +26 -0
- package/dist/editor/inject-script/state.js.map +1 -0
- package/dist/editor/inject-script/svg.d.ts +7 -0
- package/dist/editor/inject-script/svg.d.ts.map +1 -0
- package/dist/editor/inject-script/svg.js +39 -0
- package/dist/editor/inject-script/svg.js.map +1 -0
- package/dist/editor/inject-script/toolbar.d.ts +14 -0
- package/dist/editor/inject-script/toolbar.d.ts.map +1 -0
- package/dist/editor/inject-script/toolbar.js +240 -0
- package/dist/editor/inject-script/toolbar.js.map +1 -0
- package/dist/editor/inject-script/types.d.ts +102 -0
- package/dist/editor/inject-script/types.d.ts.map +1 -0
- package/dist/editor/inject-script/types.js +5 -0
- package/dist/editor/inject-script/types.js.map +1 -0
- package/dist/editor/inject-script.js +1276 -353
- package/dist/index.js +34 -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 +54 -0
- package/dist/lib/canonical.d.ts.map +1 -0
- package/dist/lib/canonical.js +152 -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 +71 -0
- package/dist/lib/diff.d.ts.map +1 -0
- package/dist/lib/diff.js +40 -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/page-ready.d.ts +18 -0
- package/dist/lib/page-ready.d.ts.map +1 -0
- package/dist/lib/page-ready.js +19 -0
- package/dist/lib/page-ready.js.map +1 -0
- package/dist/lib/screenshot.d.ts +11 -2
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/screenshot.js +73 -64
- package/dist/lib/screenshot.js.map +1 -1
- package/dist/lib/staging.d.ts +7 -0
- package/dist/lib/staging.d.ts.map +1 -0
- package/dist/lib/staging.js +21 -0
- package/dist/lib/staging.js.map +1 -0
- package/dist/types.d.ts +10 -23
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -3
- 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-script.d.ts +0 -2
- package/dist/editor/inject-script.d.ts.map +0 -1
- package/dist/editor/inject-script.js.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,123 +5,212 @@ The shadow warrior for your documentation.
|
|
|
5
5
|
|
|
6
6
|
## What it does
|
|
7
7
|
|
|
8
|
-
|
|
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
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
- **
|
|
14
|
-
- **
|
|
15
|
-
- **
|
|
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
|
|
16
16
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
# Install
|
|
21
20
|
npm install -D @wasao/kagemusha
|
|
22
21
|
|
|
23
|
-
# Interactive setup
|
|
22
|
+
# Interactive setup: config → login → discover pages → workflow
|
|
24
23
|
npx kagemusha init
|
|
25
24
|
|
|
26
|
-
#
|
|
27
|
-
npx kagemusha
|
|
25
|
+
# Capture, diff vs canonical, publish what changed
|
|
26
|
+
npx kagemusha capture
|
|
28
27
|
|
|
29
|
-
#
|
|
30
|
-
npx kagemusha run
|
|
28
|
+
# Preview only — no canonical update
|
|
29
|
+
npx kagemusha capture --dry-run
|
|
31
30
|
```
|
|
32
31
|
|
|
33
|
-
## How it works
|
|
34
|
-
|
|
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
|
|
40
|
-
|
|
41
32
|
## Commands
|
|
42
33
|
|
|
43
34
|
| Command | Description |
|
|
44
|
-
|
|
45
|
-
| `kagemusha init` | Interactive setup |
|
|
46
|
-
| `kagemusha
|
|
47
|
-
| `kagemusha
|
|
48
|
-
| `kagemusha
|
|
49
|
-
| `kagemusha
|
|
50
|
-
| `kagemusha
|
|
51
|
-
| `kagemusha
|
|
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 |
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
`init` generates these files:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
kagemusha.config.yaml # base URL, viewport, publish destination
|
|
51
|
+
.kagemusha/definitions.json # one entry per screenshot
|
|
52
|
+
.kagemusha/login.mjs # optional scripted login (CI-friendly)
|
|
53
|
+
.github/workflows/kagemusha.yml
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`kagemusha.config.yaml`:
|
|
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
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Definition (`.kagemusha/definitions.json`):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
[
|
|
74
|
+
{
|
|
75
|
+
"id": "dashboard",
|
|
76
|
+
"url": "/dashboard",
|
|
77
|
+
"capture": { "mode": "fullPage" },
|
|
78
|
+
"hideElements": [".intercom-launcher"],
|
|
79
|
+
"decorations": [
|
|
80
|
+
{ "type": "rect", "target": { "x": 32, "y": 120, "width": 310, "height": 120 } }
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
```
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
Run `kagemusha edit --id dashboard` to set the crop range and add decorations visually.
|
|
54
87
|
|
|
55
|
-
|
|
88
|
+
## Avoiding loading-state screenshots
|
|
56
89
|
|
|
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:
|
|
91
|
+
|
|
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
|
+
}
|
|
57
101
|
```
|
|
58
|
-
|
|
59
|
-
.
|
|
102
|
+
|
|
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.
|
|
104
|
+
|
|
105
|
+
## Authentication
|
|
106
|
+
|
|
107
|
+
If your app needs login, `init` generates a `.kagemusha/login.mjs` skeleton:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
/** @param {import('playwright-chromium').Page} page */
|
|
111
|
+
export const login = async (page) => {
|
|
112
|
+
await page.goto("/login");
|
|
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 ?? "");
|
|
116
|
+
await page.click('button[type="submit"]');
|
|
117
|
+
await page.waitForURL((url) => !url.pathname.startsWith("/login"));
|
|
118
|
+
};
|
|
60
119
|
```
|
|
61
120
|
|
|
62
|
-
|
|
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.
|
|
122
|
+
|
|
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).
|
|
124
|
+
|
|
125
|
+
## Deploying to GitHub Actions
|
|
126
|
+
|
|
127
|
+
`init` generates `.github/workflows/kagemusha.yml`. Required secrets:
|
|
128
|
+
|
|
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
|
|
132
|
+
|
|
133
|
+
Region is auto-detected from `publish.cdnBaseUrl` (`*.s3.<region>.amazonaws.com`), so no `AWS_REGION` env needed.
|
|
134
|
+
|
|
135
|
+
The workflow triggers on `push: main` and runs `kagemusha capture` automatically.
|
|
136
|
+
|
|
137
|
+
## Notifications
|
|
138
|
+
|
|
139
|
+
`kagemusha capture` writes `reports/summary.json` with before/after URLs (when destination is S3 and a real push happened):
|
|
63
140
|
|
|
64
141
|
```json
|
|
65
142
|
{
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
143
|
+
"schemaVersion": "1",
|
|
144
|
+
"timestamp": "2026-05-15T12:34:56.789Z",
|
|
145
|
+
"dryRun": false,
|
|
146
|
+
"canonical": "https://your-bucket.s3.ap-northeast-1.amazonaws.com",
|
|
147
|
+
"counts": { "changed": 1, "unchanged": 5, "new": 2, "missing": 0 },
|
|
148
|
+
"results": [
|
|
72
149
|
{
|
|
73
|
-
"
|
|
74
|
-
"
|
|
75
|
-
"
|
|
76
|
-
|
|
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" } },
|
|
160
|
+
{ "id": "dashboard", "status": "unchanged" }
|
|
77
161
|
]
|
|
78
162
|
}
|
|
79
163
|
```
|
|
80
164
|
|
|
81
|
-
|
|
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).
|
|
166
|
+
|
|
167
|
+
Slack notification (the generated workflow includes this; just set `SLACK_WEBHOOK_URL`):
|
|
82
168
|
|
|
83
169
|
```yaml
|
|
84
|
-
name:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
- uses: actions/checkout@v4
|
|
96
|
-
- uses: actions/setup-node@v4
|
|
97
|
-
with:
|
|
98
|
-
node-version: 20
|
|
99
|
-
- run: npm ci
|
|
100
|
-
- run: npx kagemusha run
|
|
101
|
-
env:
|
|
102
|
-
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
103
|
-
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
170
|
+
- name: Slack notify
|
|
171
|
+
env:
|
|
172
|
+
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
173
|
+
run: |
|
|
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
|
|
104
181
|
```
|
|
105
182
|
|
|
106
|
-
|
|
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.
|
|
184
|
+
|
|
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:
|
|
107
186
|
|
|
108
187
|
```bash
|
|
109
|
-
|
|
110
|
-
bun install
|
|
111
|
-
bun run serve # Start sample app
|
|
112
|
-
bunx kagemusha init # Set up kagemusha
|
|
113
|
-
bunx kagemusha preview # See screenshots
|
|
188
|
+
jq -c -f .kagemusha/notify-slack.jq reports/summary.json
|
|
114
189
|
```
|
|
115
190
|
|
|
116
|
-
|
|
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
|
+
|
|
193
|
+
## Positioning
|
|
194
|
+
|
|
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/).
|
|
196
|
+
|
|
197
|
+
kagemusha is for **post-merge auto-update of help center screenshots**, where stable embeddable URLs matter more than per-commit baseline correctness.
|
|
198
|
+
|
|
199
|
+
## Releasing
|
|
200
|
+
|
|
201
|
+
Automated via [release-please](https://github.com/googleapis/release-please).
|
|
202
|
+
|
|
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.
|
|
117
212
|
|
|
118
|
-
-
|
|
119
|
-
- [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
|
|
213
|
+
The `kagemusha` CLI reads its version from `package.json` at runtime, so release-please only needs to touch one file.
|
|
125
214
|
|
|
126
215
|
## License
|
|
127
216
|
|
|
@@ -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"}
|
package/dist/commands/capture.js
CHANGED
|
@@ -1,9 +1,56 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
1
4
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
5
|
+
import { hasAuthState, resolveLoginScriptPath } from "../lib/auth.js";
|
|
6
|
+
import { handleAwsError } from "../lib/aws-error.js";
|
|
7
|
+
import { createS3Canonical, getCanonicalPath, getOutputDir, } from "../lib/canonical.js";
|
|
3
8
|
import { findProjectRoot, loadConfig, loadDefinitions } from "../lib/config.js";
|
|
9
|
+
import { diffImages } from "../lib/diff.js";
|
|
4
10
|
import { captureScreenshots } from "../lib/screenshot.js";
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
import { cleanupStaging, ensureStagingDirs, getStagingDir, getStagingPath, } from "../lib/staging.js";
|
|
12
|
+
// Schema version of `reports/summary.json`. Bump on any breaking change.
|
|
13
|
+
const SUMMARY_SCHEMA_VERSION = "1";
|
|
14
|
+
const writeSummaryReport = (projectRoot, report) => {
|
|
15
|
+
const reportPath = path.join(projectRoot, "reports", "summary.json");
|
|
16
|
+
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
17
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
18
|
+
};
|
|
19
|
+
// Open a file with the OS's default viewer (Preview on macOS, etc.).
|
|
20
|
+
// `detached + unref` lets the kagemusha process exit while the viewer keeps
|
|
21
|
+
// running. We deliberately avoid `shell: true` so paths with spaces don't get
|
|
22
|
+
// re-split — argv is passed straight through to the program.
|
|
23
|
+
const openInDefaultApp = (filePath) => {
|
|
24
|
+
if (process.platform === "darwin") {
|
|
25
|
+
spawn("open", [filePath], { detached: true, stdio: "ignore" }).unref();
|
|
26
|
+
}
|
|
27
|
+
else if (process.platform === "win32") {
|
|
28
|
+
// `start` is a cmd.exe builtin; we invoke cmd directly. The empty
|
|
29
|
+
// string after `start` is the (required) window title placeholder.
|
|
30
|
+
spawn("cmd", ["/c", "start", "", filePath], {
|
|
31
|
+
detached: true,
|
|
32
|
+
stdio: "ignore",
|
|
33
|
+
}).unref();
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
spawn("xdg-open", [filePath], { detached: true, stdio: "ignore" }).unref();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
export const captureCommand = async (options) => {
|
|
40
|
+
try {
|
|
41
|
+
await runCapture(options);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (handleAwsError(e)) {
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const runCapture = async (options) => {
|
|
52
|
+
const dryRun = options.dryRun === true;
|
|
53
|
+
console.log(chalk.bold(`\n🥷 Kagemusha — Capture${dryRun ? " (dry-run)" : ""}\n`));
|
|
7
54
|
const projectRoot = findProjectRoot();
|
|
8
55
|
const config = loadConfig(projectRoot);
|
|
9
56
|
let definitions = loadDefinitions(projectRoot);
|
|
@@ -12,27 +59,216 @@ export async function captureCommand(options) {
|
|
|
12
59
|
definitions = definitions.filter((d) => ids.includes(d.id));
|
|
13
60
|
}
|
|
14
61
|
if (definitions.length === 0) {
|
|
15
|
-
console.log(chalk.yellow("No screenshot definitions
|
|
62
|
+
console.log(chalk.yellow("No screenshot definitions to capture.\n"));
|
|
16
63
|
return;
|
|
17
64
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
65
|
+
// Auto-login: if a login script exists (auth.scriptPath, or default
|
|
66
|
+
// .kagemusha/login.mjs) but no saved session is on disk, run it before
|
|
67
|
+
// capturing. This is what makes CI work with no pre-baked storage state.
|
|
68
|
+
if (!hasAuthState(projectRoot) &&
|
|
69
|
+
resolveLoginScriptPath(config, projectRoot)) {
|
|
70
|
+
console.log(chalk.blue("🔐 No saved session found, running login script...\n"));
|
|
71
|
+
const { loginCommand } = await import("./login.js");
|
|
72
|
+
await loginCommand();
|
|
73
|
+
// loginCommand absorbs LoginError and sets exitCode internally.
|
|
74
|
+
// If no auth-state was produced, abort capture so we don't screenshot
|
|
75
|
+
// the login screen.
|
|
76
|
+
if (!hasAuthState(projectRoot)) {
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
25
80
|
}
|
|
81
|
+
ensureStagingDirs(projectRoot);
|
|
82
|
+
const threshold = options.threshold
|
|
83
|
+
? Number.parseFloat(options.threshold)
|
|
84
|
+
: (config.screenshot.defaultDiffThreshold ?? 0.005);
|
|
85
|
+
const remote = createS3Canonical(config);
|
|
86
|
+
const outputDir = getOutputDir(config, projectRoot);
|
|
87
|
+
const stagingDir = getStagingDir(projectRoot);
|
|
88
|
+
if (remote) {
|
|
89
|
+
console.log(chalk.gray(` canonical: ${remote.label()}`));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(chalk.gray(` canonical: ${outputDir} (local)`));
|
|
93
|
+
}
|
|
94
|
+
// 1. Capture into staging (annotated)
|
|
95
|
+
console.log(chalk.blue(`\n📸 Capturing ${definitions.length} screenshot(s) to staging...\n`));
|
|
96
|
+
const failures = await captureScreenshots(config, definitions, projectRoot, {
|
|
97
|
+
outputDir: stagingDir,
|
|
98
|
+
});
|
|
99
|
+
const failureReasons = new Map(failures.map((f) => [f.id, f.reason]));
|
|
100
|
+
const results = [];
|
|
101
|
+
// Track final paths (where the user can find each capture after the run)
|
|
102
|
+
const finalPathFor = new Map();
|
|
103
|
+
const pendingPushes = [];
|
|
104
|
+
for (const def of definitions) {
|
|
105
|
+
const canonicalPath = getCanonicalPath(config, projectRoot, def.id);
|
|
106
|
+
const stagingPath = getStagingPath(projectRoot, def.id);
|
|
107
|
+
if (!fs.existsSync(stagingPath)) {
|
|
108
|
+
results.push({
|
|
109
|
+
id: def.id,
|
|
110
|
+
status: "missing",
|
|
111
|
+
reason: failureReasons.get(def.id),
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// Pull canonical from remote (S3) into outputDir; for local mode just check existence
|
|
116
|
+
const fetchResult = remote
|
|
117
|
+
? await remote.fetch(def.id, canonicalPath)
|
|
118
|
+
: fs.existsSync(canonicalPath)
|
|
119
|
+
? "ok"
|
|
120
|
+
: "not-found";
|
|
121
|
+
const queuePush = (push) => {
|
|
122
|
+
if (dryRun) {
|
|
123
|
+
finalPathFor.set(def.id, push.stagingPath);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
pendingPushes.push(push);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
// New: no canonical yet — adopt staging as canonical (unless dry-run)
|
|
130
|
+
if (fetchResult === "not-found") {
|
|
131
|
+
const result = { id: def.id, status: "new" };
|
|
132
|
+
results.push(result);
|
|
133
|
+
queuePush({
|
|
134
|
+
id: def.id,
|
|
135
|
+
stagingPath,
|
|
136
|
+
canonicalPath,
|
|
137
|
+
onUrls: (urls) => {
|
|
138
|
+
if (urls)
|
|
139
|
+
result.urls = urls;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const result = await diffImages(canonicalPath, stagingPath);
|
|
145
|
+
if (result.match) {
|
|
146
|
+
results.push({ id: def.id, status: "unchanged" });
|
|
147
|
+
fs.rmSync(stagingPath, { force: true });
|
|
148
|
+
finalPathFor.set(def.id, canonicalPath);
|
|
149
|
+
}
|
|
150
|
+
else if (result.reason === "layout-diff") {
|
|
151
|
+
const item = {
|
|
152
|
+
id: def.id,
|
|
153
|
+
status: "changed",
|
|
154
|
+
reason: "layout-diff",
|
|
155
|
+
canonical: result.canonical,
|
|
156
|
+
staging: result.staging,
|
|
157
|
+
};
|
|
158
|
+
results.push(item);
|
|
159
|
+
queuePush({
|
|
160
|
+
id: def.id,
|
|
161
|
+
stagingPath,
|
|
162
|
+
canonicalPath,
|
|
163
|
+
onUrls: (urls) => {
|
|
164
|
+
if (urls)
|
|
165
|
+
item.urls = urls;
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
const item = {
|
|
171
|
+
id: def.id,
|
|
172
|
+
status: "changed",
|
|
173
|
+
reason: "pixel-diff",
|
|
174
|
+
diffPercentage: result.diffPercentage,
|
|
175
|
+
};
|
|
176
|
+
results.push(item);
|
|
177
|
+
queuePush({
|
|
178
|
+
id: def.id,
|
|
179
|
+
stagingPath,
|
|
180
|
+
canonicalPath,
|
|
181
|
+
onUrls: (urls) => {
|
|
182
|
+
if (urls)
|
|
183
|
+
item.urls = urls;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Parallel promote — push to remote + copy to local outputDir.
|
|
189
|
+
// Promise.all keeps S3 throughput high while node manages local fs serially.
|
|
190
|
+
if (pendingPushes.length > 0) {
|
|
191
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
192
|
+
await Promise.all(pendingPushes.map(async ({ id, stagingPath, canonicalPath, onUrls }) => {
|
|
193
|
+
// Push to remote first so a failure doesn't leave local ahead of S3
|
|
194
|
+
const urls = remote ? await remote.push(id, stagingPath) : undefined;
|
|
195
|
+
fs.copyFileSync(stagingPath, canonicalPath);
|
|
196
|
+
fs.rmSync(stagingPath, { force: true });
|
|
197
|
+
finalPathFor.set(id, canonicalPath);
|
|
198
|
+
onUrls(urls);
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
// 3. Print summary
|
|
202
|
+
const changed = results.filter((r) => r.status === "changed");
|
|
203
|
+
const unchanged = results.filter((r) => r.status === "unchanged");
|
|
204
|
+
const newly = results.filter((r) => r.status === "new");
|
|
205
|
+
const missing = results.filter((r) => r.status === "missing");
|
|
206
|
+
for (const r of results) {
|
|
207
|
+
if (r.status === "unchanged") {
|
|
208
|
+
console.log(chalk.gray(` ✓ ${r.id}`));
|
|
209
|
+
}
|
|
210
|
+
else if (r.status === "new") {
|
|
211
|
+
const action = dryRun ? "would be added" : "added to canonical";
|
|
212
|
+
console.log(chalk.cyan(` + ${r.id} (${action})`));
|
|
213
|
+
}
|
|
214
|
+
else if (r.status === "missing") {
|
|
215
|
+
const detail = r.reason ? `: ${r.reason}` : "";
|
|
216
|
+
console.log(chalk.red(` ✗ ${r.id} (capture failed${detail})`));
|
|
217
|
+
}
|
|
218
|
+
else if (r.status === "changed") {
|
|
219
|
+
const detail = r.reason === "pixel-diff"
|
|
220
|
+
? `${r.diffPercentage.toFixed(2)}%`
|
|
221
|
+
: `layout-diff: ${r.canonical.width}×${r.canonical.height} → ${r.staging.width}×${r.staging.height}`;
|
|
222
|
+
const action = dryRun ? "→ would update" : "→ updated";
|
|
223
|
+
console.log(chalk.yellow(` ~ ${r.id} (${detail}) ${chalk.gray(action)}`));
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 4. Cleanup staging — applied entries are already removed individually,
|
|
227
|
+
// so this only matters when nothing changed (everything was unchanged).
|
|
228
|
+
if (changed.length === 0 && newly.length === 0) {
|
|
229
|
+
cleanupStaging(projectRoot);
|
|
230
|
+
}
|
|
231
|
+
console.log("");
|
|
232
|
+
console.log(chalk.bold(`changed: ${changed.length} / unchanged: ${unchanged.length} / new: ${newly.length}` +
|
|
233
|
+
(missing.length > 0 ? ` / missing: ${missing.length}` : "")));
|
|
234
|
+
if (dryRun && (changed.length > 0 || newly.length > 0)) {
|
|
235
|
+
console.log(chalk.gray(`\nDrop --dry-run to update canonical${remote ? ` (${remote.label()})` : ""}.`));
|
|
236
|
+
}
|
|
237
|
+
// 5. Write the structured report.
|
|
238
|
+
// `reports/summary.json` is part of kagemusha's PUBLIC API — see README
|
|
239
|
+
// "Notifications" section. Bump `schemaVersion` (or kagemusha major) on
|
|
240
|
+
// breaking changes to the shape.
|
|
241
|
+
writeSummaryReport(projectRoot, {
|
|
242
|
+
schemaVersion: SUMMARY_SCHEMA_VERSION,
|
|
243
|
+
timestamp: new Date().toISOString(),
|
|
244
|
+
dryRun,
|
|
245
|
+
canonical: remote ? remote.label() : `${outputDir} (local)`,
|
|
246
|
+
counts: {
|
|
247
|
+
changed: changed.length,
|
|
248
|
+
unchanged: unchanged.length,
|
|
249
|
+
new: newly.length,
|
|
250
|
+
missing: missing.length,
|
|
251
|
+
},
|
|
252
|
+
results,
|
|
253
|
+
});
|
|
254
|
+
// 6. Open changed/new results in default viewer
|
|
26
255
|
if (options.open) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
256
|
+
for (const r of [...changed, ...newly]) {
|
|
257
|
+
const p = finalPathFor.get(r.id);
|
|
258
|
+
if (p && fs.existsSync(p)) {
|
|
259
|
+
openInDefaultApp(p);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Exit code: 1 if dry-run found pixel diffs over threshold (CI gate use case)
|
|
264
|
+
if (dryRun) {
|
|
265
|
+
const overThreshold = changed.filter((r) => r.status === "changed" &&
|
|
266
|
+
r.reason === "pixel-diff" &&
|
|
267
|
+
r.diffPercentage / 100 > threshold);
|
|
268
|
+
if (overThreshold.length > 0) {
|
|
269
|
+
process.exitCode = 1;
|
|
33
270
|
}
|
|
34
|
-
console.log(chalk.gray("\nPress Ctrl+C to close preview.\n"));
|
|
35
|
-
await new Promise(() => { });
|
|
36
271
|
}
|
|
37
|
-
|
|
272
|
+
console.log("");
|
|
273
|
+
};
|
|
38
274
|
//# sourceMappingURL=capture.js.map
|