@buildinternet/uploads 0.42.0 → 0.42.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/cli-help.js +2 -2
- package/dist/commands/hook.js +1 -0
- package/dist/commands/install.js +2 -2
- package/dist/comment-config.d.ts +7 -54
- package/dist/comment-config.generated.d.ts +45 -0
- package/dist/comment-config.generated.js +165 -0
- package/dist/comment-config.js +7 -174
- package/dist/comment-render.generated.d.ts +151 -0
- package/dist/comment-render.generated.js +534 -0
- package/dist/github.d.ts +2 -140
- package/dist/github.js +4 -492
- package/dist/hooks-install.d.ts +10 -2
- package/dist/hooks-install.js +31 -3
- package/dist/mcp/output-schemas.d.ts +37 -0
- package/dist/mcp/output-schemas.js +338 -0
- package/dist/mcp/server.d.ts +52 -0
- package/dist/mcp/server.js +65 -0
- package/dist/mcp/tools.js +69 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npx @buildinternet/uploads --help
|
|
|
15
15
|
uploads setup
|
|
16
16
|
uploads --version
|
|
17
17
|
uploads attach ./before.png ./after.png
|
|
18
|
-
uploads screenshot https://app.example --pr 123
|
|
18
|
+
uploads screenshot https://app.example --pr 123 # capture + host in one step
|
|
19
19
|
uploads screenshot ./report.html --dark --selector "main"
|
|
20
20
|
uploads screenshot http://localhost:3000 --via local --annotate ./callouts.json
|
|
21
21
|
uploads annotate ./shot.png --spec ./callouts.json --out ./shot.marked.png
|
|
@@ -24,7 +24,7 @@ uploads put ./shot.png --destination screenshots
|
|
|
24
24
|
uploads put ./shot.png --no-optimize
|
|
25
25
|
uploads put ./mobile.png --frame phone
|
|
26
26
|
uploads put ./ui.png --frame browser --frame-url "https://app.example"
|
|
27
|
-
uploads put ./after.png --pr 123
|
|
27
|
+
uploads put ./after.png --pr 123
|
|
28
28
|
uploads put ./capture-2026-…Z.png --pr 123 --name hero.png # clean leaf, stable path
|
|
29
29
|
uploads put ./shot.png --pr 123 --name hero.png --dry-run --format url # preview URL, no upload
|
|
30
30
|
uploads gallery create --title "Release screenshots"
|
|
@@ -190,7 +190,7 @@ When adding several keys, `uploads gallery add` processes them sequentially and
|
|
|
190
190
|
individual failures in `--json` output. Gallery item updates use the API's current version to
|
|
191
191
|
avoid overwriting concurrent changes.
|
|
192
192
|
|
|
193
|
-
Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>`
|
|
193
|
+
Link a gallery to a GitHub issue or pull request with `gallery link --github`. Run `uploads comment --pr <number>` to refresh that target’s one managed comment with every linked gallery and loose attachment. Coordinates and strict `https://github.com/<owner>/<repo>/issues|pull/<number>` URLs are accepted; `gallery list --github` performs the authenticated reverse lookup. Links never change gallery identity, and GitHub repository visibility does not make the public gallery private.
|
|
194
194
|
|
|
195
195
|
Config layers (first match wins): CLI flags → env vars → `--env-file` → `~/.config/buildinternet/config`. See `config.example` for keys.
|
|
196
196
|
|
package/dist/cli-help.js
CHANGED
|
@@ -105,7 +105,7 @@ ${section(style, "Examples:")}
|
|
|
105
105
|
${style.command("uploads login")}
|
|
106
106
|
${style.command("uploads whoami")}
|
|
107
107
|
${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
|
|
108
|
-
${style.command("uploads put")} ./after.png --pr 123
|
|
108
|
+
${style.command("uploads put")} ./after.png --pr 123
|
|
109
109
|
${style.command("uploads put")} ./bug.png --issue 45
|
|
110
110
|
${style.command("uploads put")} ./shot.png --meta path=/settings --state after
|
|
111
111
|
${style.command("uploads attach")} ./before.png ./after.png
|
|
@@ -154,7 +154,7 @@ ${section(style, "Examples:")}
|
|
|
154
154
|
${style.command("uploads login")}
|
|
155
155
|
${style.command("uploads whoami")}
|
|
156
156
|
${style.command("uploads put")} ./shot.png --pr 123 --name hero.png
|
|
157
|
-
${style.command("uploads put")} ./after.png --pr 123
|
|
157
|
+
${style.command("uploads put")} ./after.png --pr 123
|
|
158
158
|
${style.command("uploads put")} ./bug.png --issue 45 --repo myorg/myapp
|
|
159
159
|
${style.command("uploads put")} ./shot.png --dry-run --format url
|
|
160
160
|
${style.command("uploads put")} ./shot.png --meta path=/settings --state after
|
package/dist/commands/hook.js
CHANGED
|
@@ -19,6 +19,7 @@ Usage:
|
|
|
19
19
|
uploads hook pre-pr-screenshot
|
|
20
20
|
|
|
21
21
|
Invoked by Claude Code / Codex / Grok / Cursor hooks. Never blocks.
|
|
22
|
+
Harness manifests no-op (exit 0, no output) when this binary is not on PATH.
|
|
22
23
|
|
|
23
24
|
pre-pr-screenshot
|
|
24
25
|
If the shell command is \`gh pr create\`, the branch touches UI files, and
|
package/dist/commands/install.js
CHANGED
|
@@ -2,7 +2,7 @@ import { flagBool, flagString, parseCommandArgs, UsageError, } from "../cli-args
|
|
|
2
2
|
import { resolveConfig } from "../config.js";
|
|
3
3
|
import { execRunner } from "../github-gh.js";
|
|
4
4
|
import { writeCommandHelp } from "../cli-style.js";
|
|
5
|
-
import { HOOK_COMMAND, installHookManifests } from "../hooks-install.js";
|
|
5
|
+
import { HOOK_COMMAND, HOOK_INVOCATION, installHookManifests, } from "../hooks-install.js";
|
|
6
6
|
export const DEFAULT_MCP_URL = "https://agents.uploads.sh/mcp";
|
|
7
7
|
const SKILL_SOURCE = "buildinternet/uploads";
|
|
8
8
|
const SKILL_NAMES = ["uploads-cli", "github-screenshots", "annotate-screenshots"];
|
|
@@ -15,7 +15,7 @@ hook for Grok / Cursor when those tools are present. The remote MCP endpoint
|
|
|
15
15
|
infers your workspace from the bearer token, so only the token is needed.
|
|
16
16
|
|
|
17
17
|
Claude Code and Codex ship the same reminder via their plugins (same command:
|
|
18
|
-
\`${
|
|
18
|
+
\`${HOOK_INVOCATION}\`) — install those plugins instead of relying on this step.
|
|
19
19
|
|
|
20
20
|
Safe to re-run. An MCP server already registered under this name is reported
|
|
21
21
|
as \`already configured\` and left as-is — including the token it was created
|
package/dist/comment-config.d.ts
CHANGED
|
@@ -1,59 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* package's comment-config.test.ts and the canonical package's
|
|
8
|
-
* index.test.ts) — change both copies together. `readLocalRepoCommentConfig`
|
|
9
|
-
* below is CLI-only: it reads the six candidate config paths off the working
|
|
10
|
-
* tree (the server instead fetches them from GitHub's contents API —
|
|
11
|
-
* apps/api/src/repo-comment-config.ts).
|
|
2
|
+
* CLI comment-config: shared parser from the generated copy of
|
|
3
|
+
* `@uploads/comment-config`, plus the local working-tree reader.
|
|
4
|
+
* `readLocalRepoCommentConfig` is CLI-only — it reads the six candidate
|
|
5
|
+
* config paths off the working tree (the server instead fetches them from
|
|
6
|
+
* GitHub's contents API — apps/api/src/repo-comment-config.ts).
|
|
12
7
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
maxInlineImages?: number;
|
|
16
|
-
metaPath?: boolean;
|
|
17
|
-
metaState?: boolean;
|
|
18
|
-
linkToFilePage?: boolean;
|
|
19
|
-
note?: string;
|
|
20
|
-
ingestGithubAttachments?: boolean;
|
|
21
|
-
}
|
|
22
|
-
export interface WorkspaceCommentDefaults {
|
|
23
|
-
imageWidth?: "full" | number;
|
|
24
|
-
maxInlineImages?: number;
|
|
25
|
-
showMetadata?: boolean;
|
|
26
|
-
linkToFilePage?: boolean;
|
|
27
|
-
note?: string;
|
|
28
|
-
ingestGithubAttachments?: boolean;
|
|
29
|
-
}
|
|
30
|
-
export interface ResolvedCommentOptions {
|
|
31
|
-
imageWidth: "auto" | "full" | number;
|
|
32
|
-
maxInlineImages: number;
|
|
33
|
-
metaPath: boolean;
|
|
34
|
-
metaState: boolean;
|
|
35
|
-
linkToFilePage: boolean;
|
|
36
|
-
note: string | null;
|
|
37
|
-
ingestGithubAttachments: boolean;
|
|
38
|
-
}
|
|
39
|
-
export type OptionSource = "repo" | "workspace" | "auto";
|
|
40
|
-
export declare const AUTO_COMMENT_OPTIONS: ResolvedCommentOptions;
|
|
41
|
-
export declare const NOTE_MAX_CHARS = 500;
|
|
42
|
-
export declare function parseRepoCommentConfig(text: string, format: "yaml" | "json"): {
|
|
43
|
-
config: RepoCommentConfig | null;
|
|
44
|
-
warnings: string[];
|
|
45
|
-
};
|
|
46
|
-
export declare function resolveCommentOptions(repo: RepoCommentConfig | null, ws: WorkspaceCommentDefaults | null): {
|
|
47
|
-
options: ResolvedCommentOptions;
|
|
48
|
-
source: Record<keyof ResolvedCommentOptions, OptionSource>;
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Candidate paths, checked in this order — the first hit wins. Must match
|
|
52
|
-
* the server's REPO_CONFIG_PATHS exactly (apps/api/src/repo-comment-config.ts)
|
|
53
|
-
* so a committed config resolves identically whether the bot or the local
|
|
54
|
-
* gh fallback renders the comment.
|
|
55
|
-
*/
|
|
56
|
-
export declare const REPO_CONFIG_PATHS: readonly [".uploads.yml", ".uploads.yaml", ".uploads.json", ".github/uploads.yml", ".github/uploads.yaml", ".github/uploads.json"];
|
|
8
|
+
import { type RepoCommentConfig } from "./comment-config.generated.js";
|
|
9
|
+
export { AUTO_COMMENT_OPTIONS, NOTE_MAX_CHARS, REPO_CONFIG_PATHS, parseRepoCommentConfig, resolveCommentOptions, type OptionSource, type RepoCommentConfig, type ResolvedCommentOptions, type WorkspaceCommentDefaults, } from "./comment-config.generated.js";
|
|
57
10
|
/**
|
|
58
11
|
* Read the repo's comment config off the local working tree — the CLI has no
|
|
59
12
|
* GitHub App installation token to fetch via the contents API, so this reads
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by packages/uploads/scripts/inline-shared.mjs — do not edit.
|
|
3
|
+
* Canonical source: packages/comment-config/src/index.ts
|
|
4
|
+
* The published CLI cannot import private @uploads/* packages, so this file
|
|
5
|
+
* is inlined into the tarball. Re-run the script after changing the source.
|
|
6
|
+
*/
|
|
7
|
+
export interface RepoCommentConfig {
|
|
8
|
+
imageWidth?: "auto" | "full" | number;
|
|
9
|
+
maxInlineImages?: number;
|
|
10
|
+
metaPath?: boolean;
|
|
11
|
+
metaState?: boolean;
|
|
12
|
+
linkToFilePage?: boolean;
|
|
13
|
+
note?: string;
|
|
14
|
+
ingestGithubAttachments?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface WorkspaceCommentDefaults {
|
|
17
|
+
imageWidth?: "full" | number;
|
|
18
|
+
maxInlineImages?: number;
|
|
19
|
+
showMetadata?: boolean;
|
|
20
|
+
linkToFilePage?: boolean;
|
|
21
|
+
note?: string;
|
|
22
|
+
ingestGithubAttachments?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface ResolvedCommentOptions {
|
|
25
|
+
imageWidth: "auto" | "full" | number;
|
|
26
|
+
maxInlineImages: number;
|
|
27
|
+
metaPath: boolean;
|
|
28
|
+
metaState: boolean;
|
|
29
|
+
linkToFilePage: boolean;
|
|
30
|
+
note: string | null;
|
|
31
|
+
ingestGithubAttachments: boolean;
|
|
32
|
+
}
|
|
33
|
+
export type OptionSource = "repo" | "workspace" | "auto";
|
|
34
|
+
export declare const AUTO_COMMENT_OPTIONS: ResolvedCommentOptions;
|
|
35
|
+
export declare const NOTE_MAX_CHARS = 500;
|
|
36
|
+
export declare function parseRepoCommentConfig(text: string, format: "yaml" | "json"): {
|
|
37
|
+
config: RepoCommentConfig | null;
|
|
38
|
+
warnings: string[];
|
|
39
|
+
};
|
|
40
|
+
export declare function resolveCommentOptions(repo: RepoCommentConfig | null, ws: WorkspaceCommentDefaults | null): {
|
|
41
|
+
options: ResolvedCommentOptions;
|
|
42
|
+
source: Record<keyof ResolvedCommentOptions, OptionSource>;
|
|
43
|
+
};
|
|
44
|
+
/** Candidate paths, checked in this order — the first hit wins. */
|
|
45
|
+
export declare const REPO_CONFIG_PATHS: readonly [".uploads.yml", ".uploads.yaml", ".uploads.json", ".github/uploads.yml", ".github/uploads.yaml", ".github/uploads.json"];
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GENERATED by packages/uploads/scripts/inline-shared.mjs — do not edit.
|
|
3
|
+
* Canonical source: packages/comment-config/src/index.ts
|
|
4
|
+
* The published CLI cannot import private @uploads/* packages, so this file
|
|
5
|
+
* is inlined into the tarball. Re-run the script after changing the source.
|
|
6
|
+
*/
|
|
7
|
+
import { parse as parseYaml } from "yaml";
|
|
8
|
+
export const AUTO_COMMENT_OPTIONS = {
|
|
9
|
+
imageWidth: "auto",
|
|
10
|
+
maxInlineImages: 16, // MAX_INLINE_ATTACHMENT_IMAGES — renderer copies own the constant
|
|
11
|
+
metaPath: true,
|
|
12
|
+
metaState: true,
|
|
13
|
+
linkToFilePage: true,
|
|
14
|
+
note: null,
|
|
15
|
+
ingestGithubAttachments: false,
|
|
16
|
+
};
|
|
17
|
+
export const NOTE_MAX_CHARS = 500;
|
|
18
|
+
const WIDTH_MIN = 160;
|
|
19
|
+
const WIDTH_MAX = 1000;
|
|
20
|
+
const MAX_INLINE_MIN = 1;
|
|
21
|
+
const MAX_INLINE_MAX = 48;
|
|
22
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, Math.round(n)));
|
|
23
|
+
export function parseRepoCommentConfig(text, format) {
|
|
24
|
+
const warnings = [];
|
|
25
|
+
let root;
|
|
26
|
+
try {
|
|
27
|
+
root = format === "json" ? JSON.parse(text) : parseYaml(text);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { config: null, warnings: ["config file could not be parsed; ignoring it"] };
|
|
31
|
+
}
|
|
32
|
+
if (typeof root !== "object" || root === null || Array.isArray(root)) {
|
|
33
|
+
return { config: null, warnings };
|
|
34
|
+
}
|
|
35
|
+
const comment = root.comment;
|
|
36
|
+
if (typeof comment !== "object" || comment === null || Array.isArray(comment)) {
|
|
37
|
+
return { config: null, warnings };
|
|
38
|
+
}
|
|
39
|
+
const c = comment;
|
|
40
|
+
const config = {};
|
|
41
|
+
// imageWidth: "auto" | "full" | finite number (clamped)
|
|
42
|
+
if ("imageWidth" in c) {
|
|
43
|
+
const v = c.imageWidth;
|
|
44
|
+
if (v === "auto" || v === "full")
|
|
45
|
+
config.imageWidth = v;
|
|
46
|
+
else if (typeof v === "number" && Number.isFinite(v))
|
|
47
|
+
config.imageWidth = clamp(v, WIDTH_MIN, WIDTH_MAX);
|
|
48
|
+
else
|
|
49
|
+
warnings.push(`imageWidth: expected "auto", "full", or a number; dropped`);
|
|
50
|
+
}
|
|
51
|
+
// maxInlineImages: finite number (clamped)
|
|
52
|
+
if ("maxInlineImages" in c) {
|
|
53
|
+
const v = c.maxInlineImages;
|
|
54
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
55
|
+
config.maxInlineImages = clamp(v, MAX_INLINE_MIN, MAX_INLINE_MAX);
|
|
56
|
+
else
|
|
57
|
+
warnings.push(`maxInlineImages: expected a number; dropped`);
|
|
58
|
+
}
|
|
59
|
+
// linkToFilePage: boolean
|
|
60
|
+
if ("linkToFilePage" in c) {
|
|
61
|
+
const v = c.linkToFilePage;
|
|
62
|
+
if (typeof v === "boolean")
|
|
63
|
+
config.linkToFilePage = v;
|
|
64
|
+
else
|
|
65
|
+
warnings.push(`linkToFilePage: expected a boolean; dropped`);
|
|
66
|
+
}
|
|
67
|
+
// ingestGithubAttachments: boolean
|
|
68
|
+
if ("ingestGithubAttachments" in c) {
|
|
69
|
+
const v = c.ingestGithubAttachments;
|
|
70
|
+
if (typeof v === "boolean")
|
|
71
|
+
config.ingestGithubAttachments = v;
|
|
72
|
+
else
|
|
73
|
+
warnings.push(`ingestGithubAttachments: expected a boolean; dropped`);
|
|
74
|
+
}
|
|
75
|
+
// meta.path / meta.state: booleans nested under `meta`
|
|
76
|
+
if ("meta" in c) {
|
|
77
|
+
const v = c.meta;
|
|
78
|
+
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
79
|
+
const meta = v;
|
|
80
|
+
if ("path" in meta) {
|
|
81
|
+
if (typeof meta.path === "boolean")
|
|
82
|
+
config.metaPath = meta.path;
|
|
83
|
+
else
|
|
84
|
+
warnings.push(`meta.path: expected a boolean; dropped`);
|
|
85
|
+
}
|
|
86
|
+
if ("state" in meta) {
|
|
87
|
+
if (typeof meta.state === "boolean")
|
|
88
|
+
config.metaState = meta.state;
|
|
89
|
+
else
|
|
90
|
+
warnings.push(`meta.state: expected a boolean; dropped`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
warnings.push(`meta: expected an object; dropped`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// note: non-empty trimmed string, max NOTE_MAX_CHARS (never truncated)
|
|
98
|
+
if ("note" in c) {
|
|
99
|
+
const v = c.note;
|
|
100
|
+
if (typeof v === "string") {
|
|
101
|
+
const trimmed = v.trim();
|
|
102
|
+
if (trimmed.length === 0) {
|
|
103
|
+
// empty/whitespace note is treated as absent
|
|
104
|
+
}
|
|
105
|
+
else if (trimmed.length > NOTE_MAX_CHARS) {
|
|
106
|
+
warnings.push(`note: longer than ${NOTE_MAX_CHARS} characters; dropped (not truncated)`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
config.note = trimmed;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
warnings.push(`note: expected a string; dropped`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { config, warnings };
|
|
117
|
+
}
|
|
118
|
+
export function resolveCommentOptions(repo, ws) {
|
|
119
|
+
const wsAsRepo = {
|
|
120
|
+
...(ws?.imageWidth !== undefined ? { imageWidth: ws.imageWidth } : {}),
|
|
121
|
+
...(ws?.maxInlineImages !== undefined ? { maxInlineImages: ws.maxInlineImages } : {}),
|
|
122
|
+
...(ws?.showMetadata !== undefined
|
|
123
|
+
? { metaPath: ws.showMetadata, metaState: ws.showMetadata }
|
|
124
|
+
: {}),
|
|
125
|
+
...(ws?.linkToFilePage !== undefined ? { linkToFilePage: ws.linkToFilePage } : {}),
|
|
126
|
+
...(ws?.note ? { note: ws.note } : {}),
|
|
127
|
+
...(ws?.ingestGithubAttachments !== undefined
|
|
128
|
+
? { ingestGithubAttachments: ws.ingestGithubAttachments }
|
|
129
|
+
: {}),
|
|
130
|
+
};
|
|
131
|
+
const options = { ...AUTO_COMMENT_OPTIONS };
|
|
132
|
+
const source = Object.fromEntries(Object.keys(AUTO_COMMENT_OPTIONS).map((k) => [k, "auto"]));
|
|
133
|
+
const apply = (cfg, from) => {
|
|
134
|
+
for (const key of [
|
|
135
|
+
"imageWidth",
|
|
136
|
+
"maxInlineImages",
|
|
137
|
+
"metaPath",
|
|
138
|
+
"metaState",
|
|
139
|
+
"linkToFilePage",
|
|
140
|
+
"ingestGithubAttachments",
|
|
141
|
+
]) {
|
|
142
|
+
if (cfg[key] !== undefined && source[key] === "auto") {
|
|
143
|
+
options[key] = cfg[key];
|
|
144
|
+
source[key] = from;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (cfg.note !== undefined && source.note === "auto") {
|
|
148
|
+
options.note = cfg.note;
|
|
149
|
+
source.note = from;
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
if (repo)
|
|
153
|
+
apply(repo, "repo");
|
|
154
|
+
apply(wsAsRepo, "workspace");
|
|
155
|
+
return { options, source };
|
|
156
|
+
}
|
|
157
|
+
/** Candidate paths, checked in this order — the first hit wins. */
|
|
158
|
+
export const REPO_CONFIG_PATHS = [
|
|
159
|
+
".uploads.yml",
|
|
160
|
+
".uploads.yaml",
|
|
161
|
+
".uploads.json",
|
|
162
|
+
".github/uploads.yml",
|
|
163
|
+
".github/uploads.yaml",
|
|
164
|
+
".github/uploads.json",
|
|
165
|
+
];
|
package/dist/comment-config.js
CHANGED
|
@@ -1,181 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* package's comment-config.test.ts and the canonical package's
|
|
8
|
-
* index.test.ts) — change both copies together. `readLocalRepoCommentConfig`
|
|
9
|
-
* below is CLI-only: it reads the six candidate config paths off the working
|
|
10
|
-
* tree (the server instead fetches them from GitHub's contents API —
|
|
11
|
-
* apps/api/src/repo-comment-config.ts).
|
|
2
|
+
* CLI comment-config: shared parser from the generated copy of
|
|
3
|
+
* `@uploads/comment-config`, plus the local working-tree reader.
|
|
4
|
+
* `readLocalRepoCommentConfig` is CLI-only — it reads the six candidate
|
|
5
|
+
* config paths off the working tree (the server instead fetches them from
|
|
6
|
+
* GitHub's contents API — apps/api/src/repo-comment-config.ts).
|
|
12
7
|
*/
|
|
13
8
|
import fs from "node:fs";
|
|
14
9
|
import { join } from "node:path";
|
|
15
|
-
import {
|
|
16
|
-
export
|
|
17
|
-
imageWidth: "auto",
|
|
18
|
-
maxInlineImages: 16, // MAX_INLINE_ATTACHMENT_IMAGES — renderer copies own the constant
|
|
19
|
-
metaPath: true,
|
|
20
|
-
metaState: true,
|
|
21
|
-
linkToFilePage: true,
|
|
22
|
-
note: null,
|
|
23
|
-
ingestGithubAttachments: false,
|
|
24
|
-
};
|
|
25
|
-
export const NOTE_MAX_CHARS = 500;
|
|
26
|
-
const WIDTH_MIN = 160;
|
|
27
|
-
const WIDTH_MAX = 1000;
|
|
28
|
-
const MAX_INLINE_MIN = 1;
|
|
29
|
-
const MAX_INLINE_MAX = 48;
|
|
30
|
-
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, Math.round(n)));
|
|
31
|
-
export function parseRepoCommentConfig(text, format) {
|
|
32
|
-
const warnings = [];
|
|
33
|
-
let root;
|
|
34
|
-
try {
|
|
35
|
-
root = format === "json" ? JSON.parse(text) : parseYaml(text);
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
return { config: null, warnings: ["config file could not be parsed; ignoring it"] };
|
|
39
|
-
}
|
|
40
|
-
if (typeof root !== "object" || root === null || Array.isArray(root)) {
|
|
41
|
-
return { config: null, warnings };
|
|
42
|
-
}
|
|
43
|
-
const comment = root.comment;
|
|
44
|
-
if (typeof comment !== "object" || comment === null || Array.isArray(comment)) {
|
|
45
|
-
return { config: null, warnings };
|
|
46
|
-
}
|
|
47
|
-
const c = comment;
|
|
48
|
-
const config = {};
|
|
49
|
-
// imageWidth: "auto" | "full" | finite number (clamped)
|
|
50
|
-
if ("imageWidth" in c) {
|
|
51
|
-
const v = c.imageWidth;
|
|
52
|
-
if (v === "auto" || v === "full")
|
|
53
|
-
config.imageWidth = v;
|
|
54
|
-
else if (typeof v === "number" && Number.isFinite(v))
|
|
55
|
-
config.imageWidth = clamp(v, WIDTH_MIN, WIDTH_MAX);
|
|
56
|
-
else
|
|
57
|
-
warnings.push(`imageWidth: expected "auto", "full", or a number; dropped`);
|
|
58
|
-
}
|
|
59
|
-
// maxInlineImages: finite number (clamped)
|
|
60
|
-
if ("maxInlineImages" in c) {
|
|
61
|
-
const v = c.maxInlineImages;
|
|
62
|
-
if (typeof v === "number" && Number.isFinite(v))
|
|
63
|
-
config.maxInlineImages = clamp(v, MAX_INLINE_MIN, MAX_INLINE_MAX);
|
|
64
|
-
else
|
|
65
|
-
warnings.push(`maxInlineImages: expected a number; dropped`);
|
|
66
|
-
}
|
|
67
|
-
// linkToFilePage: boolean
|
|
68
|
-
if ("linkToFilePage" in c) {
|
|
69
|
-
const v = c.linkToFilePage;
|
|
70
|
-
if (typeof v === "boolean")
|
|
71
|
-
config.linkToFilePage = v;
|
|
72
|
-
else
|
|
73
|
-
warnings.push(`linkToFilePage: expected a boolean; dropped`);
|
|
74
|
-
}
|
|
75
|
-
// ingestGithubAttachments: boolean
|
|
76
|
-
if ("ingestGithubAttachments" in c) {
|
|
77
|
-
const v = c.ingestGithubAttachments;
|
|
78
|
-
if (typeof v === "boolean")
|
|
79
|
-
config.ingestGithubAttachments = v;
|
|
80
|
-
else
|
|
81
|
-
warnings.push(`ingestGithubAttachments: expected a boolean; dropped`);
|
|
82
|
-
}
|
|
83
|
-
// meta.path / meta.state: booleans nested under `meta`
|
|
84
|
-
if ("meta" in c) {
|
|
85
|
-
const v = c.meta;
|
|
86
|
-
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
|
87
|
-
const meta = v;
|
|
88
|
-
if ("path" in meta) {
|
|
89
|
-
if (typeof meta.path === "boolean")
|
|
90
|
-
config.metaPath = meta.path;
|
|
91
|
-
else
|
|
92
|
-
warnings.push(`meta.path: expected a boolean; dropped`);
|
|
93
|
-
}
|
|
94
|
-
if ("state" in meta) {
|
|
95
|
-
if (typeof meta.state === "boolean")
|
|
96
|
-
config.metaState = meta.state;
|
|
97
|
-
else
|
|
98
|
-
warnings.push(`meta.state: expected a boolean; dropped`);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
warnings.push(`meta: expected an object; dropped`);
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// note: non-empty trimmed string, max NOTE_MAX_CHARS (never truncated)
|
|
106
|
-
if ("note" in c) {
|
|
107
|
-
const v = c.note;
|
|
108
|
-
if (typeof v === "string") {
|
|
109
|
-
const trimmed = v.trim();
|
|
110
|
-
if (trimmed.length === 0) {
|
|
111
|
-
// empty/whitespace note is treated as absent
|
|
112
|
-
}
|
|
113
|
-
else if (trimmed.length > NOTE_MAX_CHARS) {
|
|
114
|
-
warnings.push(`note: longer than ${NOTE_MAX_CHARS} characters; dropped (not truncated)`);
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
config.note = trimmed;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
warnings.push(`note: expected a string; dropped`);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return { config, warnings };
|
|
125
|
-
}
|
|
126
|
-
export function resolveCommentOptions(repo, ws) {
|
|
127
|
-
const wsAsRepo = {
|
|
128
|
-
...(ws?.imageWidth !== undefined ? { imageWidth: ws.imageWidth } : {}),
|
|
129
|
-
...(ws?.maxInlineImages !== undefined ? { maxInlineImages: ws.maxInlineImages } : {}),
|
|
130
|
-
...(ws?.showMetadata !== undefined
|
|
131
|
-
? { metaPath: ws.showMetadata, metaState: ws.showMetadata }
|
|
132
|
-
: {}),
|
|
133
|
-
...(ws?.linkToFilePage !== undefined ? { linkToFilePage: ws.linkToFilePage } : {}),
|
|
134
|
-
...(ws?.note ? { note: ws.note } : {}),
|
|
135
|
-
...(ws?.ingestGithubAttachments !== undefined
|
|
136
|
-
? { ingestGithubAttachments: ws.ingestGithubAttachments }
|
|
137
|
-
: {}),
|
|
138
|
-
};
|
|
139
|
-
const options = { ...AUTO_COMMENT_OPTIONS };
|
|
140
|
-
const source = Object.fromEntries(Object.keys(AUTO_COMMENT_OPTIONS).map((k) => [k, "auto"]));
|
|
141
|
-
const apply = (cfg, from) => {
|
|
142
|
-
for (const key of [
|
|
143
|
-
"imageWidth",
|
|
144
|
-
"maxInlineImages",
|
|
145
|
-
"metaPath",
|
|
146
|
-
"metaState",
|
|
147
|
-
"linkToFilePage",
|
|
148
|
-
"ingestGithubAttachments",
|
|
149
|
-
]) {
|
|
150
|
-
if (cfg[key] !== undefined && source[key] === "auto") {
|
|
151
|
-
options[key] = cfg[key];
|
|
152
|
-
source[key] = from;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
if (cfg.note !== undefined && source.note === "auto") {
|
|
156
|
-
options.note = cfg.note;
|
|
157
|
-
source.note = from;
|
|
158
|
-
}
|
|
159
|
-
};
|
|
160
|
-
if (repo)
|
|
161
|
-
apply(repo, "repo");
|
|
162
|
-
apply(wsAsRepo, "workspace");
|
|
163
|
-
return { options, source };
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Candidate paths, checked in this order — the first hit wins. Must match
|
|
167
|
-
* the server's REPO_CONFIG_PATHS exactly (apps/api/src/repo-comment-config.ts)
|
|
168
|
-
* so a committed config resolves identically whether the bot or the local
|
|
169
|
-
* gh fallback renders the comment.
|
|
170
|
-
*/
|
|
171
|
-
export const REPO_CONFIG_PATHS = [
|
|
172
|
-
".uploads.yml",
|
|
173
|
-
".uploads.yaml",
|
|
174
|
-
".uploads.json",
|
|
175
|
-
".github/uploads.yml",
|
|
176
|
-
".github/uploads.yaml",
|
|
177
|
-
".github/uploads.json",
|
|
178
|
-
];
|
|
10
|
+
import { parseRepoCommentConfig, REPO_CONFIG_PATHS, } from "./comment-config.generated.js";
|
|
11
|
+
export { AUTO_COMMENT_OPTIONS, NOTE_MAX_CHARS, REPO_CONFIG_PATHS, parseRepoCommentConfig, resolveCommentOptions, } from "./comment-config.generated.js";
|
|
179
12
|
/**
|
|
180
13
|
* Read the repo's comment config off the local working tree — the CLI has no
|
|
181
14
|
* GitHub App installation token to fetch via the contents API, so this reads
|