@buildinternet/uploads 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.js +98 -1
- package/dist/config-file.d.ts +3 -1
- package/dist/config-file.js +10 -0
- package/dist/github-gh.d.ts +16 -0
- package/dist/github-gh.js +33 -0
- package/dist/github.d.ts +12 -0
- package/dist/github.js +56 -7
- package/dist/keys.d.ts +8 -1
- package/dist/keys.js +11 -2
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -12,7 +12,8 @@ import { imageFactsFromBytes } from "./image-facts.js";
|
|
|
12
12
|
import { parseMetaFlags, validateMetaMap } from "./metadata.js";
|
|
13
13
|
import { mergeDerivedMeta, nearMissMetaWarnings, validateStateValue } from "./metadata-vocab.js";
|
|
14
14
|
import { ghAttachmentKey, ghBranchAttachmentKey, ghKeyPrefix, ghMetadataFromTarget, ghMetadataForBranch, attachmentsCommentBody, attachmentsMarker, normalizeGithubCoordinate, } from "./github.js";
|
|
15
|
-
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, classifyGhNumber, execRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
15
|
+
import { resolveRepo, resolveCurrentPullRequest, resolveCurrentBranch, resolveDefaultBranch, classifyGhNumber, execRunner, timedExecRunner, ghMetadataFromTargetWithTitle, upsertAttachmentsComment, } from "./github-gh.js";
|
|
16
|
+
import { deriveRepoFromGit } from "./keys.js";
|
|
16
17
|
import { resolvePutPrefix } from "./destinations.js";
|
|
17
18
|
import { optimizeImageForUpload, rewriteKeyExtension, } from "./optimize.js";
|
|
18
19
|
import { applyFrame, resolveFrameId } from "./frame.js";
|
|
@@ -111,6 +112,10 @@ Options:
|
|
|
111
112
|
--dry-run Print key + public URL without uploading; reports if the key would replace
|
|
112
113
|
(or, on a strict key, be refused). Not with --comment/--gallery
|
|
113
114
|
|
|
115
|
+
A bare put (no --pr/--issue/--key) on a non-default git branch prints a one-line
|
|
116
|
+
nudge toward --pr/attach --branch (stderr in human mode, a "hint" field in
|
|
117
|
+
--format json). Suppress with --quiet, UPLOADS_NO_NUDGE=1, or config UPLOADS_NO_NUDGE=1.
|
|
118
|
+
|
|
114
119
|
Exit codes: 0 ok · 2 usage/token/file · 3 auth/policy · 4 network · 1 other (incl. partial multi-file failure).
|
|
115
120
|
Scripted formats (json|url|markdown) also print failures on stdout.
|
|
116
121
|
|
|
@@ -1026,6 +1031,8 @@ async function runAttachBranch(ctx, parsed, branch, run) {
|
|
|
1026
1031
|
}
|
|
1027
1032
|
if (!ctx.quiet && uploads.length > 0) {
|
|
1028
1033
|
process.stderr.write(`>> find these later: uploads find gh.branch=${branch.toLowerCase()}\n`);
|
|
1034
|
+
process.stderr.write(`>> staged: these auto-attach to this branch's PR when it opens ` +
|
|
1035
|
+
`(or run \`uploads attach --promote\` after opening)\n`);
|
|
1029
1036
|
}
|
|
1030
1037
|
}
|
|
1031
1038
|
return failures.length === 0 ? 0 : 1;
|
|
@@ -1077,6 +1084,78 @@ async function runAttachPromoteOnly(ctx, parsed, run) {
|
|
|
1077
1084
|
}
|
|
1078
1085
|
return 0;
|
|
1079
1086
|
}
|
|
1087
|
+
/** Bounds the best-effort `gh pr view` lookup the put nudge (issue #393) makes
|
|
1088
|
+
* on top of the normal put flow — long enough for a real gh call, short
|
|
1089
|
+
* enough to never be felt as a hang. */
|
|
1090
|
+
const PUT_NUDGE_GH_TIMEOUT_MS = 3000;
|
|
1091
|
+
/**
|
|
1092
|
+
* The bare-put nudge's wording (issue #393): teaches `--pr`/`attach --branch`
|
|
1093
|
+
* as an upgrade from a targetless `put`. `pr` present → names the PR;
|
|
1094
|
+
* otherwise a generic variant that still points at `--pr <num>`. Used
|
|
1095
|
+
* verbatim for both the human-mode stderr line and the JSON `hint` field.
|
|
1096
|
+
*/
|
|
1097
|
+
function putNudgeText(branch, pr) {
|
|
1098
|
+
const prClause = pr !== undefined ? ` (PR #${pr} open) — rerun with --pr ${pr}` : ` — rerun with --pr <num>`;
|
|
1099
|
+
return (`note: on branch ${branch}${prClause} for a stable key plus a managed comment ` +
|
|
1100
|
+
`that collects this PR's media, or stage pre-PR files with: uploads attach <file> --branch`);
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Best-effort bare-put nudge (issue #393): fires only when `put` has no
|
|
1104
|
+
* targeting flag at all (`--pr`/`--issue`/`--key`; `--branch` too, though
|
|
1105
|
+
* `put` doesn't currently accept it — defensive parity with `attach`), is
|
|
1106
|
+
* inside a git repo (reusing `deriveRepoFromGit`, the same detection the
|
|
1107
|
+
* default screenshot key's repo segment uses), and the current branch isn't
|
|
1108
|
+
* the default one. Never throws — any failure (not a repo, detached HEAD,
|
|
1109
|
+
* `gh` missing/unauthenticated/timed out) degrades to "no nudge" or, once a
|
|
1110
|
+
* branch is already known, to the generic no-PR wording. Must never affect
|
|
1111
|
+
* put's exit code, stdout, or upload behavior.
|
|
1112
|
+
*/
|
|
1113
|
+
function resolvePutNudge(opts) {
|
|
1114
|
+
const { ctx, flags, ghTarget, keyHint, noGit, defaults, run } = opts;
|
|
1115
|
+
if (ctx.quiet)
|
|
1116
|
+
return undefined;
|
|
1117
|
+
if (defaults.noNudge)
|
|
1118
|
+
return undefined;
|
|
1119
|
+
if (ghTarget || keyHint || noGit)
|
|
1120
|
+
return undefined;
|
|
1121
|
+
if (flags.has("--branch"))
|
|
1122
|
+
return undefined; // not a real put flag today; defensive only
|
|
1123
|
+
try {
|
|
1124
|
+
if (deriveRepoFromGit(run) === undefined)
|
|
1125
|
+
return undefined; // not a (usable) git repo
|
|
1126
|
+
let branch;
|
|
1127
|
+
try {
|
|
1128
|
+
branch = resolveCurrentBranch(run);
|
|
1129
|
+
}
|
|
1130
|
+
catch {
|
|
1131
|
+
return undefined; // detached HEAD, or git unavailable
|
|
1132
|
+
}
|
|
1133
|
+
const defaultBranch = resolveDefaultBranch(run);
|
|
1134
|
+
const onDefaultBranch = defaultBranch
|
|
1135
|
+
? branch === defaultBranch
|
|
1136
|
+
: branch === "main" || branch === "master"; // undetermined: err toward not nudging
|
|
1137
|
+
if (onDefaultBranch)
|
|
1138
|
+
return undefined;
|
|
1139
|
+
let pr;
|
|
1140
|
+
try {
|
|
1141
|
+
// Only swap in the bounded runner for the real subprocess path — an
|
|
1142
|
+
// injected `run` (tests, or a future caller) is trusted to already be
|
|
1143
|
+
// fast/fake, and execFileSync's `timeout` option is meaningless
|
|
1144
|
+
// against anything that isn't actually shelling out.
|
|
1145
|
+
const timed = run === execRunner ? timedExecRunner(PUT_NUDGE_GH_TIMEOUT_MS) : run;
|
|
1146
|
+
const repoArg = flagString(flags, "--repo") ?? defaults.repo;
|
|
1147
|
+
const repo = resolveRepo(repoArg, timed);
|
|
1148
|
+
pr = resolveCurrentPullRequest(repo, timed).num;
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
pr = undefined; // gh missing/unauthenticated/timed out/no open PR — generic wording
|
|
1152
|
+
}
|
|
1153
|
+
return putNudgeText(branch, pr);
|
|
1154
|
+
}
|
|
1155
|
+
catch {
|
|
1156
|
+
return undefined;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1080
1159
|
export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
1081
1160
|
if (help) {
|
|
1082
1161
|
writeCommandHelp(PUT_HELP);
|
|
@@ -1227,6 +1306,18 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1227
1306
|
}
|
|
1228
1307
|
}
|
|
1229
1308
|
}
|
|
1309
|
+
// Bare-put nudge (issue #393): computed once, used for both the trailing
|
|
1310
|
+
// stderr line (human mode) and the JSON `hint` field below. Best-effort —
|
|
1311
|
+
// see resolvePutNudge; never affects exit code, stdout, or the upload.
|
|
1312
|
+
const nudge = resolvePutNudge({
|
|
1313
|
+
ctx,
|
|
1314
|
+
flags: parsed.flags,
|
|
1315
|
+
ghTarget,
|
|
1316
|
+
keyHint,
|
|
1317
|
+
noGit,
|
|
1318
|
+
defaults,
|
|
1319
|
+
run,
|
|
1320
|
+
});
|
|
1230
1321
|
const logHuman = !ctx.quiet && format === "human";
|
|
1231
1322
|
if (logHuman) {
|
|
1232
1323
|
if (multi) {
|
|
@@ -1308,6 +1399,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1308
1399
|
failures,
|
|
1309
1400
|
comment,
|
|
1310
1401
|
commentError,
|
|
1402
|
+
...(nudge ? { hint: nudge } : {}),
|
|
1311
1403
|
});
|
|
1312
1404
|
}
|
|
1313
1405
|
else {
|
|
@@ -1338,6 +1430,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1338
1430
|
for (const failure of failures) {
|
|
1339
1431
|
process.stderr.write(`warning: could not upload ${failure.file}: ${failure.error.message}\n`);
|
|
1340
1432
|
}
|
|
1433
|
+
if (nudge)
|
|
1434
|
+
process.stderr.write(`${nudge}\n`);
|
|
1341
1435
|
}
|
|
1342
1436
|
return failures.length === 0 && !galleryHadError ? 0 : 1;
|
|
1343
1437
|
}
|
|
@@ -1369,6 +1463,7 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1369
1463
|
frame: result.frame,
|
|
1370
1464
|
gallery,
|
|
1371
1465
|
...(dryRun ? { dryRun: true } : {}),
|
|
1466
|
+
...(nudge ? { hint: nudge } : {}),
|
|
1372
1467
|
});
|
|
1373
1468
|
break;
|
|
1374
1469
|
case "url":
|
|
@@ -1388,6 +1483,8 @@ export async function runPut(ctx, args, help = false, run = execRunner) {
|
|
|
1388
1483
|
if (gallery?.error) {
|
|
1389
1484
|
process.stderr.write(`warning: upload succeeded but adding it to gallery ${gallery.id} failed: ${gallery.error.message}\n`);
|
|
1390
1485
|
}
|
|
1486
|
+
if (nudge && format !== "json")
|
|
1487
|
+
process.stderr.write(`${nudge}\n`);
|
|
1391
1488
|
return gallery?.error ? 1 : 0;
|
|
1392
1489
|
}
|
|
1393
1490
|
// --- galleries ---
|
package/dist/config-file.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UploadsClientConfig } from "./config.js";
|
|
2
|
-
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META", "UPLOADS_SCREENSHOT_VIA"];
|
|
2
|
+
export declare const UPLOADS_CONFIG_KEYS: readonly ["UPLOADS_API_URL", "UPLOADS_WORKSPACE", "UPLOADS_TOKEN", "UPLOADS_DEFAULT_PREFIX", "UPLOADS_DEFAULT_REPO", "UPLOADS_DEFAULT_REF", "UPLOADS_DEFAULT_WIDTH", "UPLOADS_NO_GIT", "UPLOADS_NO_OPTIMIZE", "UPLOADS_KEEP_EXIF", "UPLOADS_NO_AUTO_META", "UPLOADS_SCREENSHOT_VIA", "UPLOADS_NO_NUDGE"];
|
|
3
3
|
export type UploadsConfigKey = (typeof UPLOADS_CONFIG_KEYS)[number];
|
|
4
4
|
export type UploadsConfigValues = Partial<Record<UploadsConfigKey, string>>;
|
|
5
5
|
export interface PutDefaults {
|
|
@@ -14,6 +14,8 @@ export interface PutDefaults {
|
|
|
14
14
|
keepExif?: boolean;
|
|
15
15
|
/** When true, `put` does NOT auto-resolve/stamp gh.* on the default path. */
|
|
16
16
|
noAutoMeta?: boolean;
|
|
17
|
+
/** When true, `put` never prints the bare-put --pr/attach nudge (issue #393). */
|
|
18
|
+
noNudge?: boolean;
|
|
17
19
|
}
|
|
18
20
|
declare const PUT_DEFAULT_KEY_MAP: Record<keyof PutDefaults, UploadsConfigKey>;
|
|
19
21
|
export declare function putDefaultsToConfigValues(defaults: PutDefaults): UploadsConfigValues;
|
package/dist/config-file.js
CHANGED
|
@@ -14,6 +14,7 @@ export const UPLOADS_CONFIG_KEYS = [
|
|
|
14
14
|
"UPLOADS_KEEP_EXIF",
|
|
15
15
|
"UPLOADS_NO_AUTO_META",
|
|
16
16
|
"UPLOADS_SCREENSHOT_VIA",
|
|
17
|
+
"UPLOADS_NO_NUDGE",
|
|
17
18
|
];
|
|
18
19
|
const PUT_DEFAULT_KEY_MAP = {
|
|
19
20
|
prefix: "UPLOADS_DEFAULT_PREFIX",
|
|
@@ -24,6 +25,7 @@ const PUT_DEFAULT_KEY_MAP = {
|
|
|
24
25
|
noOptimize: "UPLOADS_NO_OPTIMIZE",
|
|
25
26
|
keepExif: "UPLOADS_KEEP_EXIF",
|
|
26
27
|
noAutoMeta: "UPLOADS_NO_AUTO_META",
|
|
28
|
+
noNudge: "UPLOADS_NO_NUDGE",
|
|
27
29
|
};
|
|
28
30
|
function isTruthyConfigFlag(value) {
|
|
29
31
|
if (!value)
|
|
@@ -49,6 +51,8 @@ export function putDefaultsToConfigValues(defaults) {
|
|
|
49
51
|
out.UPLOADS_KEEP_EXIF = "1";
|
|
50
52
|
if (defaults.noAutoMeta)
|
|
51
53
|
out.UPLOADS_NO_AUTO_META = "1";
|
|
54
|
+
if (defaults.noNudge)
|
|
55
|
+
out.UPLOADS_NO_NUDGE = "1";
|
|
52
56
|
return out;
|
|
53
57
|
}
|
|
54
58
|
function parsePutDefaultsFromRaw(raw) {
|
|
@@ -72,6 +76,8 @@ function parsePutDefaultsFromRaw(raw) {
|
|
|
72
76
|
out.keepExif = true;
|
|
73
77
|
if (isTruthyConfigFlag(raw.UPLOADS_NO_AUTO_META))
|
|
74
78
|
out.noAutoMeta = true;
|
|
79
|
+
if (isTruthyConfigFlag(raw.UPLOADS_NO_NUDGE))
|
|
80
|
+
out.noNudge = true;
|
|
75
81
|
return out;
|
|
76
82
|
}
|
|
77
83
|
function parsePutDefaultsFromEnv() {
|
|
@@ -92,6 +98,8 @@ function parsePutDefaultsFromEnv() {
|
|
|
92
98
|
raw.UPLOADS_KEEP_EXIF = process.env.UPLOADS_KEEP_EXIF;
|
|
93
99
|
if (process.env.UPLOADS_NO_AUTO_META)
|
|
94
100
|
raw.UPLOADS_NO_AUTO_META = process.env.UPLOADS_NO_AUTO_META;
|
|
101
|
+
if (process.env.UPLOADS_NO_NUDGE)
|
|
102
|
+
raw.UPLOADS_NO_NUDGE = process.env.UPLOADS_NO_NUDGE;
|
|
95
103
|
return parsePutDefaultsFromRaw(raw);
|
|
96
104
|
}
|
|
97
105
|
/** XDG default shared across buildinternet skills (github-screenshots, uploads, …). */
|
|
@@ -163,6 +171,8 @@ export function mergePutDefaults(...layers) {
|
|
|
163
171
|
out.keepExif = layer.keepExif;
|
|
164
172
|
if (layer.noAutoMeta != null)
|
|
165
173
|
out.noAutoMeta = layer.noAutoMeta;
|
|
174
|
+
if (layer.noNudge != null)
|
|
175
|
+
out.noNudge = layer.noNudge;
|
|
166
176
|
}
|
|
167
177
|
return out;
|
|
168
178
|
}
|
package/dist/github-gh.d.ts
CHANGED
|
@@ -2,6 +2,14 @@ import { type GhTarget } from "./github.js";
|
|
|
2
2
|
/** Runs a command and returns stdout; throws on non-zero exit. Injectable for tests. */
|
|
3
3
|
export type CommandRunner = (cmd: string, args: string[], input?: string) => string;
|
|
4
4
|
export declare const execRunner: CommandRunner;
|
|
5
|
+
/**
|
|
6
|
+
* A `CommandRunner` bounded by `timeoutMs` (node's native `execFileSync`
|
|
7
|
+
* `timeout` option). There is no other subprocess-timeout wrapper in this
|
|
8
|
+
* codebase to reuse, so this is the minimal one: for a best-effort lookup
|
|
9
|
+
* that must never block its caller for long (e.g. the bare-`put` nudge's `gh
|
|
10
|
+
* pr view` check, issue #393), pass this instead of the default `execRunner`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const timedExecRunner: (timeoutMs: number) => CommandRunner;
|
|
5
13
|
/**
|
|
6
14
|
* Resolve "owner/name". Order: explicit --repo (validated) → `gh repo view`
|
|
7
15
|
* (fork-aware) → parse the origin remote → UsageError.
|
|
@@ -11,6 +19,14 @@ export declare function resolveRepo(explicit: string | undefined, run?: CommandR
|
|
|
11
19
|
export declare function resolveCurrentPullRequest(repo: string, run?: CommandRunner): GhTarget;
|
|
12
20
|
/** Resolve the current git branch (`--branch` with no value). Throws UsageError on detached HEAD or outside a git repo. */
|
|
13
21
|
export declare function resolveCurrentBranch(run?: CommandRunner): string;
|
|
22
|
+
/**
|
|
23
|
+
* Best-effort default-branch name via the local `origin/HEAD` ref (no
|
|
24
|
+
* network call — just reads the ref git already cached from the last
|
|
25
|
+
* fetch/clone). Returns undefined when it can't be determined (no origin,
|
|
26
|
+
* `origin/HEAD` never set, not a git repo) — callers should treat that as
|
|
27
|
+
* "unknown", not "no default branch exists".
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveDefaultBranch(run?: CommandRunner): string | undefined;
|
|
14
30
|
/**
|
|
15
31
|
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
16
32
|
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
package/dist/github-gh.js
CHANGED
|
@@ -3,6 +3,19 @@ import { UsageError } from "./cli-args.js";
|
|
|
3
3
|
import { ATTACHMENTS_MARKER, ghMetadataFromTarget, isValidRepo, parseRepoFromRemoteUrl, } from "./github.js";
|
|
4
4
|
import { META_VALUE_MAX, isMetaValueSafe } from "./metadata.js";
|
|
5
5
|
export const execRunner = (cmd, args, input) => execFileSync(cmd, args, { encoding: "utf8", input, stdio: ["pipe", "pipe", "pipe"] });
|
|
6
|
+
/**
|
|
7
|
+
* A `CommandRunner` bounded by `timeoutMs` (node's native `execFileSync`
|
|
8
|
+
* `timeout` option). There is no other subprocess-timeout wrapper in this
|
|
9
|
+
* codebase to reuse, so this is the minimal one: for a best-effort lookup
|
|
10
|
+
* that must never block its caller for long (e.g. the bare-`put` nudge's `gh
|
|
11
|
+
* pr view` check, issue #393), pass this instead of the default `execRunner`.
|
|
12
|
+
*/
|
|
13
|
+
export const timedExecRunner = (timeoutMs) => (cmd, args, input) => execFileSync(cmd, args, {
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
input,
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
+
timeout: timeoutMs,
|
|
18
|
+
});
|
|
6
19
|
/**
|
|
7
20
|
* Resolve "owner/name". Order: explicit --repo (validated) → `gh repo view`
|
|
8
21
|
* (fork-aware) → parse the origin remote → UsageError.
|
|
@@ -82,6 +95,26 @@ export function resolveCurrentBranch(run = execRunner) {
|
|
|
82
95
|
}
|
|
83
96
|
return branch;
|
|
84
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Best-effort default-branch name via the local `origin/HEAD` ref (no
|
|
100
|
+
* network call — just reads the ref git already cached from the last
|
|
101
|
+
* fetch/clone). Returns undefined when it can't be determined (no origin,
|
|
102
|
+
* `origin/HEAD` never set, not a git repo) — callers should treat that as
|
|
103
|
+
* "unknown", not "no default branch exists".
|
|
104
|
+
*/
|
|
105
|
+
export function resolveDefaultBranch(run = execRunner) {
|
|
106
|
+
try {
|
|
107
|
+
const out = run("git", ["symbolic-ref", "--short", "refs/remotes/origin/HEAD"]).trim();
|
|
108
|
+
if (!out)
|
|
109
|
+
return undefined;
|
|
110
|
+
const slash = out.indexOf("/");
|
|
111
|
+
const branch = slash === -1 ? out : out.slice(slash + 1);
|
|
112
|
+
return branch || undefined;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
85
118
|
/**
|
|
86
119
|
* Classify a bare PR/issue number via the GitHub API so the default `put`
|
|
87
120
|
* path can stamp the right `gh.kind`. Returns undefined on any failure (gh
|
package/dist/github.d.ts
CHANGED
|
@@ -86,6 +86,18 @@ export interface AttachmentItem {
|
|
|
86
86
|
path?: string;
|
|
87
87
|
state?: string;
|
|
88
88
|
};
|
|
89
|
+
/**
|
|
90
|
+
* Poster frame for a video (issue #299), server-computed like `embedUrl` —
|
|
91
|
+
* never taken from client-settable metadata. Absent means "no poster", and
|
|
92
|
+
* the renderer falls back to the bullet link.
|
|
93
|
+
*/
|
|
94
|
+
posterUrl?: string | null;
|
|
95
|
+
/** Derived video facts used for the caption and display width. */
|
|
96
|
+
videoMeta?: {
|
|
97
|
+
durationSeconds?: number;
|
|
98
|
+
width?: number;
|
|
99
|
+
height?: number;
|
|
100
|
+
};
|
|
89
101
|
}
|
|
90
102
|
/** A public gallery linked to the PR or issue whose managed comment is syncing. */
|
|
91
103
|
export interface GalleryCommentItem {
|
package/dist/github.js
CHANGED
|
@@ -171,6 +171,34 @@ export function attachmentImageWidth(filename) {
|
|
|
171
171
|
}
|
|
172
172
|
return ATTACHMENT_IMAGE_WIDTH_DEFAULT;
|
|
173
173
|
}
|
|
174
|
+
/** `m:ss` under an hour, `h:mm:ss` at or above one. */
|
|
175
|
+
function formatDuration(seconds) {
|
|
176
|
+
const total = Math.floor(seconds);
|
|
177
|
+
const s = total % 60;
|
|
178
|
+
const m = Math.floor(total / 60) % 60;
|
|
179
|
+
const h = Math.floor(total / 3600);
|
|
180
|
+
const ss = String(s).padStart(2, "0");
|
|
181
|
+
if (h === 0)
|
|
182
|
+
return `${m}:${ss}`;
|
|
183
|
+
return `${h}:${String(m).padStart(2, "0")}:${ss}`;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Display width for a video poster. Real dimensions only *select* among the
|
|
187
|
+
* width constants — a raw 1920 would blow out the comment column — and the
|
|
188
|
+
* result is capped at the real width so a small clip is never upscaled.
|
|
189
|
+
*/
|
|
190
|
+
function posterImageWidth(videoMeta, filename) {
|
|
191
|
+
const w = videoMeta?.width ?? 0;
|
|
192
|
+
const h = videoMeta?.height ?? 0;
|
|
193
|
+
if (w <= 0 || h <= 0)
|
|
194
|
+
return attachmentImageWidth(filename);
|
|
195
|
+
const chosen = h > w
|
|
196
|
+
? ATTACHMENT_IMAGE_WIDTH_PORTRAIT
|
|
197
|
+
: w / h >= 16 / 9
|
|
198
|
+
? ATTACHMENT_IMAGE_WIDTH_WIDE
|
|
199
|
+
: ATTACHMENT_IMAGE_WIDTH_DEFAULT;
|
|
200
|
+
return Math.min(chosen, w);
|
|
201
|
+
}
|
|
174
202
|
function escapeHtmlAttr(s) {
|
|
175
203
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
176
204
|
}
|
|
@@ -196,14 +224,19 @@ function escapeMarkdownText(s) {
|
|
|
196
224
|
* `PATCH /v1/:workspace/files/:key` can set any valid metadata value. A
|
|
197
225
|
* whitespace-only value passes that validation (length-1 printable ASCII), so
|
|
198
226
|
* treat it as absent rather than rendering a dangling separator.
|
|
227
|
+
*
|
|
228
|
+
* Bare `/` is stored/searchable but omitted from captions (issue #375) —
|
|
229
|
+
* alone it is a stray character, and as a prefix next to `state` it is
|
|
230
|
+
* noise. Only exact `/` after trim is suppressed.
|
|
199
231
|
*/
|
|
200
232
|
function metaCaptionParts(meta) {
|
|
201
233
|
const parts = [];
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
234
|
+
const path = meta?.path?.trim();
|
|
235
|
+
if (path && path !== "/")
|
|
236
|
+
parts.push(path);
|
|
237
|
+
const state = meta?.state?.trim();
|
|
238
|
+
if (state)
|
|
239
|
+
parts.push(state);
|
|
207
240
|
return parts;
|
|
208
241
|
}
|
|
209
242
|
/** `<sub>` caption body for an inline image, or null when there is nothing to say. */
|
|
@@ -254,13 +287,29 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
|
|
|
254
287
|
const src = item.embedUrl ?? item.url;
|
|
255
288
|
const link = item.pageUrl ?? stable; // click-through: file page when known, else raw
|
|
256
289
|
const isImage = Boolean(src) && inferContentType(name).startsWith("image/");
|
|
257
|
-
|
|
290
|
+
const isPosterVideo = Boolean(item.posterUrl) && inferContentType(name).startsWith("video/");
|
|
291
|
+
const inlines = isImage || isPosterVideo;
|
|
292
|
+
if (inlines && inlinedImages >= MAX_INLINE_ATTACHMENT_IMAGES) {
|
|
258
293
|
// Cap hit — defer to the collapsed overflow list below rather than
|
|
259
294
|
// embedding every remaining image inline.
|
|
260
295
|
overflowImages.push(item);
|
|
261
296
|
continue;
|
|
262
297
|
}
|
|
263
|
-
if (
|
|
298
|
+
if (isPosterVideo) {
|
|
299
|
+
inlinedImages++;
|
|
300
|
+
const w = posterImageWidth(item.videoMeta, name);
|
|
301
|
+
const href = escapeHtmlAttr(link ?? item.posterUrl);
|
|
302
|
+
lines.push(`<a href="${href}"><img width="${w}" alt="${escapeHtmlAttr(name)}" src="${escapeHtmlAttr(item.posterUrl)}"></a>`);
|
|
303
|
+
// GitHub strips <video>, so a still frame needs an explicit affordance
|
|
304
|
+
// or it reads as a screenshot.
|
|
305
|
+
const parts = ["▶ Play video"];
|
|
306
|
+
if (item.videoMeta?.durationSeconds != null) {
|
|
307
|
+
parts.push(formatDuration(item.videoMeta.durationSeconds));
|
|
308
|
+
}
|
|
309
|
+
parts.push(...metaCaptionParts(item.meta).map(escapeHtmlText));
|
|
310
|
+
lines.push(`<sub>${parts.join(" · ")}</sub>`, "");
|
|
311
|
+
}
|
|
312
|
+
else if (isImage) {
|
|
264
313
|
inlinedImages++;
|
|
265
314
|
// Markdown ![]() has no width control — phone frames become full-column giants.
|
|
266
315
|
// img src uses embed host when available (Camo revalidates); click-through prefers the file page.
|
package/dist/keys.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export declare function sanitizeKeySegment(s: string): string;
|
|
2
2
|
export declare function sha256Short(bytes: Uint8Array): Promise<string>;
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* `run` is optional and structurally matches `CommandRunner` (github-gh.ts)
|
|
5
|
+
* without importing it — callers that already have an injected runner (e.g.
|
|
6
|
+
* the put nudge, issue #393) can pass it through for testability; the
|
|
7
|
+
* default (no `run`) preserves the original direct-`execSync` behavior for
|
|
8
|
+
* every existing caller.
|
|
9
|
+
*/
|
|
10
|
+
export declare function deriveRepoFromGit(run?: (cmd: string, args: string[], input?: string) => string): string | undefined;
|
|
4
11
|
export declare function buildScreenshotKey(opts: {
|
|
5
12
|
filename: string;
|
|
6
13
|
fileBytes: Uint8Array;
|
package/dist/keys.js
CHANGED
|
@@ -9,9 +9,18 @@ export async function sha256Short(bytes) {
|
|
|
9
9
|
.join("")
|
|
10
10
|
.slice(0, 6);
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/**
|
|
13
|
+
* `run` is optional and structurally matches `CommandRunner` (github-gh.ts)
|
|
14
|
+
* without importing it — callers that already have an injected runner (e.g.
|
|
15
|
+
* the put nudge, issue #393) can pass it through for testability; the
|
|
16
|
+
* default (no `run`) preserves the original direct-`execSync` behavior for
|
|
17
|
+
* every existing caller.
|
|
18
|
+
*/
|
|
19
|
+
export function deriveRepoFromGit(run) {
|
|
13
20
|
try {
|
|
14
|
-
const url =
|
|
21
|
+
const url = run
|
|
22
|
+
? run("git", ["config", "--get", "remote.origin.url"]).trim()
|
|
23
|
+
: execSync("git config --get remote.origin.url", { encoding: "utf8" }).trim();
|
|
15
24
|
const match = url.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
16
25
|
return match?.[1];
|
|
17
26
|
}
|