@voila.dev/cliche 0.1.10 → 0.1.12
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 +89 -134
- package/apps/album/src/shots.ts +3 -3
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/setup.js +9 -9
- package/dist/setup.js.map +1 -1
- package/dist/upload.d.ts +11 -4
- package/dist/upload.d.ts.map +1 -1
- package/dist/upload.js +32 -12
- package/dist/upload.js.map +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +2 -3
- package/src/cli.ts +6 -2
- package/src/setup.ts +9 -9
- package/src/upload.ts +35 -13
package/README.md
CHANGED
|
@@ -1,69 +1,35 @@
|
|
|
1
|
-
<img src="assets/og.png" alt="cliche
|
|
1
|
+
<img src="https://raw.githubusercontent.com/voila-voila-dev/cliche/main/assets/og.png" alt="cliche: screenshot any page, get a URL. Just Bun." width="100%">
|
|
2
2
|
|
|
3
3
|
# cliche 📸
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Screenshot any page, get a URL. No Playwright, no browser download, no
|
|
6
|
+
dependencies. Just Bun. Ouistiti ! 🐒
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
>
|
|
14
|
-
> This very image was captured by cliche, from an HTML file, in one command.
|
|
15
|
-
> [cliche.voila.dev](https://cliche.voila.dev)
|
|
8
|
+
Capture runs on [`Bun.WebView`](https://bun.com/docs/runtime/webview), upload
|
|
9
|
+
on `Bun.S3Client`. Everything happens on your machine; the only thing that
|
|
10
|
+
leaves it is the upload to your own bucket. Requires Bun 1.4.0 or newer.
|
|
11
|
+
|
|
12
|
+
[cliche.voila.dev](https://cliche.voila.dev)
|
|
16
13
|
|
|
17
14
|
```sh
|
|
18
|
-
bunx @voila.dev/cliche
|
|
15
|
+
bunx @voila.dev/cliche http://localhost:3000/missions shot.png --upload --prefix pr-123
|
|
19
16
|
```
|
|
20
17
|
|
|
21
18
|
```
|
|
22
|
-
Captured
|
|
23
|
-
Uploaded
|
|
24
|
-
https://assets.example.com/pr-123/2026-08-29-
|
|
19
|
+
Captured http://localhost:3000/missions -> shot.png
|
|
20
|
+
Uploaded shot.png -> pr-123/2026-08-29-shot-d1cf773c.png
|
|
21
|
+
https://assets.example.com/pr-123/2026-08-29-shot-d1cf773c.png
|
|
25
22
|
```
|
|
26
23
|
|
|
27
|
-
|
|
28
|
-
`` line for `gh pr edit --body` instead.
|
|
24
|
+
Paste the URL anywhere, or add `--markdown` for an `` line.
|
|
29
25
|
|
|
30
|
-
|
|
31
|
-
a local MCP server:
|
|
26
|
+
## Why
|
|
32
27
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```
|
|
28
|
+
A diff shows the code. A screenshot shows what the user gets. Put both in the
|
|
29
|
+
PR and review stops being guesswork.
|
|
36
30
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
**1. Front-end PRs you can SEE.** A diff tells the reviewer what changed in
|
|
40
|
-
the code; a before/after tells them what changed *for the user*. When every
|
|
41
|
-
UI pull request ships with its pixels, review gets faster, regressions get
|
|
42
|
-
caught at a glance, and "looks good to me" actually means someone looked.
|
|
43
|
-
|
|
44
|
-
**2. You're building a photo album by accident.** Content-hashed keys mean
|
|
45
|
-
every shot stays in your bucket forever, dated and browsable. Six months in,
|
|
46
|
-
you own something no git history gives you: what your app *looked like*,
|
|
47
|
-
release by release. Retrospectives, launch recaps, "remember when the
|
|
48
|
-
dashboard looked like this?" — it's all just sitting in S3.
|
|
49
|
-
|
|
50
|
-
The usual capture path drags in a Playwright install, a 100MB browser
|
|
51
|
-
download, and a place to host the image. `cliche` is a single
|
|
52
|
-
zero-dependency CLI (and MCP server):
|
|
53
|
-
|
|
54
|
-
- **Capture** — `Bun.WebView`: the system WKWebView on macOS (nothing to
|
|
55
|
-
install), your installed Chrome via CDP on Linux/Windows. Retina-crisp PNGs.
|
|
56
|
-
- **Upload** — `Bun.S3Client`: works with Cloudflare R2, AWS S3, MinIO,
|
|
57
|
-
anything that speaks S3. Keys are content-hashed, so re-uploads never break
|
|
58
|
-
old links.
|
|
59
|
-
- **URL out** — one public URL per file on stdout (progress on stderr), or
|
|
60
|
-
`--markdown` for `` lines, so you can pipe it wherever the
|
|
61
|
-
review happens.
|
|
62
|
-
- **MCP in** — `cliche mcp` serves the same two tools (`screenshot`, `upload`)
|
|
63
|
-
over stdio to any MCP client, hand-rolled JSON-RPC, still zero dependencies.
|
|
64
|
-
Everything runs locally.
|
|
65
|
-
|
|
66
|
-
Requires Bun ≥ 1.4.0.
|
|
31
|
+
Keys are content-hashed and dated, so nothing overwrites anything. Your bucket
|
|
32
|
+
ends up holding what your app looked like, release by release.
|
|
67
33
|
|
|
68
34
|
## Capture
|
|
69
35
|
|
|
@@ -73,18 +39,18 @@ cliche <url> <out.png> [options]
|
|
|
73
39
|
|
|
74
40
|
| Option | What it does |
|
|
75
41
|
| --- | --- |
|
|
76
|
-
| `--viewport <WxH>` | Viewport, default `1440x900` (`390x844` for mobile
|
|
77
|
-
| `--
|
|
42
|
+
| `--viewport <WxH>` | Viewport, default `1440x900` (`390x844` for mobile). |
|
|
43
|
+
| `--full-page` | Grow the viewport to the full page height. |
|
|
44
|
+
| `--wait-for <css>` | Hold the shot until a selector exists. SPAs render late. |
|
|
78
45
|
| `--scroll-to <css>` | Scroll a component into view before shooting. |
|
|
79
|
-
| `--settle <ms>` | Let fonts
|
|
80
|
-
| `--full-page` | Grow the viewport to the full page height — the whole page in one shot. |
|
|
46
|
+
| `--settle <ms>` | Let fonts, images and animations finish. Default `1500`. |
|
|
81
47
|
| `--local-storage k=v` | Seed the target origin's localStorage (repeatable). |
|
|
82
48
|
|
|
83
|
-
|
|
84
|
-
|
|
49
|
+
`--local-storage` is how you shoot logged-in screens: seed the session token
|
|
50
|
+
and the page boots authenticated, no login form to script.
|
|
85
51
|
|
|
86
52
|
```sh
|
|
87
|
-
cliche http://localhost:
|
|
53
|
+
cliche http://localhost:3000/admin dashboard.png \
|
|
88
54
|
--local-storage "myapp.session-token=$TOKEN" \
|
|
89
55
|
--wait-for '[data-testid=dashboard]'
|
|
90
56
|
```
|
|
@@ -92,49 +58,56 @@ cliche http://localhost:4001/admin dashboard.png \
|
|
|
92
58
|
## Upload
|
|
93
59
|
|
|
94
60
|
```sh
|
|
95
|
-
cliche upload [--prefix pr-123] [--markdown] *.png
|
|
61
|
+
cliche upload [--prefix pr-123] [--markdown] *.png
|
|
96
62
|
```
|
|
97
63
|
|
|
98
|
-
|
|
99
|
-
|
|
64
|
+
Every setting is read from a `CLICHE_`-prefixed variable and passed to
|
|
65
|
+
`Bun.S3Client` explicitly, so cliche never picks up `S3_*` or `AWS_*`
|
|
66
|
+
variables your shell carries for something else.
|
|
100
67
|
|
|
101
68
|
| Variable | Example |
|
|
102
69
|
| --- | --- |
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
70
|
+
| `CLICHE_BUCKET` | `assets-dev` |
|
|
71
|
+
| `CLICHE_ENDPOINT` | `https://<account>.r2.cloudflarestorage.com` (omit on AWS) |
|
|
72
|
+
| `CLICHE_ACCESS_KEY_ID` | your access key |
|
|
73
|
+
| `CLICHE_SECRET_ACCESS_KEY` | your secret key |
|
|
74
|
+
| `CLICHE_REGION` | `auto` (defaults to `us-east-1` on AWS) |
|
|
75
|
+
| `CLICHE_PUBLIC_URL` | `https://assets.example.com`, the bucket's public domain |
|
|
107
76
|
|
|
108
|
-
Objects are keyed `<prefix>/<yyyy-mm-dd>-<name>-<content-hash>.<ext
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`mission-detail-after.png` → ``.
|
|
77
|
+
Objects are keyed `<prefix>/<yyyy-mm-dd>-<name>-<content-hash>.<ext>`. The
|
|
78
|
+
caption comes from the file name, so name files the way you want them read:
|
|
79
|
+
`mission-detail-after.png` becomes ``.
|
|
112
80
|
|
|
113
81
|
> [!WARNING]
|
|
114
|
-
>
|
|
115
|
-
>
|
|
82
|
+
> Point cliche at a bucket you're happy to have public: PR descriptions live
|
|
83
|
+
> forever. Never capture real user data.
|
|
116
84
|
|
|
117
|
-
|
|
85
|
+
### R2 in one command
|
|
118
86
|
|
|
119
87
|
```sh
|
|
120
|
-
cliche
|
|
88
|
+
bunx @voila.dev/cliche setup [--bucket my-shots]
|
|
121
89
|
```
|
|
122
90
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
`prefix`, `markdown`. Without `out` the shot lands in a temp file; with
|
|
128
|
-
`upload: true` the result is the public URL.
|
|
129
|
-
- **`upload`** — `files` (required), `prefix`, `markdown`.
|
|
91
|
+
Through your existing `wrangler login`, this creates the bucket, enables its
|
|
92
|
+
public `r2.dev` URL and writes the `CLICHE_*` block to `.env`. Wrangler can't
|
|
93
|
+
mint API keys, so the command prints the dashboard link for the two you paste
|
|
94
|
+
back in. Any other S3-compatible service works with the same variables.
|
|
130
95
|
|
|
131
|
-
|
|
96
|
+
## MCP server
|
|
132
97
|
|
|
133
98
|
```sh
|
|
134
|
-
claude mcp add cliche -- bunx @voila.dev/cliche mcp
|
|
99
|
+
claude mcp add cliche -- bunx @voila.dev/cliche mcp
|
|
135
100
|
```
|
|
136
101
|
|
|
137
|
-
|
|
102
|
+
Two tools over stdio:
|
|
103
|
+
|
|
104
|
+
- **`screenshot`**: `url` (required), `out`, `viewport`, `full_page`,
|
|
105
|
+
`wait_for`, `scroll_to`, `settle_ms`, `local_storage`, `upload`, `prefix`,
|
|
106
|
+
`markdown`. Without `out` the shot lands in a temp file; with `upload: true`
|
|
107
|
+
the result is the public URL.
|
|
108
|
+
- **`upload`**: `files` (required), `prefix`, `markdown`.
|
|
109
|
+
|
|
110
|
+
Or in a project's `.mcp.json`:
|
|
138
111
|
|
|
139
112
|
```json
|
|
140
113
|
{
|
|
@@ -144,74 +117,56 @@ or in a project's `.mcp.json`:
|
|
|
144
117
|
}
|
|
145
118
|
```
|
|
146
119
|
|
|
147
|
-
##
|
|
148
|
-
|
|
149
|
-
```ts
|
|
150
|
-
import { capture, upload } from "@voila.dev/cliche";
|
|
151
|
-
|
|
152
|
-
await capture({
|
|
153
|
-
url: "http://localhost:3000",
|
|
154
|
-
out: "home.png",
|
|
155
|
-
viewport: { width: 390, height: 844 },
|
|
156
|
-
waitFor: "main",
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const [shot] = await upload({ files: ["home.png"], prefix: "pr-7" });
|
|
160
|
-
console.log(shot.markdown);
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
## One-command S3 setup (Cloudflare R2)
|
|
120
|
+
## The album 📔
|
|
164
121
|
|
|
165
122
|
```sh
|
|
166
|
-
bunx @voila.dev/cliche
|
|
123
|
+
bunx @voila.dev/cliche album
|
|
167
124
|
```
|
|
168
125
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
`
|
|
172
|
-
keys — the command prints the exact dashboard link, you paste two values,
|
|
173
|
-
done. Any other S3-compatible service works too with the plain env vars.
|
|
126
|
+
Opens your bucket as a photo album: every cliché, grouped by month, with a
|
|
127
|
+
filter and a lightbox. A small Bun app that ships in the package; source in
|
|
128
|
+
`apps/album`.
|
|
174
129
|
|
|
175
|
-
|
|
130
|
+
<img src="https://raw.githubusercontent.com/voila-voila-dev/cliche/main/assets/album-preview.png" alt="album: real tries.care pages captured by cliche, grouped by month" width="100%">
|
|
176
131
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
132
|
+
Running on [tries.care](https://tries.care)'s screenshots bucket.
|
|
133
|
+
|
|
134
|
+
## Claude Code skill
|
|
180
135
|
|
|
181
136
|
```sh
|
|
182
|
-
bunx @voila.dev/cliche
|
|
137
|
+
bunx @voila.dev/cliche skill
|
|
183
138
|
```
|
|
184
139
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
140
|
+
Writes a PR-screenshots skill to `.claude/skills/pr-screenshots/SKILL.md`, so
|
|
141
|
+
Claude captures, uploads and embeds before/after shots whenever a PR touches
|
|
142
|
+
something visible. Also served at
|
|
143
|
+
[cliche.voila.dev/skill.md](https://cliche.voila.dev/skill.md).
|
|
189
144
|
|
|
190
|
-
|
|
191
|
-
served from its PR-screenshots bucket.)*
|
|
145
|
+
## Programmatic API
|
|
192
146
|
|
|
193
|
-
|
|
147
|
+
```ts
|
|
148
|
+
import { capture, upload } from "@voila.dev/cliche";
|
|
194
149
|
|
|
195
|
-
|
|
196
|
-
|
|
150
|
+
await capture({
|
|
151
|
+
url: "http://localhost:3000",
|
|
152
|
+
out: "home.png",
|
|
153
|
+
viewport: { width: 390, height: 844 },
|
|
154
|
+
waitFor: "main",
|
|
155
|
+
});
|
|
197
156
|
|
|
198
|
-
|
|
199
|
-
|
|
157
|
+
const [shot] = await upload({ files: ["home.png"], prefix: "pr-7" });
|
|
158
|
+
console.log(shot.url);
|
|
200
159
|
```
|
|
201
160
|
|
|
202
|
-
(Also served at [cliche.voila.dev/skill.md](https://cliche.voila.dev/skill.md)
|
|
203
|
-
if you'd rather `curl` it.) With the skill in place, Claude captures, uploads,
|
|
204
|
-
and embeds before/after screenshots whenever a PR touches something visible.
|
|
205
|
-
|
|
206
161
|
## Platform notes
|
|
207
162
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
163
|
+
macOS uses the system WKWebView, nothing to install, and shots come out at the
|
|
164
|
+
display's scale factor. Linux and Windows drive an installed Chrome, Chromium,
|
|
165
|
+
Edge or Brave over the DevTools Protocol, which is why capture also works on
|
|
166
|
+
GitHub Actions runners.
|
|
167
|
+
|
|
168
|
+
`Bun.WebView` is marked experimental by Bun, and cliche tracks it as it
|
|
169
|
+
settles.
|
|
215
170
|
|
|
216
171
|
## License
|
|
217
172
|
|
package/apps/album/src/shots.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { publicBaseUrl } from "../../../src/upload.ts";
|
|
1
|
+
import { createClient, publicBaseUrl } from "../../../src/upload.ts";
|
|
2
2
|
|
|
3
3
|
export interface Shot {
|
|
4
4
|
readonly key: string;
|
|
@@ -15,7 +15,7 @@ function dateOf(key: string, lastModified: string | undefined): string {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async function listBucket(baseUrl: string): Promise<Array<Shot>> {
|
|
18
|
-
const client =
|
|
18
|
+
const client = createClient();
|
|
19
19
|
const shots: Array<Shot> = [];
|
|
20
20
|
let continuationToken: string | undefined;
|
|
21
21
|
do {
|
|
@@ -78,7 +78,7 @@ export async function listShots(): Promise<{ demo: boolean; shots: Array<Shot> }
|
|
|
78
78
|
return { demo: false, shots: await listBucket(baseUrl) };
|
|
79
79
|
} catch (error) {
|
|
80
80
|
console.error(
|
|
81
|
-
`No bucket configured (${error instanceof Error ? error.message.split(":")[0] : error})
|
|
81
|
+
`No bucket configured (${error instanceof Error ? error.message.split(":")[0] : error}). Serving the demo album; run \`bunx @voila.dev/cliche setup\` to plug a real one.`,
|
|
82
82
|
);
|
|
83
83
|
return { demo: true, shots: demoShots() };
|
|
84
84
|
}
|
package/dist/cli.js
CHANGED
|
@@ -39,8 +39,12 @@ Upload options:
|
|
|
39
39
|
--prefix <name> Object key prefix, e.g. pr-123.
|
|
40
40
|
--markdown Print  lines instead of URLs.
|
|
41
41
|
|
|
42
|
-
Upload configuration (
|
|
43
|
-
|
|
42
|
+
Upload configuration (environment, all prefixed so nothing else collides):
|
|
43
|
+
CLICHE_BUCKET Bucket name.
|
|
44
|
+
CLICHE_ENDPOINT S3 endpoint (omit on AWS).
|
|
45
|
+
CLICHE_ACCESS_KEY_ID Access key.
|
|
46
|
+
CLICHE_SECRET_ACCESS_KEY Secret key.
|
|
47
|
+
CLICHE_REGION Region (defaults to us-east-1 on AWS).
|
|
44
48
|
CLICHE_PUBLIC_URL Public base URL of the bucket (custom domain).
|
|
45
49
|
|
|
46
50
|
URLs go to stdout, progress to stderr. Ouistiti !`;
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";;;;;;;;;AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AACnC,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC1C,OAAO,EAAE,MAAM,EAAqB,MAAM,aAAa,CAAC;AAExD,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/setup.js
CHANGED
|
@@ -30,15 +30,15 @@ export function parsePublicUrl(devUrlOutput) {
|
|
|
30
30
|
function environmentBlock(bucket, publicUrl, accountId) {
|
|
31
31
|
const lines = [
|
|
32
32
|
"",
|
|
33
|
-
"# cliche
|
|
34
|
-
`
|
|
33
|
+
"# cliche, added by `cliche setup` (https://cliche.voila.dev)",
|
|
34
|
+
`CLICHE_BUCKET=${bucket}`,
|
|
35
35
|
accountId === null
|
|
36
|
-
? "#
|
|
37
|
-
: `
|
|
36
|
+
? "# CLICHE_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com"
|
|
37
|
+
: `CLICHE_ENDPOINT=https://${accountId}.r2.cloudflarestorage.com`,
|
|
38
38
|
...(publicUrl === null ? [] : [`CLICHE_PUBLIC_URL=${publicUrl}`]),
|
|
39
39
|
`# Mint the two keys at ${DASHBOARD_TOKENS_URL} (Object Read & Write on ${bucket}):`,
|
|
40
|
-
"#
|
|
41
|
-
"#
|
|
40
|
+
"# CLICHE_ACCESS_KEY_ID=",
|
|
41
|
+
"# CLICHE_SECRET_ACCESS_KEY=",
|
|
42
42
|
"",
|
|
43
43
|
];
|
|
44
44
|
return lines.join("\n");
|
|
@@ -65,8 +65,8 @@ export async function setup(bucket) {
|
|
|
65
65
|
const publicUrl = parsePublicUrl(devUrl.output);
|
|
66
66
|
const environmentFile = Bun.file(".env");
|
|
67
67
|
const existing = (await environmentFile.exists()) ? await environmentFile.text() : "";
|
|
68
|
-
if (existing.includes("
|
|
69
|
-
console.error(".env already has
|
|
68
|
+
if (existing.includes("CLICHE_BUCKET=")) {
|
|
69
|
+
console.error(".env already has a CLICHE_BUCKET. Printing the block instead of appending:");
|
|
70
70
|
console.log(environmentBlock(bucket, publicUrl, accountId));
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
@@ -77,7 +77,7 @@ export async function setup(bucket) {
|
|
|
77
77
|
Almost there — one last step wrangler cannot do:
|
|
78
78
|
1. Open ${DASHBOARD_TOKENS_URL}
|
|
79
79
|
2. Create an API token with "Object Read & Write" on "${bucket}"
|
|
80
|
-
3. Put the two values in .env as
|
|
80
|
+
3. Put the two values in .env as CLICHE_ACCESS_KEY_ID / CLICHE_SECRET_ACCESS_KEY
|
|
81
81
|
|
|
82
82
|
Then take your first cliché:
|
|
83
83
|
bunx @voila.dev/cliche https://example.com shot.png --upload`);
|
package/dist/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,oBAAoB,GAAG,yDAAyD,CAAC;AAEvF,KAAK,UAAU,QAAQ,CACrB,IAAmB,EACnB,UAAqC,EAAE;IAEvC,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,EAAE;QAC1D,KAAK,EAAE,SAAS;QAChB,MAAM,EAAE,OAAO,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;QACzD,MAAM,EAAE,OAAO,CAAC,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM;KAC1D,CAAC,CAAC;IACH,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,CAAC;IACzC,MAAM,MAAM,GACV,OAAO,CAAC,WAAW,KAAK,IAAI;QAC1B,CAAC,CAAC,EAAE;QACJ,CAAC,CAAC,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;YAC9C,CAAC,MAAM,IAAI,QAAQ,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,YAAoB;IACjD,4EAA4E;IAC5E,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,YAAY,CAAC,KAAK,CAAC,mBAAmB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IACxE,OAAO,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,YAAoB;IACjD,OAAO,YAAY,CAAC,KAAK,CAAC,+BAA+B,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;AAC1E,CAAC;AAED,SAAS,gBAAgB,CACvB,MAAc,EACd,SAAwB,EACxB,SAAwB;IAExB,MAAM,KAAK,GAAG;QACZ,EAAE;QACF
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../src/setup.ts"],"names":[],"mappings":"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"}
|
package/dist/upload.d.ts
CHANGED
|
@@ -16,12 +16,19 @@ export interface ObjectWriter {
|
|
|
16
16
|
type: string;
|
|
17
17
|
}): Promise<unknown>;
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Every setting is read from a CLICHE_-prefixed variable and handed to
|
|
21
|
+
* Bun.S3Client explicitly, so cliche never picks up the S3_/AWS_ variables
|
|
22
|
+
* a shell may already carry for something else.
|
|
23
|
+
*/
|
|
24
|
+
export declare function s3Options(): Bun.S3Options;
|
|
25
|
+
/** An S3 client for the configured bucket. */
|
|
26
|
+
export declare function createClient(): Bun.S3Client;
|
|
19
27
|
export declare function publicBaseUrl(): string;
|
|
20
28
|
/**
|
|
21
|
-
* Upload images to the S3-compatible bucket described by the
|
|
22
|
-
* environment variables
|
|
23
|
-
*
|
|
24
|
-
* public base URL (custom domains, R2 public buckets).
|
|
29
|
+
* Upload images to the S3-compatible bucket described by the CLICHE_
|
|
30
|
+
* environment variables, and return one entry per file. `CLICHE_PUBLIC_URL`
|
|
31
|
+
* sets the public base URL (custom domains, R2 public buckets).
|
|
25
32
|
*/
|
|
26
33
|
export declare function upload(options: UploadOptions, writer?: ObjectWriter): Promise<Array<UploadedFile>>;
|
|
27
34
|
//# sourceMappingURL=upload.d.ts.map
|
package/dist/upload.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpF;AAED,wBAAgB,aAAa,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,yEAAyE;AACzE,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACpF;AAED;;;;GAIG;AACH,wBAAgB,SAAS,IAAI,GAAG,CAAC,SAAS,CAkBzC;AAED,8CAA8C;AAC9C,wBAAgB,YAAY,IAAI,GAAG,CAAC,QAAQ,CAE3C;AAED,wBAAgB,aAAa,IAAI,MAAM,CAMtC;AAED;;;;GAIG;AACH,wBAAsB,MAAM,CAAC,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC,CAoBxG"}
|
package/dist/upload.js
CHANGED
|
@@ -7,29 +7,49 @@ const CONTENT_TYPES = {
|
|
|
7
7
|
".webp": "image/webp",
|
|
8
8
|
".gif": "image/gif",
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Every setting is read from a CLICHE_-prefixed variable and handed to
|
|
12
|
+
* Bun.S3Client explicitly, so cliche never picks up the S3_/AWS_ variables
|
|
13
|
+
* a shell may already carry for something else.
|
|
14
|
+
*/
|
|
15
|
+
export function s3Options() {
|
|
16
|
+
const bucket = process.env.CLICHE_BUCKET;
|
|
17
|
+
if (bucket === undefined) {
|
|
18
|
+
throw new Error("No bucket configured: set CLICHE_BUCKET (plus CLICHE_ENDPOINT, CLICHE_ACCESS_KEY_ID and CLICHE_SECRET_ACCESS_KEY). Run `cliche setup` to create one on Cloudflare R2.");
|
|
19
|
+
}
|
|
20
|
+
const endpoint = process.env.CLICHE_ENDPOINT;
|
|
21
|
+
const accessKeyId = process.env.CLICHE_ACCESS_KEY_ID;
|
|
22
|
+
const secretAccessKey = process.env.CLICHE_SECRET_ACCESS_KEY;
|
|
23
|
+
const region = process.env.CLICHE_REGION;
|
|
24
|
+
return {
|
|
25
|
+
bucket,
|
|
26
|
+
...(endpoint === undefined ? {} : { endpoint }),
|
|
27
|
+
...(accessKeyId === undefined ? {} : { accessKeyId }),
|
|
28
|
+
...(secretAccessKey === undefined ? {} : { secretAccessKey }),
|
|
29
|
+
...(region === undefined ? {} : { region }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** An S3 client for the configured bucket. */
|
|
33
|
+
export function createClient() {
|
|
34
|
+
return new Bun.S3Client(s3Options());
|
|
35
|
+
}
|
|
10
36
|
export function publicBaseUrl() {
|
|
11
37
|
const configured = process.env.CLICHE_PUBLIC_URL;
|
|
12
38
|
if (configured !== undefined)
|
|
13
39
|
return configured.replace(/\/$/, "");
|
|
14
|
-
const bucket
|
|
15
|
-
if (bucket === undefined) {
|
|
16
|
-
throw new Error("No bucket configured: set S3_BUCKET (and S3_ENDPOINT + S3_ACCESS_KEY_ID + S3_SECRET_ACCESS_KEY), or CLICHE_PUBLIC_URL alone if the bucket is resolved elsewhere.");
|
|
17
|
-
}
|
|
18
|
-
const endpoint = process.env.S3_ENDPOINT ?? process.env.AWS_ENDPOINT;
|
|
40
|
+
const { bucket, endpoint, region } = s3Options();
|
|
19
41
|
if (endpoint !== undefined)
|
|
20
42
|
return `${endpoint.replace(/\/$/, "")}/${bucket}`;
|
|
21
|
-
|
|
22
|
-
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
43
|
+
return `https://${bucket}.s3.${region ?? "us-east-1"}.amazonaws.com`;
|
|
23
44
|
}
|
|
24
45
|
/**
|
|
25
|
-
* Upload images to the S3-compatible bucket described by the
|
|
26
|
-
* environment variables
|
|
27
|
-
*
|
|
28
|
-
* public base URL (custom domains, R2 public buckets).
|
|
46
|
+
* Upload images to the S3-compatible bucket described by the CLICHE_
|
|
47
|
+
* environment variables, and return one entry per file. `CLICHE_PUBLIC_URL`
|
|
48
|
+
* sets the public base URL (custom domains, R2 public buckets).
|
|
29
49
|
*/
|
|
30
50
|
export async function upload(options, writer) {
|
|
31
51
|
const baseUrl = publicBaseUrl();
|
|
32
|
-
const client = writer ??
|
|
52
|
+
const client = writer ?? createClient();
|
|
33
53
|
const prefix = options.prefix ?? "cliche";
|
|
34
54
|
const date = new Date().toISOString().slice(0, 10);
|
|
35
55
|
const uploaded = [];
|
package/dist/upload.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAEnD,MAAM,aAAa,GAA2B;IAC5C,MAAM,EAAE,WAAW;IACnB,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,WAAW;CACpB,CAAC;AAqBF,MAAM,UAAU,
|
|
1
|
+
{"version":3,"file":"upload.js","sourceRoot":"","sources":["../src/upload.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -44,9 +44,8 @@ bunx @voila.dev/cliche upload --prefix pr-<number> --markdown qa-screenshots/*.p
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Prints one `` line per file on stdout (without `--markdown`,
|
|
47
|
-
bare URLs). Bucket configuration
|
|
48
|
-
|
|
49
|
-
`CLICHE_PUBLIC_URL` (see the package README).
|
|
47
|
+
bare URLs). Bucket configuration comes from the `CLICHE_*` environment
|
|
48
|
+
variables (see the package README).
|
|
50
49
|
|
|
51
50
|
## 3. Embed in the PR
|
|
52
51
|
|
package/src/cli.ts
CHANGED
|
@@ -32,8 +32,12 @@ Upload options:
|
|
|
32
32
|
--prefix <name> Object key prefix, e.g. pr-123.
|
|
33
33
|
--markdown Print  lines instead of URLs.
|
|
34
34
|
|
|
35
|
-
Upload configuration (
|
|
36
|
-
|
|
35
|
+
Upload configuration (environment, all prefixed so nothing else collides):
|
|
36
|
+
CLICHE_BUCKET Bucket name.
|
|
37
|
+
CLICHE_ENDPOINT S3 endpoint (omit on AWS).
|
|
38
|
+
CLICHE_ACCESS_KEY_ID Access key.
|
|
39
|
+
CLICHE_SECRET_ACCESS_KEY Secret key.
|
|
40
|
+
CLICHE_REGION Region (defaults to us-east-1 on AWS).
|
|
37
41
|
CLICHE_PUBLIC_URL Public base URL of the bucket (custom domain).
|
|
38
42
|
|
|
39
43
|
URLs go to stdout, progress to stderr. Ouistiti !`;
|
package/src/setup.ts
CHANGED
|
@@ -43,15 +43,15 @@ function environmentBlock(
|
|
|
43
43
|
): string {
|
|
44
44
|
const lines = [
|
|
45
45
|
"",
|
|
46
|
-
"# cliche
|
|
47
|
-
`
|
|
46
|
+
"# cliche, added by `cliche setup` (https://cliche.voila.dev)",
|
|
47
|
+
`CLICHE_BUCKET=${bucket}`,
|
|
48
48
|
accountId === null
|
|
49
|
-
? "#
|
|
50
|
-
: `
|
|
49
|
+
? "# CLICHE_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com"
|
|
50
|
+
: `CLICHE_ENDPOINT=https://${accountId}.r2.cloudflarestorage.com`,
|
|
51
51
|
...(publicUrl === null ? [] : [`CLICHE_PUBLIC_URL=${publicUrl}`]),
|
|
52
52
|
`# Mint the two keys at ${DASHBOARD_TOKENS_URL} (Object Read & Write on ${bucket}):`,
|
|
53
|
-
"#
|
|
54
|
-
"#
|
|
53
|
+
"# CLICHE_ACCESS_KEY_ID=",
|
|
54
|
+
"# CLICHE_SECRET_ACCESS_KEY=",
|
|
55
55
|
"",
|
|
56
56
|
];
|
|
57
57
|
return lines.join("\n");
|
|
@@ -85,8 +85,8 @@ export async function setup(bucket: string): Promise<void> {
|
|
|
85
85
|
|
|
86
86
|
const environmentFile = Bun.file(".env");
|
|
87
87
|
const existing = (await environmentFile.exists()) ? await environmentFile.text() : "";
|
|
88
|
-
if (existing.includes("
|
|
89
|
-
console.error(".env already has
|
|
88
|
+
if (existing.includes("CLICHE_BUCKET=")) {
|
|
89
|
+
console.error(".env already has a CLICHE_BUCKET. Printing the block instead of appending:");
|
|
90
90
|
console.log(environmentBlock(bucket, publicUrl, accountId));
|
|
91
91
|
} else {
|
|
92
92
|
await Bun.write(".env", existing + environmentBlock(bucket, publicUrl, accountId));
|
|
@@ -97,7 +97,7 @@ export async function setup(bucket: string): Promise<void> {
|
|
|
97
97
|
Almost there — one last step wrangler cannot do:
|
|
98
98
|
1. Open ${DASHBOARD_TOKENS_URL}
|
|
99
99
|
2. Create an API token with "Object Read & Write" on "${bucket}"
|
|
100
|
-
3. Put the two values in .env as
|
|
100
|
+
3. Put the two values in .env as CLICHE_ACCESS_KEY_ID / CLICHE_SECRET_ACCESS_KEY
|
|
101
101
|
|
|
102
102
|
Then take your first cliché:
|
|
103
103
|
bunx @voila.dev/cliche https://example.com shot.png --upload`);
|
package/src/upload.ts
CHANGED
|
@@ -28,30 +28,52 @@ export interface ObjectWriter {
|
|
|
28
28
|
write(key: string, bytes: Uint8Array, options: { type: string }): Promise<unknown>;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Every setting is read from a CLICHE_-prefixed variable and handed to
|
|
33
|
+
* Bun.S3Client explicitly, so cliche never picks up the S3_/AWS_ variables
|
|
34
|
+
* a shell may already carry for something else.
|
|
35
|
+
*/
|
|
36
|
+
export function s3Options(): Bun.S3Options {
|
|
37
|
+
const bucket = process.env.CLICHE_BUCKET;
|
|
35
38
|
if (bucket === undefined) {
|
|
36
39
|
throw new Error(
|
|
37
|
-
"No bucket configured: set
|
|
40
|
+
"No bucket configured: set CLICHE_BUCKET (plus CLICHE_ENDPOINT, CLICHE_ACCESS_KEY_ID and CLICHE_SECRET_ACCESS_KEY). Run `cliche setup` to create one on Cloudflare R2.",
|
|
38
41
|
);
|
|
39
42
|
}
|
|
40
|
-
const endpoint = process.env.
|
|
43
|
+
const endpoint = process.env.CLICHE_ENDPOINT;
|
|
44
|
+
const accessKeyId = process.env.CLICHE_ACCESS_KEY_ID;
|
|
45
|
+
const secretAccessKey = process.env.CLICHE_SECRET_ACCESS_KEY;
|
|
46
|
+
const region = process.env.CLICHE_REGION;
|
|
47
|
+
return {
|
|
48
|
+
bucket,
|
|
49
|
+
...(endpoint === undefined ? {} : { endpoint }),
|
|
50
|
+
...(accessKeyId === undefined ? {} : { accessKeyId }),
|
|
51
|
+
...(secretAccessKey === undefined ? {} : { secretAccessKey }),
|
|
52
|
+
...(region === undefined ? {} : { region }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** An S3 client for the configured bucket. */
|
|
57
|
+
export function createClient(): Bun.S3Client {
|
|
58
|
+
return new Bun.S3Client(s3Options());
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function publicBaseUrl(): string {
|
|
62
|
+
const configured = process.env.CLICHE_PUBLIC_URL;
|
|
63
|
+
if (configured !== undefined) return configured.replace(/\/$/, "");
|
|
64
|
+
const { bucket, endpoint, region } = s3Options();
|
|
41
65
|
if (endpoint !== undefined) return `${endpoint.replace(/\/$/, "")}/${bucket}`;
|
|
42
|
-
|
|
43
|
-
return `https://${bucket}.s3.${region}.amazonaws.com`;
|
|
66
|
+
return `https://${bucket}.s3.${region ?? "us-east-1"}.amazonaws.com`;
|
|
44
67
|
}
|
|
45
68
|
|
|
46
69
|
/**
|
|
47
|
-
* Upload images to the S3-compatible bucket described by the
|
|
48
|
-
* environment variables
|
|
49
|
-
*
|
|
50
|
-
* public base URL (custom domains, R2 public buckets).
|
|
70
|
+
* Upload images to the S3-compatible bucket described by the CLICHE_
|
|
71
|
+
* environment variables, and return one entry per file. `CLICHE_PUBLIC_URL`
|
|
72
|
+
* sets the public base URL (custom domains, R2 public buckets).
|
|
51
73
|
*/
|
|
52
74
|
export async function upload(options: UploadOptions, writer?: ObjectWriter): Promise<Array<UploadedFile>> {
|
|
53
75
|
const baseUrl = publicBaseUrl();
|
|
54
|
-
const client = writer ??
|
|
76
|
+
const client = writer ?? createClient();
|
|
55
77
|
const prefix = options.prefix ?? "cliche";
|
|
56
78
|
const date = new Date().toISOString().slice(0, 10);
|
|
57
79
|
const uploaded: Array<UploadedFile> = [];
|