@buildinternet/uploads 0.16.0 → 0.17.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.
@@ -0,0 +1,8 @@
1
+ export interface DistStalenessResult {
2
+ stale: boolean;
3
+ checked: boolean;
4
+ reason?: string;
5
+ }
6
+
7
+ export declare function checkDistStaleness(packageRoot: string): DistStalenessResult;
8
+ export declare function warnIfDistStale(packageRoot: string): void;
@@ -0,0 +1,94 @@
1
+ // Dev-only staleness guard for the linked `uploads` CLI (issue #295).
2
+ //
3
+ // A globally-linked `uploads` binary resolves to this monorepo package and
4
+ // imports the compiled `dist/cli.js`. `dist/` is only rebuilt by an explicit
5
+ // `pnpm --filter @buildinternet/uploads build`, so after pulling new source
6
+ // the linked CLI can silently keep running old compiled code while
7
+ // `--version` still reports the current package version.
8
+ //
9
+ // This module is intentionally plain, dependency-free JS (not compiled)
10
+ // so it can run before `dist/` is imported, and so it has no cost at all
11
+ // for a published npm install: `files` in package.json ships only
12
+ // `bin/`, `dist/`, and `README.md` — never `src/` — so `sourceDir` below
13
+ // simply won't exist there, and `checkDistStaleness` returns early after
14
+ // one `existsSync` call.
15
+
16
+ import { existsSync, readdirSync, statSync } from "node:fs";
17
+ import { join } from "node:path";
18
+
19
+ /** Recursively find the newest mtime (ms) among files under `dir`. */
20
+ function newestMtimeMs(dir) {
21
+ let newest = -Infinity;
22
+ const stack = [dir];
23
+ while (stack.length > 0) {
24
+ const current = stack.pop();
25
+ let entries;
26
+ try {
27
+ entries = readdirSync(current, { withFileTypes: true });
28
+ } catch {
29
+ continue;
30
+ }
31
+ for (const entry of entries) {
32
+ if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
33
+ const full = join(current, entry.name);
34
+ if (entry.isDirectory()) {
35
+ stack.push(full);
36
+ } else if (entry.isFile()) {
37
+ try {
38
+ const mtimeMs = statSync(full).mtimeMs;
39
+ if (mtimeMs > newest) newest = mtimeMs;
40
+ } catch {
41
+ // ignore races (file removed between readdir and stat)
42
+ }
43
+ }
44
+ }
45
+ }
46
+ return newest;
47
+ }
48
+
49
+ /**
50
+ * Compare source mtime against dist mtime.
51
+ *
52
+ * @param {string} packageRoot absolute path to packages/uploads
53
+ * @returns {{ stale: boolean, checked: boolean, reason?: string }}
54
+ * `checked` is false when there's no source tree to compare against
55
+ * (i.e. a published install) — the check is a no-op in that case.
56
+ */
57
+ export function checkDistStaleness(packageRoot) {
58
+ const sourceDir = join(packageRoot, "src");
59
+ const distDir = join(packageRoot, "dist");
60
+
61
+ // No src/ tree ships in the published npm package (see package.json
62
+ // "files"), so this is the cheapest reliable signal that we're running
63
+ // inside the monorepo (dev/linked context) rather than a published install.
64
+ if (!existsSync(sourceDir)) {
65
+ return { stale: false, checked: false };
66
+ }
67
+
68
+ if (!existsSync(distDir)) {
69
+ return { stale: true, checked: true, reason: "dist/ is missing" };
70
+ }
71
+
72
+ const sourceMtime = newestMtimeMs(sourceDir);
73
+ const distMtime = newestMtimeMs(distDir);
74
+
75
+ if (sourceMtime > distMtime) {
76
+ return { stale: true, checked: true, reason: "dist/ predates src/" };
77
+ }
78
+
79
+ return { stale: false, checked: true };
80
+ }
81
+
82
+ /** Print a one-line stderr warning if the dev build looks stale. Never throws. */
83
+ export function warnIfDistStale(packageRoot) {
84
+ try {
85
+ const result = checkDistStaleness(packageRoot);
86
+ if (result.checked && result.stale) {
87
+ process.stderr.write(
88
+ `warning: uploads dev build is stale (${result.reason}) — run \`pnpm --filter @buildinternet/uploads build\`\n`,
89
+ );
90
+ }
91
+ } catch {
92
+ // Never let the staleness check itself break the CLI.
93
+ }
94
+ }
package/bin/uploads.js CHANGED
@@ -1,5 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from "../dist/cli.js";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+ import { warnIfDistStale } from "./dist-staleness.mjs";
5
+
6
+ const packageRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
7
+ warnIfDistStale(packageRoot);
8
+
9
+ const { runCli } = await import("../dist/cli.js");
3
10
 
4
11
  runCli(process.argv)
5
12
  .then((code) => process.exit(code ?? 0))
package/dist/client.d.ts CHANGED
@@ -236,6 +236,13 @@ export interface GithubHealthResult {
236
236
  events: string[] | null;
237
237
  missingEvents: string[];
238
238
  requiredEvents: string[];
239
+ /**
240
+ * Recommended-but-non-gating events, e.g. `issue_comment` (issue #333).
241
+ * Optional: older servers' health payload predates this field — treat a
242
+ * missing field as "no recommendations", not an error.
243
+ */
244
+ recommendedEvents?: string[];
245
+ missingRecommendedEvents?: string[];
239
246
  hint?: string;
240
247
  }
241
248
  export interface UsageResult {
package/dist/commands.js CHANGED
@@ -1755,6 +1755,16 @@ Examples:
1755
1755
  uploads github unlink --repo buildinternet/uploads
1756
1756
  uploads github doctor
1757
1757
  `;
1758
+ /** Older servers' health payload predates recommendedEvents/missingRecommendedEvents — treat as no recommendations rather than crashing. */
1759
+ function missingRecommendedEventsOf(result) {
1760
+ return Array.isArray(result.missingRecommendedEvents) ? result.missingRecommendedEvents : [];
1761
+ }
1762
+ function recommendedNoteLine(result) {
1763
+ const missing = missingRecommendedEventsOf(result);
1764
+ if (missing.length === 0)
1765
+ return "";
1766
+ return `note: not subscribed to ${missing.join(", ")} (recommended) — enables bot-comment self-healing; subscribe under the App's Permissions & events\n`;
1767
+ }
1758
1768
  function formatGithubDoctor(result) {
1759
1769
  if (!result.configured) {
1760
1770
  return `github app: not configured on this server${result.hint ? ` — ${result.hint}` : ""}\n`;
@@ -1763,10 +1773,12 @@ function formatGithubDoctor(result) {
1763
1773
  return `github app: configured, but health check failed${result.hint ? ` — ${result.hint}` : ""}\n`;
1764
1774
  }
1765
1775
  if (result.ok) {
1766
- return `github app: ok — subscribed to ${result.requiredEvents.join(", ")}\n`;
1776
+ return (`github app: ok — subscribed to ${result.requiredEvents.join(", ")}\n` +
1777
+ recommendedNoteLine(result));
1767
1778
  }
1768
1779
  return (`github app: missing webhook event subscription(s): ${result.missingEvents.join(", ")}\n` +
1769
- (result.hint ? ` ${result.hint}\n` : ""));
1780
+ (result.hint ? ` ${result.hint}\n` : "") +
1781
+ recommendedNoteLine(result));
1770
1782
  }
1771
1783
  function formatGithubLink(repo, result) {
1772
1784
  return result.workspace
package/dist/github.js CHANGED
@@ -239,5 +239,6 @@ export function attachmentsCommentBody(items, galleries = [], marker = ATTACHMEN
239
239
  lines.push("", "</details>", "");
240
240
  }
241
241
  lines.push('<sub>Maintained by <a href="https://uploads.sh">uploads.sh</a> — re-uploading a file with the same name updates it everywhere it is embedded.</sub>');
242
+ lines.push('<sub>Add media: <code>uploads put &lt;file&gt; --pr &lt;N&gt; --comment</code> (or <code>--issue &lt;N&gt;</code>) · <a href="https://uploads.sh/docs/github-app">docs</a></sub>');
242
243
  return lines.join("\n");
243
244
  }
package/dist/mcp/tools.js CHANGED
@@ -334,7 +334,7 @@ export function createUploadsMcpTools(opts) {
334
334
  noGit: { type: "boolean", description: "Don't derive the repo segment from git." },
335
335
  comment: {
336
336
  type: "boolean",
337
- description: "With pr/issue: create or update the managed attachments comment via local gh auth (best-effort).",
337
+ description: "With pr/issue: create or update the managed attachments comment. Posts as uploads-sh[bot] when the GitHub App is installed on the repo; otherwise via local gh auth (best-effort).",
338
338
  },
339
339
  dryRun: {
340
340
  type: "boolean",
@@ -1049,7 +1049,7 @@ export function createUploadsMcpTools(opts) {
1049
1049
  },
1050
1050
  {
1051
1051
  name: "comment",
1052
- description: "Create or update the managed attachments comment on a GitHub PR or issue, listing everything uploaded for it. Uses local gh auth; edits its own prior comment in place and never touches other comments.",
1052
+ description: "Create or update the managed attachments comment on a GitHub PR or issue, listing everything uploaded for it. Posts as uploads-sh[bot] when the GitHub App is installed on the repo; otherwise via local gh auth. Edits its own prior comment in place and never touches other comments.",
1053
1053
  inputSchema: {
1054
1054
  type: "object",
1055
1055
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buildinternet/uploads",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "CLI and client for uploads.sh — workspace-scoped image hosting for GitHub embeds",
5
5
  "type": "module",
6
6
  "sideEffects": false,