@buildinternet/uploads 0.4.0 → 0.6.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/README.md +49 -14
- package/dist/agent.js +1 -1
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.js +5 -0
- package/dist/cli.js +70 -20
- package/dist/client.d.ts +91 -0
- package/dist/client.js +93 -11
- package/dist/commands/admin-enrollment.d.ts +2 -0
- package/dist/commands/admin-enrollment.js +60 -8
- package/dist/commands/login.js +6 -0
- package/dist/commands/mcp.js +2 -5
- package/dist/commands.d.ts +3 -0
- package/dist/commands.js +299 -8
- package/dist/github.d.ts +23 -1
- package/dist/github.js +65 -2
- package/dist/index.d.ts +1 -1
- package/dist/mcp/tools.js +140 -2
- package/dist/package-version.d.ts +1 -0
- package/dist/package-version.js +19 -0
- package/dist/provenance.js +1 -15
- package/dist/update-check.d.ts +30 -0
- package/dist/update-check.js +134 -0
- package/package.json +1 -1
package/dist/github.js
CHANGED
|
@@ -10,6 +10,47 @@ export function parseRepoFromRemoteUrl(url) {
|
|
|
10
10
|
const repo = match?.[1];
|
|
11
11
|
return repo && isValidRepo(repo) ? repo : undefined;
|
|
12
12
|
}
|
|
13
|
+
/** Normalize a GitHub issue or pull-request coordinate for gallery linking. */
|
|
14
|
+
export function normalizeGithubCoordinate(value) {
|
|
15
|
+
const input = value.trim();
|
|
16
|
+
let match = /^([^/\s#]+)\/([^/\s#]+)#([1-9][0-9]*)$/.exec(input);
|
|
17
|
+
if (!match) {
|
|
18
|
+
try {
|
|
19
|
+
const url = new URL(input);
|
|
20
|
+
if (url.protocol !== "https:" ||
|
|
21
|
+
url.hostname.toLowerCase() !== "github.com" ||
|
|
22
|
+
url.port ||
|
|
23
|
+
url.username ||
|
|
24
|
+
url.password ||
|
|
25
|
+
url.search ||
|
|
26
|
+
url.hash)
|
|
27
|
+
return undefined;
|
|
28
|
+
match = /^\/([^/]+)\/([^/]+)\/(?:issues|pull)\/([1-9][0-9]*)\/?$/.exec(url.pathname);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (!match)
|
|
35
|
+
return undefined;
|
|
36
|
+
const [, ownerRaw, repositoryRaw, numberRaw] = match;
|
|
37
|
+
const repo = ownerRaw + "/" + repositoryRaw;
|
|
38
|
+
const number = Number(numberRaw);
|
|
39
|
+
if (!isValidRepo(repo) || !Number.isSafeInteger(number))
|
|
40
|
+
return undefined;
|
|
41
|
+
const owner = ownerRaw.toLowerCase();
|
|
42
|
+
const repository = repositoryRaw.toLowerCase();
|
|
43
|
+
const coordinate = owner + "/" + repository + "#" + number;
|
|
44
|
+
return {
|
|
45
|
+
coordinate,
|
|
46
|
+
canonicalUrl: "https://github.com/" +
|
|
47
|
+
encodeURIComponent(owner) +
|
|
48
|
+
"/" +
|
|
49
|
+
encodeURIComponent(repository) +
|
|
50
|
+
"/issues/" +
|
|
51
|
+
number,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
13
54
|
export function ghKeyPrefix(target) {
|
|
14
55
|
const [owner, name] = target.repo.split("/");
|
|
15
56
|
return `gh/${sanitizeKeySegment(owner)}/${sanitizeKeySegment(name)}/${target.kind}/${target.num}/`;
|
|
@@ -48,9 +89,31 @@ export function attachmentImageWidth(filename) {
|
|
|
48
89
|
function escapeHtmlAttr(s) {
|
|
49
90
|
return s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
50
91
|
}
|
|
51
|
-
|
|
92
|
+
function escapeHtmlText(s) {
|
|
93
|
+
return escapeHtmlAttr(s).replace(/'/g, "'").replace(/>/g, ">");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Render the one marker-owned GitHub comment. When there are no galleries this
|
|
97
|
+
* intentionally preserves the legacy attachment-only body byte-for-byte.
|
|
98
|
+
*/
|
|
99
|
+
export function attachmentsCommentBody(items, galleries = []) {
|
|
52
100
|
const sorted = items.toSorted((a, b) => a.key.localeCompare(b.key));
|
|
53
|
-
const
|
|
101
|
+
const sortedGalleries = galleries.toSorted((a, b) => a.title.localeCompare(b.title) || a.url.localeCompare(b.url));
|
|
102
|
+
const lines = [ATTACHMENTS_MARKER];
|
|
103
|
+
if (sortedGalleries.length > 0) {
|
|
104
|
+
lines.push("### 🖼️ Galleries", "");
|
|
105
|
+
for (const gallery of sortedGalleries) {
|
|
106
|
+
const href = escapeHtmlAttr(gallery.url);
|
|
107
|
+
lines.push(`#### <a href="${href}">${escapeHtmlText(gallery.title)}</a>`);
|
|
108
|
+
for (const preview of gallery.previews ?? []) {
|
|
109
|
+
lines.push(`<a href="${href}"><img width="320" alt="${escapeHtmlAttr(preview.alt)}" src="${escapeHtmlAttr(preview.url)}"></a>`);
|
|
110
|
+
}
|
|
111
|
+
lines.push(`<sub><a href="${href}">Open gallery</a></sub>`, "");
|
|
112
|
+
}
|
|
113
|
+
lines.push("");
|
|
114
|
+
}
|
|
115
|
+
if (sorted.length > 0 || sortedGalleries.length === 0)
|
|
116
|
+
lines.push("### 📎 Attachments", "");
|
|
54
117
|
for (const item of sorted) {
|
|
55
118
|
const name = item.key.slice(item.key.lastIndexOf("/") + 1);
|
|
56
119
|
if (item.url && inferContentType(name).startsWith("image/")) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { sanitizeKeySegment, sha256Short, deriveRepoFromGit, buildScreenshotKey
|
|
|
3
3
|
export { BUILTIN_DESTINATIONS, isBuiltinDestination, keyMatchesDestination, resolveDestinationRoot, resolvePutPrefix, type BuiltinDestinationId, } from "./destinations.js";
|
|
4
4
|
export { DEFAULT_API_URL, DEFAULT_WORKSPACE, UPLOADS_CONFIG_KEYS, defaultConfigPath, resolveConfigPath, loadConfigFile, loadEnvFile, resolveApiUrl, resolveConfig, describeConfigSources, redactToken, writeConfigKeys, configValuesFromClient, putDefaultsToConfigValues, resolvePutDefaults, mergePutDefaults, workspaceFromToken, workspaceMismatch, type UploadsClientConfig, type ResolvedConfig, type WorkspaceSource, type ConfigValueSource, type ConfigSources, type UploadsConfigKey, type UploadsConfigValues, type PutDefaults, } from "./config.js";
|
|
5
5
|
export { UploadsError, type UploadsErrorCode } from "./errors.js";
|
|
6
|
-
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
6
|
+
export { createUploadsClient, type UploadsClient, type PutOptions, type ProvenanceInput, type ListOptions, type PutResult, type ListItem, type ListResult, type HeadResult, type DeleteResult, type GalleryItem, type Gallery, type GallerySummary, type GalleryListOptions, type GalleryListResult, type CreateGalleryOptions, type AddGalleryItemOptions, type DeleteGalleryOptions, type HealthResult, type UsageResult, type ReconcileResult, type PurgeExpiredResult, type PurgeExpiredResponse, } from "./client.js";
|
|
7
7
|
export { buildCliProvenance } from "./provenance.js";
|
|
8
8
|
export { ATTACHMENTS_MARKER, attachmentsCommentBody, ghAttachmentKey, ghKeyPrefix, isValidRepo, parseRepoFromRemoteUrl, type AttachmentItem, type GhTarget, type GhTargetKind, } from "./github.js";
|
|
9
9
|
export { DEFAULT_OPTIMIZE_MAX_EDGE, DEFAULT_OPTIMIZE_QUALITY, optimizeImageForUpload, rewriteKeyExtension, withImageExtension, type OptimizeImageOptions, type OptimizeImageResult, type OptimizeOutputFormat, } from "./optimize.js";
|
package/dist/mcp/tools.js
CHANGED
|
@@ -88,6 +88,23 @@ const frameProps = {
|
|
|
88
88
|
function ghTargetFromArgs(args, run) {
|
|
89
89
|
return makeGhTarget(optPosInt(args, "pr"), optPosInt(args, "issue"), optString(args, "repo"), run);
|
|
90
90
|
}
|
|
91
|
+
function galleryId(args) {
|
|
92
|
+
const id = optString(args, "galleryId");
|
|
93
|
+
if (!id)
|
|
94
|
+
usage("galleryId is required");
|
|
95
|
+
return id;
|
|
96
|
+
}
|
|
97
|
+
function galleryReference(args) {
|
|
98
|
+
const provider = optString(args, "provider");
|
|
99
|
+
const coordinate = optString(args, "coordinate");
|
|
100
|
+
if (!provider)
|
|
101
|
+
usage("provider is required");
|
|
102
|
+
if (!coordinate)
|
|
103
|
+
usage("coordinate is required");
|
|
104
|
+
if (provider !== "github")
|
|
105
|
+
usage("provider must be github");
|
|
106
|
+
return { provider, coordinate };
|
|
107
|
+
}
|
|
91
108
|
const workspaceProp = {
|
|
92
109
|
type: "string",
|
|
93
110
|
description: "Override the workspace for this call (like the CLI's --workspace flag).",
|
|
@@ -136,9 +153,130 @@ export function createUploadsMcpTools(opts) {
|
|
|
136
153
|
return { comment, commentError };
|
|
137
154
|
};
|
|
138
155
|
return [
|
|
156
|
+
{
|
|
157
|
+
name: "gallery_create",
|
|
158
|
+
description: "Create a public ordered media gallery in the workspace. The returned canonical URL is safe to give users, but anyone who knows it can view the gallery and its media.",
|
|
159
|
+
inputSchema: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
title: { type: "string", description: "Gallery title (1–120 characters)." },
|
|
163
|
+
description: { type: "string", description: "Optional public gallery description." },
|
|
164
|
+
workspace: workspaceProp,
|
|
165
|
+
},
|
|
166
|
+
required: ["title"],
|
|
167
|
+
additionalProperties: false,
|
|
168
|
+
},
|
|
169
|
+
async handler(args) {
|
|
170
|
+
const title = optString(args, "title");
|
|
171
|
+
if (!title)
|
|
172
|
+
usage("title is required");
|
|
173
|
+
const { client } = clientFor(args);
|
|
174
|
+
return client.createGallery({ title, description: optString(args, "description") });
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "gallery_get",
|
|
179
|
+
description: "Get a workspace-owned gallery, including ordered media and its canonical public URL. Gallery media is public to anyone with the URL.",
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: "object",
|
|
182
|
+
properties: {
|
|
183
|
+
galleryId: { type: "string", description: "Opaque gallery ID." },
|
|
184
|
+
workspace: workspaceProp,
|
|
185
|
+
},
|
|
186
|
+
required: ["galleryId"],
|
|
187
|
+
additionalProperties: false,
|
|
188
|
+
},
|
|
189
|
+
async handler(args) {
|
|
190
|
+
const { client } = clientFor(args);
|
|
191
|
+
return client.getGallery(galleryId(args));
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "gallery_add",
|
|
196
|
+
description: "Add one existing, publicly served workspace object to a gallery. Reads the latest gallery version before writing, so the optimistic API version is handled safely. Does not upload or delete the object.",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
properties: {
|
|
200
|
+
galleryId: { type: "string", description: "Opaque gallery ID." },
|
|
201
|
+
objectKey: { type: "string", description: "Existing public object key to add." },
|
|
202
|
+
caption: { type: "string", description: "Optional public caption." },
|
|
203
|
+
altText: { type: "string", description: "Optional public alt text." },
|
|
204
|
+
workspace: workspaceProp,
|
|
205
|
+
},
|
|
206
|
+
required: ["galleryId", "objectKey"],
|
|
207
|
+
additionalProperties: false,
|
|
208
|
+
},
|
|
209
|
+
async handler(args) {
|
|
210
|
+
const objectKey = optString(args, "objectKey");
|
|
211
|
+
if (!objectKey)
|
|
212
|
+
usage("objectKey is required");
|
|
213
|
+
const { client } = clientFor(args);
|
|
214
|
+
const id = galleryId(args);
|
|
215
|
+
const current = await client.getGallery(id);
|
|
216
|
+
return client.addGalleryItem(id, objectKey, {
|
|
217
|
+
expectedVersion: current.version,
|
|
218
|
+
caption: optString(args, "caption"),
|
|
219
|
+
altText: optString(args, "altText"),
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "gallery_link",
|
|
225
|
+
description: "Link a gallery to an external reference. References use provider-neutral fields; github currently accepts owner/repo#number or a strict GitHub issue/PR URL. No GitHub credentials or API calls are used.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
galleryId: { type: "string", description: "Opaque gallery ID." },
|
|
230
|
+
provider: { type: "string", description: "External provider (currently github)." },
|
|
231
|
+
coordinate: {
|
|
232
|
+
type: "string",
|
|
233
|
+
description: "Provider-native external reference coordinate.",
|
|
234
|
+
},
|
|
235
|
+
workspace: workspaceProp,
|
|
236
|
+
},
|
|
237
|
+
required: ["galleryId", "provider", "coordinate"],
|
|
238
|
+
additionalProperties: false,
|
|
239
|
+
},
|
|
240
|
+
async handler(args) {
|
|
241
|
+
const { client } = clientFor(args);
|
|
242
|
+
const id = galleryId(args);
|
|
243
|
+
const current = await client.getGallery(id);
|
|
244
|
+
return client.linkGalleryExternalReference(id, {
|
|
245
|
+
expectedVersion: current.version,
|
|
246
|
+
...galleryReference(args),
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
name: "gallery_find_by_reference",
|
|
252
|
+
description: "Find workspace galleries linked to an external reference. Returns gallery summaries and canonical public URLs without contacting the provider.",
|
|
253
|
+
inputSchema: {
|
|
254
|
+
type: "object",
|
|
255
|
+
properties: {
|
|
256
|
+
provider: { type: "string", description: "External provider (currently github)." },
|
|
257
|
+
coordinate: {
|
|
258
|
+
type: "string",
|
|
259
|
+
description: "Provider-native external reference coordinate.",
|
|
260
|
+
},
|
|
261
|
+
limit: { type: "number", description: "Page size (default 50, max 100)." },
|
|
262
|
+
cursor: { type: "string", description: "Pagination cursor from a previous response." },
|
|
263
|
+
workspace: workspaceProp,
|
|
264
|
+
},
|
|
265
|
+
required: ["provider", "coordinate"],
|
|
266
|
+
additionalProperties: false,
|
|
267
|
+
},
|
|
268
|
+
async handler(args) {
|
|
269
|
+
const { client } = clientFor(args);
|
|
270
|
+
return client.findGalleriesByReference({
|
|
271
|
+
...galleryReference(args),
|
|
272
|
+
limit: optPosInt(args, "limit"),
|
|
273
|
+
cursor: optString(args, "cursor"),
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
},
|
|
139
277
|
{
|
|
140
278
|
name: "put",
|
|
141
|
-
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment.",
|
|
279
|
+
description: "Upload a file to uploads.sh and get a public URL plus GitHub-ready embed markdown (the returned `markdown` is ready to paste into a PR or issue). Pass `file` (a local path) or `contentBase64` + `filename` for in-memory content; with `pr`/`issue` the key is stable (same filename → same URL) and `comment` syncs the managed attachments comment. All uploads are public; pr/issue keys are predictable and remain public for private/internal GitHub repositories, so upload only non-sensitive media.",
|
|
142
280
|
inputSchema: {
|
|
143
281
|
type: "object",
|
|
144
282
|
properties: {
|
|
@@ -302,7 +440,7 @@ export function createUploadsMcpTools(opts) {
|
|
|
302
440
|
},
|
|
303
441
|
{
|
|
304
442
|
name: "attach",
|
|
305
|
-
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them (each upload's `markdown` is ready to paste into GitHub). With no pr/issue, targets the pull request for the current branch.",
|
|
443
|
+
description: "Upload one or more files as stable PR/issue attachments and maintain a single managed GitHub comment listing them (each upload's `markdown` is ready to paste into GitHub). With no pr/issue, targets the pull request for the current branch. Attachments are public and their repo/number/filename keys are predictable even for private/internal GitHub repositories; upload only non-sensitive media.",
|
|
306
444
|
inputSchema: {
|
|
307
445
|
type: "object",
|
|
308
446
|
properties: {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function packageVersion(): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the published package version for CLI headers, --version, and
|
|
3
|
+
* update checks. Reads package.json once per process.
|
|
4
|
+
*/
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
let cachedVersion;
|
|
7
|
+
export function packageVersion() {
|
|
8
|
+
if (cachedVersion)
|
|
9
|
+
return cachedVersion;
|
|
10
|
+
try {
|
|
11
|
+
const require = createRequire(import.meta.url);
|
|
12
|
+
const pkg = require("../package.json");
|
|
13
|
+
cachedVersion = pkg.version ?? "0.0.0";
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
cachedVersion = "0.0.0";
|
|
17
|
+
}
|
|
18
|
+
return cachedVersion;
|
|
19
|
+
}
|
package/dist/provenance.js
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
let cachedVersion;
|
|
3
|
-
function packageVersion() {
|
|
4
|
-
if (cachedVersion)
|
|
5
|
-
return cachedVersion;
|
|
6
|
-
try {
|
|
7
|
-
const require = createRequire(import.meta.url);
|
|
8
|
-
const pkg = require("../package.json");
|
|
9
|
-
cachedVersion = pkg.version ?? "0.0.0";
|
|
10
|
-
}
|
|
11
|
-
catch {
|
|
12
|
-
cachedVersion = "0.0.0";
|
|
13
|
-
}
|
|
14
|
-
return cachedVersion;
|
|
15
|
-
}
|
|
1
|
+
import { packageVersion } from "./package-version.js";
|
|
16
2
|
export function buildCliProvenance(opts) {
|
|
17
3
|
const provenance = {
|
|
18
4
|
client: opts.client ?? "uploads-cli",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare const PACKAGE_NAME = "@buildinternet/uploads";
|
|
2
|
+
export interface UpdateCache {
|
|
3
|
+
checkedAt: number;
|
|
4
|
+
latest: string;
|
|
5
|
+
current: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UpdateCheckOptions {
|
|
8
|
+
quiet?: boolean;
|
|
9
|
+
/** mcp is always skipped (stdio purity). */
|
|
10
|
+
command?: string;
|
|
11
|
+
currentVersion?: string;
|
|
12
|
+
cachePath?: string;
|
|
13
|
+
now?: number;
|
|
14
|
+
/** Default 24h. Pass 0 in tests to force a network check. */
|
|
15
|
+
ttlMs?: number;
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
fetchImpl?: typeof fetch;
|
|
18
|
+
write?: (text: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/** Parse major.minor.patch (ignores pre-release). */
|
|
21
|
+
export declare function parseSemver(version: string): [number, number, number] | null;
|
|
22
|
+
/** True when `latest` is strictly greater than `current`. */
|
|
23
|
+
export declare function isNewerVersion(latest: string, current: string): boolean;
|
|
24
|
+
export declare function readUpdateCache(path: string): UpdateCache | undefined;
|
|
25
|
+
export declare function writeUpdateCache(path: string, cache: UpdateCache): void;
|
|
26
|
+
/**
|
|
27
|
+
* If a newer published version is known (or can be fetched within the timeout),
|
|
28
|
+
* write a one-line stderr hint. Always resolves; never throws.
|
|
29
|
+
*/
|
|
30
|
+
export declare function maybeHintUpdate(opts?: UpdateCheckOptions): Promise<void>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional npm update notifier for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Checks the registry at most once per day, never throws, never blocks longer
|
|
5
|
+
* than a short timeout, and writes only to stderr. Silence with --quiet,
|
|
6
|
+
* UPLOADS_NO_UPDATE=1, or NO_UPDATE_NOTIFIER=1.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { dirname, join } from "node:path";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { packageVersion } from "./package-version.js";
|
|
12
|
+
export const PACKAGE_NAME = "@buildinternet/uploads";
|
|
13
|
+
const REGISTRY_LATEST = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
|
|
14
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
15
|
+
const DEFAULT_TIMEOUT_MS = 1500;
|
|
16
|
+
function truthyEnv(name) {
|
|
17
|
+
const v = process.env[name];
|
|
18
|
+
if (!v)
|
|
19
|
+
return false;
|
|
20
|
+
const lower = v.toLowerCase();
|
|
21
|
+
return lower !== "0" && lower !== "false" && lower !== "no";
|
|
22
|
+
}
|
|
23
|
+
function defaultCachePath() {
|
|
24
|
+
const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
|
|
25
|
+
return join(base, "uploads", "version-check.json");
|
|
26
|
+
}
|
|
27
|
+
/** Parse major.minor.patch (ignores pre-release). */
|
|
28
|
+
export function parseSemver(version) {
|
|
29
|
+
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim());
|
|
30
|
+
if (!m)
|
|
31
|
+
return null;
|
|
32
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
33
|
+
}
|
|
34
|
+
/** True when `latest` is strictly greater than `current`. */
|
|
35
|
+
export function isNewerVersion(latest, current) {
|
|
36
|
+
const a = parseSemver(latest);
|
|
37
|
+
const b = parseSemver(current);
|
|
38
|
+
if (!a || !b)
|
|
39
|
+
return false;
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if (a[i] > b[i])
|
|
42
|
+
return true;
|
|
43
|
+
if (a[i] < b[i])
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
export function readUpdateCache(path) {
|
|
49
|
+
try {
|
|
50
|
+
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
if (typeof raw.checkedAt !== "number" ||
|
|
52
|
+
typeof raw.latest !== "string" ||
|
|
53
|
+
typeof raw.current !== "string") {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return { checkedAt: raw.checkedAt, latest: raw.latest, current: raw.current };
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function writeUpdateCache(path, cache) {
|
|
63
|
+
try {
|
|
64
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
65
|
+
writeFileSync(path, JSON.stringify(cache) + "\n", { mode: 0o600 });
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best-effort — never fail the CLI for a cache write error.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* If a newer published version is known (or can be fetched within the timeout),
|
|
73
|
+
* write a one-line stderr hint. Always resolves; never throws.
|
|
74
|
+
*/
|
|
75
|
+
export async function maybeHintUpdate(opts = {}) {
|
|
76
|
+
try {
|
|
77
|
+
if (opts.quiet || opts.command === "mcp")
|
|
78
|
+
return;
|
|
79
|
+
if (truthyEnv("UPLOADS_NO_UPDATE") || truthyEnv("NO_UPDATE_NOTIFIER"))
|
|
80
|
+
return;
|
|
81
|
+
const current = opts.currentVersion ?? packageVersion();
|
|
82
|
+
const cachePath = opts.cachePath ?? defaultCachePath();
|
|
83
|
+
const now = opts.now ?? Date.now();
|
|
84
|
+
const ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
|
|
85
|
+
const write = opts.write ?? ((text) => process.stderr.write(text));
|
|
86
|
+
const cached = readUpdateCache(cachePath);
|
|
87
|
+
let latest;
|
|
88
|
+
if (cached && now - cached.checkedAt < ttlMs && cached.current === current) {
|
|
89
|
+
latest = cached.latest;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const fetched = await fetchLatestVersion(opts.fetchImpl, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
93
|
+
if (fetched) {
|
|
94
|
+
latest = fetched;
|
|
95
|
+
writeUpdateCache(cachePath, { checkedAt: now, latest: fetched, current });
|
|
96
|
+
}
|
|
97
|
+
else if (cached?.current === current) {
|
|
98
|
+
latest = cached.latest; // stale cache if network failed
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (latest && isNewerVersion(latest, current)) {
|
|
102
|
+
write(`hint: ${PACKAGE_NAME}@${latest} is available (you have ${current}). Update: npm i -g ${PACKAGE_NAME}\n`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Never surface update-check failures.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function fetchLatestVersion(fetchImpl, timeoutMs) {
|
|
110
|
+
const fetchFn = fetchImpl ?? globalThis.fetch;
|
|
111
|
+
if (typeof fetchFn !== "function")
|
|
112
|
+
return undefined;
|
|
113
|
+
const controller = new AbortController();
|
|
114
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
115
|
+
try {
|
|
116
|
+
const res = await fetchFn(REGISTRY_LATEST, {
|
|
117
|
+
signal: controller.signal,
|
|
118
|
+
headers: {
|
|
119
|
+
accept: "application/json",
|
|
120
|
+
"user-agent": `${PACKAGE_NAME}/${packageVersion()} (update-check)`,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok)
|
|
124
|
+
return undefined;
|
|
125
|
+
const body = (await res.json());
|
|
126
|
+
return typeof body.version === "string" ? body.version : undefined;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
}
|