gh-pr-attach-screenshots 0.1.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/LICENSE +21 -0
- package/README.md +131 -0
- package/dist/attach.d.mts +5 -0
- package/dist/attach.d.mts.map +1 -0
- package/dist/attach.mjs +22 -0
- package/dist/attach.mjs.map +1 -0
- package/dist/cli.d.mts +3 -0
- package/dist/cli.d.mts.map +1 -0
- package/dist/cli.mjs +20 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/github-cli.d.mts +14 -0
- package/dist/github-cli.d.mts.map +1 -0
- package/dist/github-cli.mjs +75 -0
- package/dist/github-cli.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +5 -0
- package/dist/index.mjs.map +1 -0
- package/dist/parse-args.d.mts +11 -0
- package/dist/parse-args.d.mts.map +1 -0
- package/dist/parse-args.mjs +70 -0
- package/dist/parse-args.mjs.map +1 -0
- package/dist/screenshots-section.d.mts +4 -0
- package/dist/screenshots-section.d.mts.map +1 -0
- package/dist/screenshots-section.mjs +43 -0
- package/dist/screenshots-section.mjs.map +1 -0
- package/dist/src/attach.d.mts +5 -0
- package/dist/src/attach.d.mts.map +1 -0
- package/dist/src/attach.mjs +23 -0
- package/dist/src/attach.mjs.map +1 -0
- package/dist/src/cli.d.mts +3 -0
- package/dist/src/cli.d.mts.map +1 -0
- package/dist/src/cli.mjs +20 -0
- package/dist/src/cli.mjs.map +1 -0
- package/dist/src/github-cli.d.mts +14 -0
- package/dist/src/github-cli.d.mts.map +1 -0
- package/dist/src/github-cli.mjs +75 -0
- package/dist/src/github-cli.mjs.map +1 -0
- package/dist/src/index.d.mts +5 -0
- package/dist/src/index.d.mts.map +1 -0
- package/dist/src/index.mjs +5 -0
- package/dist/src/index.mjs.map +1 -0
- package/dist/src/parse-args.d.mts +11 -0
- package/dist/src/parse-args.d.mts.map +1 -0
- package/dist/src/parse-args.mjs +70 -0
- package/dist/src/parse-args.mjs.map +1 -0
- package/dist/src/screenshots-section.d.mts +4 -0
- package/dist/src/screenshots-section.d.mts.map +1 -0
- package/dist/src/screenshots-section.mjs +43 -0
- package/dist/src/screenshots-section.mjs.map +1 -0
- package/package.json +61 -0
- package/skills/gh-pr-attach-screenshots/SKILL.md +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan Ong
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# gh-pr-attach-screenshots
|
|
2
|
+
|
|
3
|
+
Upload local screenshots with [`gh-image`](https://github.com/drogers0/gh-image) and attach them to a GitHub PR description.
|
|
4
|
+
|
|
5
|
+
Manages a `## Screenshots` section delimited by HTML comments so the tool can be run multiple times without duplicating images:
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
## Screenshots
|
|
9
|
+
|
|
10
|
+
<!-- agent-screenshots:start -->
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
<!-- agent-screenshots:end -->
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
Both tools must be installed before running.
|
|
20
|
+
|
|
21
|
+
**GitHub CLI (`gh`)**
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
brew install gh # macOS
|
|
25
|
+
gh auth login
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
See [cli.github.com](https://cli.github.com) for Linux/Windows install options.
|
|
29
|
+
|
|
30
|
+
**`gh-image` extension**
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
gh extension install drogers0/gh-image
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> This step may need to run **outside any sandbox** — the extension stores credentials
|
|
37
|
+
> under `~/.config/gh` and may read browser session tokens.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install -g gh-pr-attach-screenshots
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or use it directly with `npx`:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
npx gh-pr-attach-screenshots ./screenshot.png
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
gh-pr-attach-screenshots [--pr <number|branch|url>] [--repo owner/repo] [--replace] <image...>
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--pr <value> PR number, branch name, or URL (defaults to current branch PR)
|
|
58
|
+
--repo <value> Repository in owner/repo format (defaults to current repo)
|
|
59
|
+
--replace Replace existing screenshots instead of merging
|
|
60
|
+
--help, -h Show this help message
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Examples**
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
# Attach a screenshot to the current branch's PR
|
|
67
|
+
gh-pr-attach-screenshots ./desktop.png
|
|
68
|
+
|
|
69
|
+
# Attach multiple screenshots
|
|
70
|
+
gh-pr-attach-screenshots ./desktop.png ./mobile.png
|
|
71
|
+
|
|
72
|
+
# Replace existing screenshots
|
|
73
|
+
gh-pr-attach-screenshots --replace ./new-desktop.png
|
|
74
|
+
|
|
75
|
+
# Specify a PR and repo explicitly
|
|
76
|
+
gh-pr-attach-screenshots --pr 123 --repo owner/repo ./screenshot.png
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Programmatic API
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { attachPrScreenshots, parseArgs, upsertScreenshotsSection } from "gh-pr-attach-screenshots";
|
|
83
|
+
|
|
84
|
+
// Full attach flow
|
|
85
|
+
attachPrScreenshots({
|
|
86
|
+
images: ["./screenshot.png"],
|
|
87
|
+
pr: "123",
|
|
88
|
+
repo: "owner/repo",
|
|
89
|
+
replace: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Parse CLI args
|
|
93
|
+
const options = parseArgs(["--pr", "123", "./screenshot.png"]);
|
|
94
|
+
|
|
95
|
+
// Upsert the screenshots section in a PR body string
|
|
96
|
+
const newBody = upsertScreenshotsSection(existingBody, imageMarkdown, { replace: false });
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Agent skill
|
|
100
|
+
|
|
101
|
+
Install this skill so your AI agent automatically knows how to use this tool:
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
npx skills add jonathanong/gh-pr-attach-screenshots -a claude-code
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The [`skills`](https://www.npmjs.com/package/skills) CLI supports many agents besides Claude Code — pass a different `-a` flag for OpenAI Codex, Cursor, and others. The skill covers invocation, prerequisites, and the browser-screenshot recipe.
|
|
108
|
+
|
|
109
|
+
## Agent usage notes
|
|
110
|
+
|
|
111
|
+
This tool is designed for use by AI agents. Key behaviors:
|
|
112
|
+
|
|
113
|
+
- **Fail-fast**: if `gh` or the `gh-image` extension is missing, the tool exits immediately with actionable install instructions.
|
|
114
|
+
- **Idempotent**: running the tool multiple times merges images without duplicates.
|
|
115
|
+
- **Success feedback**: after a successful attach, the tool prints to stderr:
|
|
116
|
+
```
|
|
117
|
+
Attached N screenshot(s) to PR #<number> in <owner>/<repo>
|
|
118
|
+
```
|
|
119
|
+
- **Exit codes**: `0` on success or `--help`, `2` on error.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```sh
|
|
124
|
+
pnpm install
|
|
125
|
+
pnpm build # compile src/*.mts → dist/*.mjs
|
|
126
|
+
pnpm typecheck # type-check src + test
|
|
127
|
+
pnpm lint # oxlint
|
|
128
|
+
pnpm format # oxfmt
|
|
129
|
+
pnpm test # run tests
|
|
130
|
+
pnpm test:coverage # run tests with 100% coverage gate
|
|
131
|
+
```
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CommandRunner } from "./github-cli.mjs";
|
|
2
|
+
import { type AttachScreenshotOptions } from "./parse-args.mjs";
|
|
3
|
+
export type { AttachScreenshotOptions };
|
|
4
|
+
export declare function attachPrScreenshots(options: AttachScreenshotOptions, runner?: CommandRunner): string;
|
|
5
|
+
//# sourceMappingURL=attach.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.d.mts","sourceRoot":"","sources":["../src/attach.mts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,aAAa,EAMnB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAGhE,YAAY,EAAE,uBAAuB,EAAE,CAAC;AAExC,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,uBAAuB,EAChC,MAAM,GAAE,aAA0B,GACjC,MAAM,CAwBR"}
|
package/dist/attach.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { editPrBody, ensureGhImageExtension, gh, runCommand, validateGh, } from "./github-cli.mjs";
|
|
3
|
+
import { upsertScreenshotsSection } from "./screenshots-section.mjs";
|
|
4
|
+
export function attachPrScreenshots(options, runner = runCommand) {
|
|
5
|
+
validateGh(runner);
|
|
6
|
+
ensureGhImageExtension(runner);
|
|
7
|
+
const repo = options.repo ??
|
|
8
|
+
gh(runner, ["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]);
|
|
9
|
+
const pr = options.pr ?? gh(runner, ["pr", "view", "--repo", repo, "--json", "number", "--jq", ".number"]);
|
|
10
|
+
const imageMarkdown = options.images.map((image) => {
|
|
11
|
+
if (!existsSync(image)) {
|
|
12
|
+
throw new Error(`Screenshot not found: ${image}`);
|
|
13
|
+
}
|
|
14
|
+
return gh(runner, ["image", image, "--repo", repo]);
|
|
15
|
+
});
|
|
16
|
+
const body = gh(runner, ["pr", "view", pr, "--repo", repo, "--json", "body", "--jq", ".body"]);
|
|
17
|
+
const nextBody = upsertScreenshotsSection(body, imageMarkdown, { replace: options.replace });
|
|
18
|
+
editPrBody(runner, pr, repo, nextBody);
|
|
19
|
+
process.stderr.write(`Attached ${imageMarkdown.length} screenshot(s) to PR #${pr} in ${repo}\n`);
|
|
20
|
+
return nextBody;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=attach.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.mjs","sourceRoot":"","sources":["../src/attach.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAEL,UAAU,EACV,sBAAsB,EACtB,EAAE,EACF,UAAU,EACV,UAAU,GACX,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAIrE,MAAM,UAAU,mBAAmB,CACjC,OAAgC,EAChC,SAAwB,UAAU;IAElC,UAAU,CAAC,MAAM,CAAC,CAAC;IACnB,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAE/B,MAAM,IAAI,GACR,OAAO,CAAC,IAAI;QACZ,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC;IACpF,MAAM,EAAE,GACN,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAElG,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QACjD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/F,MAAM,QAAQ,GAAG,wBAAwB,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7F,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;IAEvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,aAAa,CAAC,MAAM,yBAAyB,EAAE,OAAO,IAAI,IAAI,CAAC,CAAC;IAEjG,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.mts","sourceRoot":"","sources":["../src/cli.mts"],"names":[],"mappings":";AAIA,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAWzC"}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { attachPrScreenshots } from "./attach.mjs";
|
|
3
|
+
import { HelpRequested, parseArgs } from "./parse-args.mjs";
|
|
4
|
+
export function main(args) {
|
|
5
|
+
try {
|
|
6
|
+
attachPrScreenshots(parseArgs(args));
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error instanceof HelpRequested) {
|
|
10
|
+
console.log(error.message);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (process.argv[1] === import.meta.filename) {
|
|
18
|
+
main(process.argv.slice(2));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=cli.mjs.map
|
package/dist/cli.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","sourceRoot":"","sources":["../src/cli.mts"],"names":[],"mappings":";AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE5D,MAAM,UAAU,IAAI,CAAC,IAAc;IACjC,IAAI,CAAC;QACH,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACvC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;YAC3B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CommandResult = {
|
|
2
|
+
error?: Error;
|
|
3
|
+
status: number | null;
|
|
4
|
+
stderr: string;
|
|
5
|
+
stdout: string;
|
|
6
|
+
};
|
|
7
|
+
export type CommandRunner = (command: string, args: string[]) => CommandResult;
|
|
8
|
+
export declare function runCommand(command: string, args: string[]): CommandResult;
|
|
9
|
+
export declare function formatCommandError(command: string, result: CommandResult): string;
|
|
10
|
+
export declare function gh(runner: CommandRunner, args: string[]): string;
|
|
11
|
+
export declare function validateGh(runner: CommandRunner): void;
|
|
12
|
+
export declare function ensureGhImageExtension(runner: CommandRunner): void;
|
|
13
|
+
export declare function editPrBody(runner: CommandRunner, pr: string, repo: string, body: string): void;
|
|
14
|
+
//# sourceMappingURL=github-cli.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-cli.d.mts","sourceRoot":"","sources":["../src/github-cli.mts"],"names":[],"mappings":"AAOA,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,aAAa,CAAC;AAE/E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAYzE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,CAGjF;AAED,wBAAgB,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAMhE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAiBtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAmBlE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS9F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const extensionName = "drogers0/gh-image";
|
|
6
|
+
export function runCommand(command, args) {
|
|
7
|
+
const options = {
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
10
|
+
};
|
|
11
|
+
const result = spawnSync(command, args, options);
|
|
12
|
+
return {
|
|
13
|
+
error: result.error,
|
|
14
|
+
status: result.status,
|
|
15
|
+
stderr: result.stderr ?? "",
|
|
16
|
+
stdout: result.stdout ?? "",
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function formatCommandError(command, result) {
|
|
20
|
+
const detail = (result.error?.message ?? result.stderr.trim()) || result.stdout.trim();
|
|
21
|
+
return detail ? `${command} failed: ${detail}` : `${command} failed.`;
|
|
22
|
+
}
|
|
23
|
+
export function gh(runner, args) {
|
|
24
|
+
const result = runner("gh", args);
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
throw new Error(formatCommandError(`gh ${args.join(" ")}`, result));
|
|
27
|
+
}
|
|
28
|
+
return result.stdout.trim();
|
|
29
|
+
}
|
|
30
|
+
export function validateGh(runner) {
|
|
31
|
+
const result = runner("gh", ["--version"]);
|
|
32
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
33
|
+
throw new Error([
|
|
34
|
+
"`gh` (GitHub CLI) is not installed or not on PATH.",
|
|
35
|
+
"",
|
|
36
|
+
"Install it:",
|
|
37
|
+
" macOS: brew install gh",
|
|
38
|
+
" Linux: https://github.com/cli/cli#installation",
|
|
39
|
+
" Windows: winget install --id GitHub.cli",
|
|
40
|
+
"",
|
|
41
|
+
"Then authenticate:",
|
|
42
|
+
" gh auth login",
|
|
43
|
+
].join("\n"));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function ensureGhImageExtension(runner) {
|
|
47
|
+
const result = runner("gh", ["extension", "list"]);
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
throw new Error(formatCommandError("gh extension list", result));
|
|
50
|
+
}
|
|
51
|
+
if (result.stdout.includes(extensionName) || /^gh-image\b/m.test(result.stdout)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
throw new Error([
|
|
55
|
+
"The `gh-image` extension is not installed.",
|
|
56
|
+
"",
|
|
57
|
+
"Install it (this may need to run outside any sandbox, as the extension",
|
|
58
|
+
"stores credentials under ~/.config/gh):",
|
|
59
|
+
" gh extension install drogers0/gh-image",
|
|
60
|
+
"",
|
|
61
|
+
"Project: https://github.com/drogers0/gh-image",
|
|
62
|
+
].join("\n"));
|
|
63
|
+
}
|
|
64
|
+
export function editPrBody(runner, pr, repo, body) {
|
|
65
|
+
const dir = mkdtempSync(join(tmpdir(), "gh-pr-attach-screenshots-"));
|
|
66
|
+
const bodyFile = join(dir, "body.md");
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(bodyFile, body);
|
|
69
|
+
gh(runner, ["pr", "edit", pr, "--repo", repo, "--body-file", bodyFile]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
rmSync(dir, { force: true, recursive: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=github-cli.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-cli.mjs","sourceRoot":"","sources":["../src/github-cli.mts"],"names":[],"mappings":"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"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { attachPrScreenshots, type AttachScreenshotOptions } from "./attach.mjs";
|
|
2
|
+
export { HelpRequested, parseArgs } from "./parse-args.mjs";
|
|
3
|
+
export { upsertScreenshotsSection } from "./screenshots-section.mjs";
|
|
4
|
+
export { type CommandResult, type CommandRunner, runCommand } from "./github-cli.mjs";
|
|
5
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { attachPrScreenshots } from "./attach.mjs";
|
|
2
|
+
export { HelpRequested, parseArgs } from "./parse-args.mjs";
|
|
3
|
+
export { upsertScreenshotsSection } from "./screenshots-section.mjs";
|
|
4
|
+
export { runCommand } from "./github-cli.mjs";
|
|
5
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgC,MAAM,cAAc,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC5D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAA0C,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AttachScreenshotOptions = {
|
|
2
|
+
images: string[];
|
|
3
|
+
pr?: string;
|
|
4
|
+
repo?: string;
|
|
5
|
+
replace: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare class HelpRequested extends Error {
|
|
8
|
+
constructor();
|
|
9
|
+
}
|
|
10
|
+
export declare function parseArgs(args: string[]): AttachScreenshotOptions;
|
|
11
|
+
//# sourceMappingURL=parse-args.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-args.d.mts","sourceRoot":"","sources":["../src/parse-args.mts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB,CAAC;AAEF,qBAAa,aAAc,SAAQ,KAAK;;CAIvC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAiDjE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export class HelpRequested extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super(usage());
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export function parseArgs(args) {
|
|
7
|
+
const parsed = { images: [], replace: false };
|
|
8
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9
|
+
const arg = args[index];
|
|
10
|
+
if (arg === "--replace") {
|
|
11
|
+
parsed.replace = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg === "--pr") {
|
|
15
|
+
parsed.pr = readOptionValue(args, index, "--pr");
|
|
16
|
+
index += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg.startsWith("--pr=")) {
|
|
20
|
+
parsed.pr = arg.slice("--pr=".length);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === "--repo") {
|
|
24
|
+
parsed.repo = readOptionValue(args, index, "--repo");
|
|
25
|
+
index += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg.startsWith("--repo=")) {
|
|
29
|
+
parsed.repo = arg.slice("--repo=".length);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === "--help" || arg === "-h") {
|
|
33
|
+
throw new HelpRequested();
|
|
34
|
+
}
|
|
35
|
+
if (arg.startsWith("-")) {
|
|
36
|
+
throw new Error(`Unknown option "${arg}".\n\n${usage()}`);
|
|
37
|
+
}
|
|
38
|
+
parsed.images.push(arg);
|
|
39
|
+
}
|
|
40
|
+
if (parsed.images.length === 0) {
|
|
41
|
+
throw new Error(`At least one image path is required.\n\n${usage()}`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
function readOptionValue(args, index, option) {
|
|
46
|
+
const value = args[index + 1];
|
|
47
|
+
if (value === undefined || value.startsWith("-")) {
|
|
48
|
+
throw new Error(`${option} requires a value.`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function usage() {
|
|
53
|
+
return [
|
|
54
|
+
"Usage: gh-pr-attach-screenshots [--pr <number|branch|url>] [--repo owner/repo] [--replace] <image...>",
|
|
55
|
+
"",
|
|
56
|
+
"Uploads screenshots with gh-image and attaches them to the PR description.",
|
|
57
|
+
"",
|
|
58
|
+
"Options:",
|
|
59
|
+
" --pr <value> PR number, branch name, or URL (defaults to current branch PR)",
|
|
60
|
+
" --repo <value> Repository in owner/repo format (defaults to current repo)",
|
|
61
|
+
" --replace Replace existing screenshots instead of merging",
|
|
62
|
+
" --help, -h Show this help message",
|
|
63
|
+
"",
|
|
64
|
+
"Prerequisites:",
|
|
65
|
+
" - gh (GitHub CLI): brew install gh && gh auth login",
|
|
66
|
+
" - gh-image extension: gh extension install drogers0/gh-image",
|
|
67
|
+
" (may need to run outside any sandbox)",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=parse-args.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-args.mjs","sourceRoot":"","sources":["../src/parse-args.mts"],"names":[],"mappings":"AAOA,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC;QACE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;IACjB,CAAC;CACF;AAED,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,MAAM,GAA4B,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAEvE,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAE,CAAC;QAEzB,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;YACxB,MAAM,CAAC,OAAO,GAAG,IAAI,CAAC;YACtB,SAAS;QACX,CAAC;QAED,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YACnB,MAAM,CAAC,EAAE,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;YACjD,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QAED,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YACtC,SAAS;QACX,CAAC;QAED,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;YACrD,KAAK,IAAI,CAAC,CAAC;YACX,SAAS;QACX,CAAC;QAED,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;YAC1C,SAAS;QACX,CAAC;QAED,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACrC,MAAM,IAAI,aAAa,EAAE,CAAC;QAC5B,CAAC;QAED,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,SAAS,KAAK,EAAE,EAAE,CAAC,CAAC;QAC5D,CAAC;QAED,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,2CAA2C,KAAK,EAAE,EAAE,CAAC,CAAC;IACxE,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,eAAe,CAAC,IAAc,EAAE,KAAa,EAAE,MAAc;IACpE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;IAC9B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,oBAAoB,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,KAAK;IACZ,OAAO;QACL,uGAAuG;QACvG,EAAE;QACF,4EAA4E;QAC5E,EAAE;QACF,UAAU;QACV,kFAAkF;QAClF,8EAA8E;QAC9E,mEAAmE;QACnE,0CAA0C;QAC1C,EAAE;QACF,gBAAgB;QAChB,uDAAuD;QACvD,gEAAgE;QAChE,2CAA2C;KAC5C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshots-section.d.mts","sourceRoot":"","sources":["../src/screenshots-section.mts"],"names":[],"mappings":"AAGA,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EAAE,EACvB,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAChC,MAAM,CAeR"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const sectionStart = "<!-- agent-screenshots:start -->";
|
|
2
|
+
const sectionEnd = "<!-- agent-screenshots:end -->";
|
|
3
|
+
export function upsertScreenshotsSection(body, imageMarkdown, { replace }) {
|
|
4
|
+
const existingSection = findScreenshotsSection(body);
|
|
5
|
+
const existingImages = existingSection === null || replace ? [] : screenshotLines(existingSection.content);
|
|
6
|
+
const nextImages = dedupe([
|
|
7
|
+
...existingImages,
|
|
8
|
+
...imageMarkdown.map((line) => line.trim()).filter(Boolean),
|
|
9
|
+
]);
|
|
10
|
+
const nextSection = renderScreenshotsSection(nextImages);
|
|
11
|
+
if (existingSection === null) {
|
|
12
|
+
return `${body.trimEnd()}\n\n${nextSection}\n`;
|
|
13
|
+
}
|
|
14
|
+
return `${body.slice(0, existingSection.start)}${nextSection}${body.slice(existingSection.end)}`;
|
|
15
|
+
}
|
|
16
|
+
function findScreenshotsSection(body) {
|
|
17
|
+
const startMarker = body.indexOf(sectionStart);
|
|
18
|
+
const endMarker = body.indexOf(sectionEnd, startMarker + sectionStart.length);
|
|
19
|
+
if (startMarker === -1 || endMarker === -1) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const headingStart = body.lastIndexOf("## Screenshots", startMarker);
|
|
23
|
+
const start = headingStart === -1 ? startMarker : headingStart;
|
|
24
|
+
const end = endMarker + sectionEnd.length;
|
|
25
|
+
return {
|
|
26
|
+
content: body.slice(startMarker + sectionStart.length, endMarker),
|
|
27
|
+
end,
|
|
28
|
+
start,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function screenshotLines(content) {
|
|
32
|
+
return content
|
|
33
|
+
.split("\n")
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter((line) => line.startsWith(");
|
|
36
|
+
}
|
|
37
|
+
function dedupe(values) {
|
|
38
|
+
return [...new Set(values)];
|
|
39
|
+
}
|
|
40
|
+
function renderScreenshotsSection(imageMarkdown) {
|
|
41
|
+
return ["## Screenshots", sectionStart, ...imageMarkdown, sectionEnd].join("\n");
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=screenshots-section.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshots-section.mjs","sourceRoot":"","sources":["../src/screenshots-section.mts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,kCAAkC,CAAC;AACxD,MAAM,UAAU,GAAG,gCAAgC,CAAC;AAEpD,MAAM,UAAU,wBAAwB,CACtC,IAAY,EACZ,aAAuB,EACvB,EAAE,OAAO,EAAwB;IAEjC,MAAM,eAAe,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACrD,MAAM,cAAc,GAClB,eAAe,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;IACtF,MAAM,UAAU,GAAG,MAAM,CAAC;QACxB,GAAG,cAAc;QACjB,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;KAC5D,CAAC,CAAC;IACH,MAAM,WAAW,GAAG,wBAAwB,CAAC,UAAU,CAAC,CAAC;IAEzD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,IAAI,CAAC;IACjD,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC;AACnG,CAAC;AAED,SAAS,sBAAsB,CAC7B,IAAY;IAEZ,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAC/C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;IAC9E,IAAI,WAAW,KAAK,CAAC,CAAC,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAC;IACrE,MAAM,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC;IAC/D,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,CAAC,MAAM,CAAC;IAC1C,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC;QACjE,GAAG;QACH,KAAK;KACN,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO,OAAO;SACX,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;AACxE,CAAC;AAED,SAAS,MAAM,CAAC,MAAgB;IAC9B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,wBAAwB,CAAC,aAAuB;IACvD,OAAO,CAAC,gBAAgB,EAAE,YAAY,EAAE,GAAG,aAAa,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACnF,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type CommandRunner } from './github-cli.mts';
|
|
2
|
+
import { type AttachScreenshotOptions } from './parse-args.mts';
|
|
3
|
+
export type { AttachScreenshotOptions };
|
|
4
|
+
export declare function attachPrScreenshots(options: AttachScreenshotOptions, runner?: CommandRunner): string;
|
|
5
|
+
//# sourceMappingURL=attach.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.d.mts","sourceRoot":"","sources":["../../src/attach.mts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,aAAa,EAMnB,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,kBAAkB,CAAA;AAG/D,YAAY,EAAE,uBAAuB,EAAE,CAAA;AAEvC,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,uBAAuB,EAChC,MAAM,GAAE,aAA0B,GACjC,MAAM,CAyBR"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { editPrBody, ensureGhImageExtension, gh, runCommand, validateGh, } from './github-cli.mts';
|
|
3
|
+
import { upsertScreenshotsSection } from './screenshots-section.mts';
|
|
4
|
+
export function attachPrScreenshots(options, runner = runCommand) {
|
|
5
|
+
validateGh(runner);
|
|
6
|
+
ensureGhImageExtension(runner);
|
|
7
|
+
const repo = options.repo ??
|
|
8
|
+
gh(runner, ['repo', 'view', '--json', 'nameWithOwner', '--jq', '.nameWithOwner']);
|
|
9
|
+
const pr = options.pr ??
|
|
10
|
+
gh(runner, ['pr', 'view', '--repo', repo, '--json', 'number', '--jq', '.number']);
|
|
11
|
+
const imageMarkdown = options.images.map(image => {
|
|
12
|
+
if (!existsSync(image)) {
|
|
13
|
+
throw new Error(`Screenshot not found: ${image}`);
|
|
14
|
+
}
|
|
15
|
+
return gh(runner, ['image', image, '--repo', repo]);
|
|
16
|
+
});
|
|
17
|
+
const body = gh(runner, ['pr', 'view', pr, '--repo', repo, '--json', 'body', '--jq', '.body']);
|
|
18
|
+
const nextBody = upsertScreenshotsSection(body, imageMarkdown, { replace: options.replace });
|
|
19
|
+
editPrBody(runner, pr, repo, nextBody);
|
|
20
|
+
process.stderr.write(`Attached ${imageMarkdown.length} screenshot(s) to PR #${pr} in ${repo}\n`);
|
|
21
|
+
return nextBody;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=attach.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attach.mjs","sourceRoot":"","sources":["../../src/attach.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAEL,UAAU,EACV,sBAAsB,EACtB,EAAE,EACF,UAAU,EACV,UAAU,GACX,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AAIpE,MAAM,UAAU,mBAAmB,CACjC,OAAgC,EAChC,SAAwB,UAAU;IAElC,UAAU,CAAC,MAAM,CAAC,CAAA;IAClB,sBAAsB,CAAC,MAAM,CAAC,CAAA;IAE9B,MAAM,IAAI,GACR,OAAO,CAAC,IAAI;QACZ,EAAE,CAAC,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAA;IACnF,MAAM,EAAE,GACN,OAAO,CAAC,EAAE;QACV,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;IAEnF,MAAM,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE;QAC/C,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,yBAAyB,KAAK,EAAE,CAAC,CAAA;QACnD,CAAC;QACD,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAA;IACrD,CAAC,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;IAC9F,MAAM,QAAQ,GAAG,wBAAwB,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAC5F,UAAU,CAAC,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;IAEtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,YAAY,aAAa,CAAC,MAAM,yBAAyB,EAAE,OAAO,IAAI,IAAI,CAAC,CAAA;IAEhG,OAAO,QAAQ,CAAA;AACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.mts","sourceRoot":"","sources":["../../src/cli.mts"],"names":[],"mappings":";AAIA,wBAAgB,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAWzC"}
|
package/dist/src/cli.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { attachPrScreenshots } from './attach.mts';
|
|
3
|
+
import { HelpRequested, parseArgs } from './parse-args.mts';
|
|
4
|
+
export function main(args) {
|
|
5
|
+
try {
|
|
6
|
+
attachPrScreenshots(parseArgs(args));
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error instanceof HelpRequested) {
|
|
10
|
+
console.log(error.message);
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
14
|
+
process.exit(2);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (process.argv[1] === import.meta.filename) {
|
|
18
|
+
main(process.argv.slice(2));
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=cli.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.mjs","sourceRoot":"","sources":["../../src/cli.mts"],"names":[],"mappings":";AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAE3D,MAAM,UAAU,IAAI,CAAC,IAAc;IACjC,IAAI,CAAC;QACH,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,aAAa,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YAC1B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjB,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAA;QACrE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACjB,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;IAC7C,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;AAC7B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CommandResult = {
|
|
2
|
+
error?: Error;
|
|
3
|
+
status: number | null;
|
|
4
|
+
stderr: string;
|
|
5
|
+
stdout: string;
|
|
6
|
+
};
|
|
7
|
+
export type CommandRunner = (command: string, args: string[]) => CommandResult;
|
|
8
|
+
export declare function runCommand(command: string, args: string[]): CommandResult;
|
|
9
|
+
export declare function formatCommandError(command: string, result: CommandResult): string;
|
|
10
|
+
export declare function gh(runner: CommandRunner, args: string[]): string;
|
|
11
|
+
export declare function validateGh(runner: CommandRunner): void;
|
|
12
|
+
export declare function ensureGhImageExtension(runner: CommandRunner): void;
|
|
13
|
+
export declare function editPrBody(runner: CommandRunner, pr: string, repo: string, body: string): void;
|
|
14
|
+
//# sourceMappingURL=github-cli.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-cli.d.mts","sourceRoot":"","sources":["../../src/github-cli.mts"],"names":[],"mappings":"AAOA,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,CAAC,EAAE,KAAK,CAAA;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,aAAa,CAAA;AAE9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAYzE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,GAAG,MAAM,CAGjF;AAED,wBAAgB,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAMhE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAiBtD;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAmBlE;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAS9F"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
const extensionName = 'drogers0/gh-image';
|
|
6
|
+
export function runCommand(command, args) {
|
|
7
|
+
const options = {
|
|
8
|
+
encoding: 'utf8',
|
|
9
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
10
|
+
};
|
|
11
|
+
const result = spawnSync(command, args, options);
|
|
12
|
+
return {
|
|
13
|
+
error: result.error,
|
|
14
|
+
status: result.status,
|
|
15
|
+
stderr: result.stderr ?? '',
|
|
16
|
+
stdout: result.stdout ?? '',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function formatCommandError(command, result) {
|
|
20
|
+
const detail = (result.error?.message ?? result.stderr.trim()) || result.stdout.trim();
|
|
21
|
+
return detail ? `${command} failed: ${detail}` : `${command} failed.`;
|
|
22
|
+
}
|
|
23
|
+
export function gh(runner, args) {
|
|
24
|
+
const result = runner('gh', args);
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
throw new Error(formatCommandError(`gh ${args.join(' ')}`, result));
|
|
27
|
+
}
|
|
28
|
+
return result.stdout.trim();
|
|
29
|
+
}
|
|
30
|
+
export function validateGh(runner) {
|
|
31
|
+
const result = runner('gh', ['--version']);
|
|
32
|
+
if (result.error !== undefined || result.status !== 0) {
|
|
33
|
+
throw new Error([
|
|
34
|
+
'`gh` (GitHub CLI) is not installed or not on PATH.',
|
|
35
|
+
'',
|
|
36
|
+
'Install it:',
|
|
37
|
+
' macOS: brew install gh',
|
|
38
|
+
' Linux: https://github.com/cli/cli#installation',
|
|
39
|
+
' Windows: winget install --id GitHub.cli',
|
|
40
|
+
'',
|
|
41
|
+
'Then authenticate:',
|
|
42
|
+
' gh auth login',
|
|
43
|
+
].join('\n'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function ensureGhImageExtension(runner) {
|
|
47
|
+
const result = runner('gh', ['extension', 'list']);
|
|
48
|
+
if (result.status !== 0) {
|
|
49
|
+
throw new Error(formatCommandError('gh extension list', result));
|
|
50
|
+
}
|
|
51
|
+
if (result.stdout.includes(extensionName) || /^gh-image\b/m.test(result.stdout)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
throw new Error([
|
|
55
|
+
'The `gh-image` extension is not installed.',
|
|
56
|
+
'',
|
|
57
|
+
'Install it (this may need to run outside any sandbox, as the extension',
|
|
58
|
+
'stores credentials under ~/.config/gh):',
|
|
59
|
+
' gh extension install drogers0/gh-image',
|
|
60
|
+
'',
|
|
61
|
+
'Project: https://github.com/drogers0/gh-image',
|
|
62
|
+
].join('\n'));
|
|
63
|
+
}
|
|
64
|
+
export function editPrBody(runner, pr, repo, body) {
|
|
65
|
+
const dir = mkdtempSync(join(tmpdir(), 'gh-pr-attach-screenshots-'));
|
|
66
|
+
const bodyFile = join(dir, 'body.md');
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(bodyFile, body);
|
|
69
|
+
gh(runner, ['pr', 'edit', pr, '--repo', repo, '--body-file', bodyFile]);
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
rmSync(dir, { force: true, recursive: true });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=github-cli.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"github-cli.mjs","sourceRoot":"","sources":["../../src/github-cli.mts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { attachPrScreenshots, type AttachScreenshotOptions } from './attach.mts';
|
|
2
|
+
export { HelpRequested, parseArgs } from './parse-args.mts';
|
|
3
|
+
export { upsertScreenshotsSection } from './screenshots-section.mts';
|
|
4
|
+
export { type CommandResult, type CommandRunner, runCommand } from './github-cli.mts';
|
|
5
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACpE,OAAO,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { attachPrScreenshots } from './attach.mts';
|
|
2
|
+
export { HelpRequested, parseArgs } from './parse-args.mts';
|
|
3
|
+
export { upsertScreenshotsSection } from './screenshots-section.mts';
|
|
4
|
+
export { runCommand } from './github-cli.mts';
|
|
5
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../src/index.mts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAgC,MAAM,cAAc,CAAA;AAChF,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC3D,OAAO,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAA;AACpE,OAAO,EAA0C,UAAU,EAAE,MAAM,kBAAkB,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type AttachScreenshotOptions = {
|
|
2
|
+
images: string[];
|
|
3
|
+
pr?: string;
|
|
4
|
+
repo?: string;
|
|
5
|
+
replace: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare class HelpRequested extends Error {
|
|
8
|
+
constructor();
|
|
9
|
+
}
|
|
10
|
+
export declare function parseArgs(args: string[]): AttachScreenshotOptions;
|
|
11
|
+
//# sourceMappingURL=parse-args.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-args.d.mts","sourceRoot":"","sources":["../../src/parse-args.mts"],"names":[],"mappings":"AAAA,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,qBAAa,aAAc,SAAQ,KAAK;;CAIvC;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAiDjE"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export class HelpRequested extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super(usage());
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export function parseArgs(args) {
|
|
7
|
+
const parsed = { images: [], replace: false };
|
|
8
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9
|
+
const arg = args[index];
|
|
10
|
+
if (arg === '--replace') {
|
|
11
|
+
parsed.replace = true;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (arg === '--pr') {
|
|
15
|
+
parsed.pr = readOptionValue(args, index, '--pr');
|
|
16
|
+
index += 1;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg.startsWith('--pr=')) {
|
|
20
|
+
parsed.pr = arg.slice('--pr='.length);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (arg === '--repo') {
|
|
24
|
+
parsed.repo = readOptionValue(args, index, '--repo');
|
|
25
|
+
index += 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (arg.startsWith('--repo=')) {
|
|
29
|
+
parsed.repo = arg.slice('--repo='.length);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (arg === '--help' || arg === '-h') {
|
|
33
|
+
throw new HelpRequested();
|
|
34
|
+
}
|
|
35
|
+
if (arg.startsWith('-')) {
|
|
36
|
+
throw new Error(`Unknown option "${arg}".\n\n${usage()}`);
|
|
37
|
+
}
|
|
38
|
+
parsed.images.push(arg);
|
|
39
|
+
}
|
|
40
|
+
if (parsed.images.length === 0) {
|
|
41
|
+
throw new Error(`At least one image path is required.\n\n${usage()}`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
function readOptionValue(args, index, option) {
|
|
46
|
+
const value = args[index + 1];
|
|
47
|
+
if (value === undefined || value.startsWith('-')) {
|
|
48
|
+
throw new Error(`${option} requires a value.`);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
function usage() {
|
|
53
|
+
return [
|
|
54
|
+
'Usage: gh-pr-attach-screenshots [--pr <number|branch|url>] [--repo owner/repo] [--replace] <image...>',
|
|
55
|
+
'',
|
|
56
|
+
'Uploads screenshots with gh-image and attaches them to the PR description.',
|
|
57
|
+
'',
|
|
58
|
+
'Options:',
|
|
59
|
+
' --pr <value> PR number, branch name, or URL (defaults to current branch PR)',
|
|
60
|
+
' --repo <value> Repository in owner/repo format (defaults to current repo)',
|
|
61
|
+
' --replace Replace existing screenshots instead of merging',
|
|
62
|
+
' --help, -h Show this help message',
|
|
63
|
+
'',
|
|
64
|
+
'Prerequisites:',
|
|
65
|
+
' - gh (GitHub CLI): brew install gh && gh auth login',
|
|
66
|
+
' - gh-image extension: gh extension install drogers0/gh-image',
|
|
67
|
+
' (may need to run outside any sandbox)',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=parse-args.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-args.mjs","sourceRoot":"","sources":["../../src/parse-args.mts"],"names":[],"mappings":"AAOA,MAAM,OAAO,aAAc,SAAQ,KAAK;IACtC;QACE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAA;IAChB,CAAC;CACF;AAED,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,MAAM,GAA4B,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAA;IAEtE,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAE,CAAA;QAExB,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;YACxB,MAAM,CAAC,OAAO,GAAG,IAAI,CAAA;YACrB,SAAQ;QACV,CAAC;QAED,IAAI,GAAG,KAAK,MAAM,EAAE,CAAC;YACnB,MAAM,CAAC,EAAE,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAA;YAChD,KAAK,IAAI,CAAC,CAAA;YACV,SAAQ;QACV,CAAC;QAED,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,MAAM,CAAC,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;YACrC,SAAQ;QACV,CAAC;QAED,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;YACrB,MAAM,CAAC,IAAI,GAAG,eAAe,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAA;YACpD,KAAK,IAAI,CAAC,CAAA;YACV,SAAQ;QACV,CAAC;QAED,IAAI,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9B,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;YACzC,SAAQ;QACV,CAAC;QAED,IAAI,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACrC,MAAM,IAAI,aAAa,EAAE,CAAA;QAC3B,CAAC;QAED,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,SAAS,KAAK,EAAE,EAAE,CAAC,CAAA;QAC3D,CAAC;QAED,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IACzB,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,2CAA2C,KAAK,EAAE,EAAE,CAAC,CAAA;IACvE,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAS,eAAe,CAAC,IAAc,EAAE,KAAa,EAAE,MAAc;IACpE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAA;IAC7B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,GAAG,MAAM,oBAAoB,CAAC,CAAA;IAChD,CAAC;IACD,OAAO,KAAK,CAAA;AACd,CAAC;AAED,SAAS,KAAK;IACZ,OAAO;QACL,uGAAuG;QACvG,EAAE;QACF,4EAA4E;QAC5E,EAAE;QACF,UAAU;QACV,kFAAkF;QAClF,8EAA8E;QAC9E,mEAAmE;QACnE,0CAA0C;QAC1C,EAAE;QACF,gBAAgB;QAChB,uDAAuD;QACvD,gEAAgE;QAChE,2CAA2C;KAC5C,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACd,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshots-section.d.mts","sourceRoot":"","sources":["../../src/screenshots-section.mts"],"names":[],"mappings":"AAGA,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,MAAM,EAAE,EACvB,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAChC,MAAM,CAeR"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const sectionStart = '<!-- agent-screenshots:start -->';
|
|
2
|
+
const sectionEnd = '<!-- agent-screenshots:end -->';
|
|
3
|
+
export function upsertScreenshotsSection(body, imageMarkdown, { replace }) {
|
|
4
|
+
const existingSection = findScreenshotsSection(body);
|
|
5
|
+
const existingImages = existingSection === null || replace ? [] : screenshotLines(existingSection.content);
|
|
6
|
+
const nextImages = dedupe([
|
|
7
|
+
...existingImages,
|
|
8
|
+
...imageMarkdown.map(line => line.trim()).filter(Boolean),
|
|
9
|
+
]);
|
|
10
|
+
const nextSection = renderScreenshotsSection(nextImages);
|
|
11
|
+
if (existingSection === null) {
|
|
12
|
+
return `${body.trimEnd()}\n\n${nextSection}\n`;
|
|
13
|
+
}
|
|
14
|
+
return `${body.slice(0, existingSection.start)}${nextSection}${body.slice(existingSection.end)}`;
|
|
15
|
+
}
|
|
16
|
+
function findScreenshotsSection(body) {
|
|
17
|
+
const startMarker = body.indexOf(sectionStart);
|
|
18
|
+
const endMarker = body.indexOf(sectionEnd, startMarker + sectionStart.length);
|
|
19
|
+
if (startMarker === -1 || endMarker === -1) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
const headingStart = body.lastIndexOf('## Screenshots', startMarker);
|
|
23
|
+
const start = headingStart === -1 ? startMarker : headingStart;
|
|
24
|
+
const end = endMarker + sectionEnd.length;
|
|
25
|
+
return {
|
|
26
|
+
content: body.slice(startMarker + sectionStart.length, endMarker),
|
|
27
|
+
end,
|
|
28
|
+
start,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function screenshotLines(content) {
|
|
32
|
+
return content
|
|
33
|
+
.split('\n')
|
|
34
|
+
.map(line => line.trim())
|
|
35
|
+
.filter(line => line.startsWith(');
|
|
36
|
+
}
|
|
37
|
+
function dedupe(values) {
|
|
38
|
+
return [...new Set(values)];
|
|
39
|
+
}
|
|
40
|
+
function renderScreenshotsSection(imageMarkdown) {
|
|
41
|
+
return ['## Screenshots', sectionStart, ...imageMarkdown, sectionEnd].join('\n');
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=screenshots-section.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screenshots-section.mjs","sourceRoot":"","sources":["../../src/screenshots-section.mts"],"names":[],"mappings":"AAAA,MAAM,YAAY,GAAG,kCAAkC,CAAA;AACvD,MAAM,UAAU,GAAG,gCAAgC,CAAA;AAEnD,MAAM,UAAU,wBAAwB,CACtC,IAAY,EACZ,aAAuB,EACvB,EAAE,OAAO,EAAwB;IAEjC,MAAM,eAAe,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAA;IACpD,MAAM,cAAc,GAClB,eAAe,KAAK,IAAI,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,eAAe,CAAC,OAAO,CAAC,CAAA;IACrF,MAAM,UAAU,GAAG,MAAM,CAAC;QACxB,GAAG,cAAc;QACjB,GAAG,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;KAC1D,CAAC,CAAA;IACF,MAAM,WAAW,GAAG,wBAAwB,CAAC,UAAU,CAAC,CAAA;IAExD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,WAAW,IAAI,CAAA;IAChD,CAAC;IAED,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,eAAe,CAAC,KAAK,CAAC,GAAG,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAA;AAClG,CAAC;AAED,SAAS,sBAAsB,CAC7B,IAAY;IAEZ,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAA;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,WAAW,GAAG,YAAY,CAAC,MAAM,CAAC,CAAA;IAC7E,IAAI,WAAW,KAAK,CAAC,CAAC,IAAI,SAAS,KAAK,CAAC,CAAC,EAAE,CAAC;QAC3C,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,gBAAgB,EAAE,WAAW,CAAC,CAAA;IACpE,MAAM,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY,CAAA;IAC9D,MAAM,GAAG,GAAG,SAAS,GAAG,UAAU,CAAC,MAAM,CAAA;IACzC,OAAO;QACL,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,WAAW,GAAG,YAAY,CAAC,MAAM,EAAE,SAAS,CAAC;QACjE,GAAG;QACH,KAAK;KACN,CAAA;AACH,CAAC;AAED,SAAS,eAAe,CAAC,OAAe;IACtC,OAAO,OAAO;SACX,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SACxB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAA;AACrE,CAAC;AAED,SAAS,MAAM,CAAC,MAAgB;IAC9B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,CAAA;AAC7B,CAAC;AAED,SAAS,wBAAwB,CAAC,aAAuB;IACvD,OAAO,CAAC,gBAAgB,EAAE,YAAY,EAAE,GAAG,aAAa,EAAE,UAAU,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAClF,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "gh-pr-attach-screenshots",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Upload screenshots with gh-image and attach them to a GitHub PR description",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"cli",
|
|
8
|
+
"gh",
|
|
9
|
+
"github",
|
|
10
|
+
"pr",
|
|
11
|
+
"pull-request",
|
|
12
|
+
"screenshot"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/jonathanong/gh-pr-attach-screenshots",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/jonathanong/gh-pr-attach-screenshots/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "Jonathan Ong <jonathanrichardong@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/jonathanong/gh-pr-attach-screenshots.git"
|
|
23
|
+
},
|
|
24
|
+
"bin": {
|
|
25
|
+
"gh-pr-attach-screenshots": "./dist/cli.mjs"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"skills",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"type": "module",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.mts",
|
|
37
|
+
"import": "./dist/index.mjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.0.0",
|
|
42
|
+
"@vitest/coverage-v8": "^4.1.6",
|
|
43
|
+
"oxfmt": "^0.50.0",
|
|
44
|
+
"oxlint": "^1.65.0",
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"vitest": "^4.1.6"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc --project tsconfig.build.json && chmod +x dist/cli.mjs",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:coverage": "vitest run --coverage",
|
|
56
|
+
"lint": "oxlint --deny-warnings .",
|
|
57
|
+
"lint:fix": "oxlint --deny-warnings --fix .",
|
|
58
|
+
"format": "oxfmt .",
|
|
59
|
+
"format:check": "oxfmt --check ."
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gh-pr-attach-screenshots
|
|
3
|
+
description: Use this skill when the user asks to attach a screenshot to a GitHub PR, dog-food a UI change visually, or upload a local image into a pull request's description. Invokes the `gh-pr-attach-screenshots` CLI, which uploads images via `gh-image` and manages a `## Screenshots` section in the PR body.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## What it does
|
|
7
|
+
|
|
8
|
+
`gh-pr-attach-screenshots` uploads local image files via the [`drogers0/gh-image`](https://github.com/drogers0/gh-image) `gh` extension and upserts a delimited `## Screenshots` section in the PR description. Running it multiple times merges images without duplicates. `--replace` swaps the managed block entirely.
|
|
9
|
+
|
|
10
|
+
## When to use it
|
|
11
|
+
|
|
12
|
+
- User asks to "attach a screenshot to the PR" or "post this image on my PR"
|
|
13
|
+
- User shares a local image path and references an open PR
|
|
14
|
+
- After a visual UI change, to show before/after on the PR
|
|
15
|
+
- You (the agent) took a browser screenshot of a rendered page and want to surface it in the PR
|
|
16
|
+
|
|
17
|
+
## How to invoke
|
|
18
|
+
|
|
19
|
+
Run without installing (recommended for agents):
|
|
20
|
+
|
|
21
|
+
| Package manager | Command |
|
|
22
|
+
| --------------- | ---------------------------------------------------- |
|
|
23
|
+
| npm | `npx gh-pr-attach-screenshots ./screenshot.png` |
|
|
24
|
+
| pnpm | `pnpm dlx gh-pr-attach-screenshots ./screenshot.png` |
|
|
25
|
+
| yarn | `yarn dlx gh-pr-attach-screenshots ./screenshot.png` |
|
|
26
|
+
| bun | `bunx gh-pr-attach-screenshots ./screenshot.png` |
|
|
27
|
+
|
|
28
|
+
Common variants:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# Attach multiple images
|
|
32
|
+
npx gh-pr-attach-screenshots ./desktop.png ./mobile.png
|
|
33
|
+
|
|
34
|
+
# Target a specific PR (accepts number, branch name, or URL)
|
|
35
|
+
npx gh-pr-attach-screenshots --pr 123 ./screenshot.png
|
|
36
|
+
|
|
37
|
+
# Replace the existing screenshots block instead of merging
|
|
38
|
+
npx gh-pr-attach-screenshots --replace ./new.png
|
|
39
|
+
|
|
40
|
+
# Explicit repo
|
|
41
|
+
npx gh-pr-attach-screenshots --repo owner/repo ./screenshot.png
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`--pr` defaults to the current branch's PR. `--repo` defaults to the current repo.
|
|
45
|
+
|
|
46
|
+
## Prerequisites & fail-fast recovery
|
|
47
|
+
|
|
48
|
+
The tool exits non-zero immediately if prerequisites are missing and prints actionable instructions to stderr. Surface stderr verbatim to the user.
|
|
49
|
+
|
|
50
|
+
**`gh` not installed:**
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
brew install gh # macOS
|
|
54
|
+
gh auth login
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
See [cli.github.com](https://cli.github.com) for Linux/Windows install options.
|
|
58
|
+
|
|
59
|
+
**`gh-image` extension not installed:**
|
|
60
|
+
|
|
61
|
+
```sh
|
|
62
|
+
gh extension install drogers0/gh-image
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> Run this command **outside any sandbox** — the extension stores credentials under `~/.config/gh` and may read browser session tokens.
|
|
66
|
+
|
|
67
|
+
**Image file not found:** the tool prints `Screenshot not found: <path>`. Check the path and retry.
|
|
68
|
+
|
|
69
|
+
## Success signal
|
|
70
|
+
|
|
71
|
+
On success the CLI prints to stderr and exits `0`:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
Attached N screenshot(s) to PR #<number> in <owner>/<repo>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Consider the task complete when you see that line.
|
|
78
|
+
|
|
79
|
+
## Browser screenshot recipe
|
|
80
|
+
|
|
81
|
+
When you need to produce the image first:
|
|
82
|
+
|
|
83
|
+
1. Use the available browser MCP tool to navigate to the page and take a screenshot:
|
|
84
|
+
- `mcp__claude-in-chrome__browser_take_screenshot`
|
|
85
|
+
- `mcp__plugin_playwright_playwright__browser_take_screenshot`
|
|
86
|
+
2. Save to a temp file (e.g., /tmp/screenshot.png)
|
|
87
|
+
3. Run the CLI with that path.
|
|
88
|
+
4. Delete the temp file when done.
|