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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/dist/attach.d.mts +5 -0
  4. package/dist/attach.d.mts.map +1 -0
  5. package/dist/attach.mjs +22 -0
  6. package/dist/attach.mjs.map +1 -0
  7. package/dist/cli.d.mts +3 -0
  8. package/dist/cli.d.mts.map +1 -0
  9. package/dist/cli.mjs +20 -0
  10. package/dist/cli.mjs.map +1 -0
  11. package/dist/github-cli.d.mts +14 -0
  12. package/dist/github-cli.d.mts.map +1 -0
  13. package/dist/github-cli.mjs +75 -0
  14. package/dist/github-cli.mjs.map +1 -0
  15. package/dist/index.d.mts +5 -0
  16. package/dist/index.d.mts.map +1 -0
  17. package/dist/index.mjs +5 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/parse-args.d.mts +11 -0
  20. package/dist/parse-args.d.mts.map +1 -0
  21. package/dist/parse-args.mjs +70 -0
  22. package/dist/parse-args.mjs.map +1 -0
  23. package/dist/screenshots-section.d.mts +4 -0
  24. package/dist/screenshots-section.d.mts.map +1 -0
  25. package/dist/screenshots-section.mjs +43 -0
  26. package/dist/screenshots-section.mjs.map +1 -0
  27. package/dist/src/attach.d.mts +5 -0
  28. package/dist/src/attach.d.mts.map +1 -0
  29. package/dist/src/attach.mjs +23 -0
  30. package/dist/src/attach.mjs.map +1 -0
  31. package/dist/src/cli.d.mts +3 -0
  32. package/dist/src/cli.d.mts.map +1 -0
  33. package/dist/src/cli.mjs +20 -0
  34. package/dist/src/cli.mjs.map +1 -0
  35. package/dist/src/github-cli.d.mts +14 -0
  36. package/dist/src/github-cli.d.mts.map +1 -0
  37. package/dist/src/github-cli.mjs +75 -0
  38. package/dist/src/github-cli.mjs.map +1 -0
  39. package/dist/src/index.d.mts +5 -0
  40. package/dist/src/index.d.mts.map +1 -0
  41. package/dist/src/index.mjs +5 -0
  42. package/dist/src/index.mjs.map +1 -0
  43. package/dist/src/parse-args.d.mts +11 -0
  44. package/dist/src/parse-args.d.mts.map +1 -0
  45. package/dist/src/parse-args.mjs +70 -0
  46. package/dist/src/parse-args.mjs.map +1 -0
  47. package/dist/src/screenshots-section.d.mts +4 -0
  48. package/dist/src/screenshots-section.d.mts.map +1 -0
  49. package/dist/src/screenshots-section.mjs +43 -0
  50. package/dist/src/screenshots-section.mjs.map +1 -0
  51. package/package.json +61 -0
  52. 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
+ ![screenshot](https://github.com/user-attachments/assets/...)
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"}
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(args: string[]): void;
3
+ //# sourceMappingURL=cli.d.mts.map
@@ -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
@@ -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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"}
@@ -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,4 @@
1
+ export declare function upsertScreenshotsSection(body: string, imageMarkdown: string[], { replace }: {
2
+ replace: boolean;
3
+ }): string;
4
+ //# sourceMappingURL=screenshots-section.d.mts.map
@@ -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("![") && line.includes("](http"));
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,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(args: string[]): void;
3
+ //# sourceMappingURL=cli.d.mts.map
@@ -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"}
@@ -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,4 @@
1
+ export declare function upsertScreenshotsSection(body: string, imageMarkdown: string[], { replace }: {
2
+ replace: boolean;
3
+ }): string;
4
+ //# sourceMappingURL=screenshots-section.d.mts.map
@@ -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('![') && line.includes('](http'));
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.