@buildinternet/uploads 0.42.1 → 0.43.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.
@@ -1,181 +1,14 @@
1
1
  /**
2
- * Published-CLI COPY of the workspace parser/resolver (issue #307)
3
- * (packages/comment-config/src/index.ts). The published CLI imports no
4
- * @uploads/* package, so that private package cannot be shared here — its
5
- * parser/resolver is copied verbatim below. Kept in sync by
6
- * test/fixtures/comment-config-golden.json, asserted from both sides (this
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 { parse as parseYaml } from "yaml";
16
- export const AUTO_COMMENT_OPTIONS = {
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
@@ -0,0 +1,151 @@
1
+ /**
2
+ * GENERATED by packages/uploads/scripts/inline-shared.mjs — do not edit.
3
+ * Canonical source: packages/comment-render/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 type GhTargetKind = "pull" | "issues";
8
+ export interface GhTarget {
9
+ /** "owner/name" */
10
+ repo: string;
11
+ kind: GhTargetKind;
12
+ num: number;
13
+ }
14
+ /** Non-safe chars → `-` for a single R2 key segment (owner/name/branch/…).
15
+ * Exported for reuse by other GitHub-key builders (github-ingest.ts) that
16
+ * need the identical sanitization rule — github-promote.ts keeps its own
17
+ * byte-identical private copy rather than importing this one. */
18
+ export declare function sanitizeKeySegment(s: string): string;
19
+ export declare function ghKeyPrefix(target: GhTarget): string;
20
+ /** Literal root under which every private-repo attachment key lives. */
21
+ export declare const GH_PRIVATE_ROOT = "gh/private/";
22
+ /**
23
+ * Private-repo key prefix: `gh/private/<32-hex-id>/<kind>/<num>/`.
24
+ * Deliberately omits the repo (unlike `ghKeyPrefix`) — the id is a random,
25
+ * unguessable per-repo prefix rather than an owner/name path, so callers
26
+ * that need the repo back must read `gh.repo` metadata (see
27
+ * `parseGhPrivateKey`, which cannot recover it from the key alone).
28
+ */
29
+ export declare function ghPrivateKeyPrefix(prefixId: string, target: GhTarget): string;
30
+ /** Private-repo attachment key: `ghPrivateKeyPrefix` + the sanitized filename. */
31
+ export declare function ghPrivateAttachmentKey(prefixId: string, target: GhTarget, filename: string): string;
32
+ /**
33
+ * Private-repo branch-staged key prefix: `gh/private/<32-hex-id>/branch/`.
34
+ * Unlike `ghBranchKeyPrefix`, there is deliberately NO branch-name segment —
35
+ * the branch name itself is not embedded in a private-repo key.
36
+ */
37
+ export declare function ghPrivateBranchKeyPrefix(prefixId: string): string;
38
+ /** Private-repo branch-staged attachment key: `ghPrivateBranchKeyPrefix` + the sanitized filename. */
39
+ export declare function ghPrivateBranchAttachmentKey(prefixId: string, filename: string): string;
40
+ /**
41
+ * Inverse of `ghPrivateKeyPrefix`: parse the prefix id/kind/number back out
42
+ * of a private-repo attachment key, or undefined for any other key shape.
43
+ * Cannot recover the repo — callers that need it read `gh.repo` metadata.
44
+ */
45
+ export declare function parseGhPrivateKey(key: string): {
46
+ prefixId: string;
47
+ kind: GhTargetKind;
48
+ num: number;
49
+ } | undefined;
50
+ /** Hidden marker identifying the one comment this CLI manages. Never change it — existing comments are found by exact match. */
51
+ export declare const ATTACHMENTS_MARKER = "<!-- uploads.sh:attachments -->";
52
+ /**
53
+ * Per-workspace marker (`<!-- uploads.sh:attachments ws=<workspace> -->`) so
54
+ * two workspaces managing the same repo don't clobber each other's comment.
55
+ * Falls back to the shared legacy marker when `workspace` is missing or does
56
+ * not look like a safe slug — degrade, don't guess or risk breaking the
57
+ * comment's HTML.
58
+ */
59
+ export declare function attachmentsMarker(workspace?: string): string;
60
+ /** Max attachments embedded as inline `<img>` tags before the rest collapse
61
+ * into a `<details>` link list. Keeps very large threads from becoming a wall
62
+ * of images. */
63
+ export declare const MAX_INLINE_ATTACHMENT_IMAGES = 16;
64
+ /**
65
+ * Per-render knobs for the managed comment (issue #307), sourced from repo
66
+ * comment config. `imageWidth: "auto"` uses per-item filename heuristics plus
67
+ * density-aware sizing (solo/sparse/dense from the inlined count); `"full"`
68
+ * omits the `width` attribute entirely; a number overrides every width site.
69
+ */
70
+ export interface CommentRenderOptions {
71
+ imageWidth: "auto" | "full" | number;
72
+ maxInlineImages: number;
73
+ metaPath: boolean;
74
+ metaState: boolean;
75
+ note: string | null;
76
+ }
77
+ /** Today's behavior, expressed as options — the default for every caller that
78
+ * hasn't opted into repo comment config. */
79
+ export declare const AUTO_RENDER_OPTIONS: CommentRenderOptions;
80
+ export interface AttachmentItem {
81
+ key: string;
82
+ url: string | null;
83
+ /** Prefer for `<img src>` on GitHub (Camo-friendly host). Falls back to `url`. */
84
+ embedUrl?: string | null;
85
+ /** Canonical `/f/` file-page URL (server-computed). Preferred click-through target; falls back to `url`. */
86
+ pageUrl?: string | null;
87
+ /**
88
+ * The only canonical metadata the managed comment renders (issue #365).
89
+ * Deliberately two named fields rather than `Record<string, string>`: the
90
+ * comment is posted publicly, and keeping the set narrow at the type level
91
+ * mirrors the server-side query filter that never fetches EXIF-derived
92
+ * keys like `device`/`software` for this path.
93
+ */
94
+ meta?: {
95
+ path?: string;
96
+ state?: string;
97
+ };
98
+ /**
99
+ * Poster frame for a video (issue #299), server-computed like `embedUrl` —
100
+ * never taken from client-settable metadata. Absent means "no poster", and
101
+ * the renderer falls back to the bullet link.
102
+ */
103
+ posterUrl?: string | null;
104
+ /** Derived video facts used for the caption and display width. */
105
+ videoMeta?: {
106
+ durationSeconds?: number;
107
+ width?: number;
108
+ height?: number;
109
+ };
110
+ }
111
+ /** A public gallery linked to the PR or issue whose managed comment is syncing. */
112
+ export interface GalleryCommentItem {
113
+ title: string;
114
+ /** Canonical URL returned by the API; callers must not synthesize it. */
115
+ url: string;
116
+ /** A bounded set of available images; each links to its item page when known, else the gallery. */
117
+ previews?: {
118
+ url: string;
119
+ alt: string;
120
+ embedUrl?: string | null;
121
+ itemUrl?: string;
122
+ }[];
123
+ }
124
+ /**
125
+ * How crowded the managed comment is. Sparse comments (one shot, a single
126
+ * before/after) get larger embeds; dense comments keep compact historical sizes.
127
+ */
128
+ export type AttachmentDensity = "solo" | "sparse" | "dense";
129
+ /** Dense (historical) default max width for images in the managed comment. */
130
+ export declare const ATTACHMENT_IMAGE_WIDTH_DEFAULT = 400;
131
+ /** Portrait / device mockups — keep phones readable, not full-column. */
132
+ export declare const ATTACHMENT_IMAGE_WIDTH_PORTRAIT = 280;
133
+ /** Wide UI / browser chrome. */
134
+ export declare const ATTACHMENT_IMAGE_WIDTH_WIDE = 640;
135
+ /** Dense pair-cell cap (side-by-side before/after). */
136
+ export declare const ATTACHMENT_IMAGE_WIDTH_PAIR = 320;
137
+ /** Map an inlined-media count onto a density tier. */
138
+ export declare function attachmentDensityForCount(inlinedCount: number): AttachmentDensity;
139
+ /** Pair-cell cap for the given density. */
140
+ export declare function attachmentPairWidth(density?: AttachmentDensity): number;
141
+ /**
142
+ * Display width for a GitHub comment embed. Filenames are a weak but practical
143
+ * signal (we don't re-fetch dimensions when rebuilding the comment). `density`
144
+ * only affects managed-comment auto layout; other callers leave it `"dense"`.
145
+ */
146
+ export declare function attachmentImageWidth(filename: string, density?: AttachmentDensity): number;
147
+ /**
148
+ * Render the one marker-owned GitHub comment. When there are no galleries this
149
+ * intentionally preserves the legacy attachment-only body byte-for-byte.
150
+ */
151
+ export declare function attachmentsCommentBody(items: AttachmentItem[], galleries?: GalleryCommentItem[], marker?: string, options?: CommentRenderOptions): string;