@shard-for-obsidian/cli 0.2.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/index.js +1998 -0
- package/dist/index.js.map +7 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1998 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
|
|
6
|
+
// ../shard-lib/dist/parsing/IndexParser.js
|
|
7
|
+
var DEFAULT_INDEX_NAME = "docker.io";
|
|
8
|
+
var DEFAULT_INDEX_URL = "https://registry-1.docker.io";
|
|
9
|
+
var DEFAULT_LOGIN_SERVERNAME = "https://index.docker.io/v1/";
|
|
10
|
+
function parseIndex(arg) {
|
|
11
|
+
if (!arg || arg === DEFAULT_LOGIN_SERVERNAME) {
|
|
12
|
+
return {
|
|
13
|
+
scheme: "https",
|
|
14
|
+
name: DEFAULT_INDEX_NAME,
|
|
15
|
+
official: true
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
let indexName;
|
|
19
|
+
let scheme = "https";
|
|
20
|
+
const protoSepIdx = arg.indexOf("://");
|
|
21
|
+
if (protoSepIdx !== -1) {
|
|
22
|
+
const foundScheme = arg.slice(0, protoSepIdx);
|
|
23
|
+
if (foundScheme !== "http" && foundScheme !== "https") {
|
|
24
|
+
throw new Error('invalid index scheme, must be "http" or "https": ' + arg);
|
|
25
|
+
}
|
|
26
|
+
scheme = foundScheme;
|
|
27
|
+
indexName = arg.slice(protoSepIdx + 3);
|
|
28
|
+
} else {
|
|
29
|
+
scheme = isLocalhost(arg) ? "http" : "https";
|
|
30
|
+
indexName = arg;
|
|
31
|
+
}
|
|
32
|
+
if (!indexName) {
|
|
33
|
+
throw new Error("invalid index, empty host: " + arg);
|
|
34
|
+
} else if (indexName.indexOf(".") === -1 && indexName.indexOf(":") === -1 && indexName !== "localhost") {
|
|
35
|
+
throw new Error(`invalid index, "${indexName}" does not look like a valid host: ${arg}`);
|
|
36
|
+
} else {
|
|
37
|
+
if (indexName[indexName.length - 1] === "/") {
|
|
38
|
+
indexName = indexName.slice(0, indexName.length - 1);
|
|
39
|
+
}
|
|
40
|
+
if (indexName.indexOf("/") !== -1) {
|
|
41
|
+
throw new Error("invalid index, trailing repo: " + arg);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (indexName === "index." + DEFAULT_INDEX_NAME) {
|
|
45
|
+
indexName = DEFAULT_INDEX_NAME;
|
|
46
|
+
}
|
|
47
|
+
const index = {
|
|
48
|
+
name: indexName,
|
|
49
|
+
official: indexName === DEFAULT_INDEX_NAME,
|
|
50
|
+
scheme
|
|
51
|
+
};
|
|
52
|
+
if (index.official && index.scheme === "http") {
|
|
53
|
+
throw new Error("invalid index, plaintext HTTP to official index is disallowed: " + arg);
|
|
54
|
+
}
|
|
55
|
+
return index;
|
|
56
|
+
}
|
|
57
|
+
function urlFromIndex(index, scheme) {
|
|
58
|
+
if (index.official) {
|
|
59
|
+
if (scheme != null && scheme !== "https")
|
|
60
|
+
throw new Error(`Unencrypted communication with docker.io is not allowed`);
|
|
61
|
+
return DEFAULT_INDEX_URL;
|
|
62
|
+
} else {
|
|
63
|
+
if (scheme != null && scheme !== "https" && scheme !== "http")
|
|
64
|
+
throw new Error(`Non-HTTP communication with docker registries is not allowed`);
|
|
65
|
+
return `${scheme ?? index.scheme}://${index.name}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function isLocalhost(host) {
|
|
69
|
+
const lead = host.split(":")[0];
|
|
70
|
+
if (lead === "localhost" || lead === "127.0.0.1" || host.includes("::1")) {
|
|
71
|
+
return true;
|
|
72
|
+
} else {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ../shard-lib/dist/utils/ValidationUtils.js
|
|
78
|
+
function splitIntoTwo(str, sep) {
|
|
79
|
+
const slashIdx = str.indexOf(sep);
|
|
80
|
+
return slashIdx == -1 ? [str] : [str.slice(0, slashIdx), str.slice(slashIdx + 1)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ../shard-lib/dist/parsing/RepoParser.js
|
|
84
|
+
var DEFAULT_TAG = "latest";
|
|
85
|
+
var VALID_NS = /^[a-z0-9._-]*$/;
|
|
86
|
+
var VALID_REPO = /^[a-z0-9_/.-]*$/;
|
|
87
|
+
function parseRepo(arg, defaultIndex) {
|
|
88
|
+
let index;
|
|
89
|
+
let remoteNameRaw;
|
|
90
|
+
const protoSepIdx = arg.indexOf("://");
|
|
91
|
+
if (protoSepIdx !== -1) {
|
|
92
|
+
const slashIdx = arg.indexOf("/", protoSepIdx + 3);
|
|
93
|
+
if (slashIdx === -1) {
|
|
94
|
+
throw new Error('invalid repository name, no "/REPO" after hostame: ' + arg);
|
|
95
|
+
}
|
|
96
|
+
const indexName = arg.slice(0, slashIdx);
|
|
97
|
+
remoteNameRaw = arg.slice(slashIdx + 1);
|
|
98
|
+
index = parseIndex(indexName);
|
|
99
|
+
} else {
|
|
100
|
+
const parts = splitIntoTwo(arg, "/");
|
|
101
|
+
if (parts.length === 1 || /* or if parts[0] doesn't look like a hostname or IP */
|
|
102
|
+
parts[0].indexOf(".") === -1 && parts[0].indexOf(":") === -1 && parts[0] !== "localhost") {
|
|
103
|
+
if (defaultIndex === void 0) {
|
|
104
|
+
index = parseIndex();
|
|
105
|
+
} else if (typeof defaultIndex === "string") {
|
|
106
|
+
index = parseIndex(defaultIndex);
|
|
107
|
+
} else {
|
|
108
|
+
index = defaultIndex;
|
|
109
|
+
}
|
|
110
|
+
remoteNameRaw = arg;
|
|
111
|
+
} else {
|
|
112
|
+
index = parseIndex(parts[0]);
|
|
113
|
+
remoteNameRaw = parts[1];
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const nameParts = splitIntoTwo(remoteNameRaw, "/");
|
|
117
|
+
let ns = "", name;
|
|
118
|
+
if (nameParts.length === 2) {
|
|
119
|
+
name = nameParts[1];
|
|
120
|
+
ns = nameParts[0];
|
|
121
|
+
if (ns.length < 2 || ns.length > 255) {
|
|
122
|
+
throw new Error("invalid repository namespace, must be between 2 and 255 characters: " + ns);
|
|
123
|
+
}
|
|
124
|
+
if (!VALID_NS.test(ns)) {
|
|
125
|
+
throw new Error("invalid repository namespace, may only contain [a-z0-9._-] characters: " + ns);
|
|
126
|
+
}
|
|
127
|
+
if (ns[0] === "-" && ns[ns.length - 1] === "-") {
|
|
128
|
+
throw new Error("invalid repository namespace, cannot start or end with a hypen: " + ns);
|
|
129
|
+
}
|
|
130
|
+
if (ns.indexOf("--") !== -1) {
|
|
131
|
+
throw new Error("invalid repository namespace, cannot contain consecutive hyphens: " + ns);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
name = remoteNameRaw;
|
|
135
|
+
if (index.official) {
|
|
136
|
+
ns = "library";
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (!VALID_REPO.test(name)) {
|
|
140
|
+
throw new Error("invalid repository name, may only contain [a-z0-9_/.-] characters: " + name);
|
|
141
|
+
}
|
|
142
|
+
const isLibrary = index.official && ns === "library";
|
|
143
|
+
const remoteName = ns ? `${ns}/${name}` : name;
|
|
144
|
+
const localName = index.official ? isLibrary ? name : remoteName : `${index.name}/${remoteName}`;
|
|
145
|
+
const canonicalName = index.official ? `${parseIndex().name}/${localName}` : localName;
|
|
146
|
+
return {
|
|
147
|
+
index,
|
|
148
|
+
official: isLibrary,
|
|
149
|
+
remoteName,
|
|
150
|
+
localName,
|
|
151
|
+
canonicalName
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function parseRepoAndRef(arg, defaultIndex) {
|
|
155
|
+
let digest = null;
|
|
156
|
+
let tag = null;
|
|
157
|
+
const atIdx = arg.lastIndexOf("@");
|
|
158
|
+
if (atIdx !== -1) {
|
|
159
|
+
digest = arg.slice(atIdx + 1);
|
|
160
|
+
arg = arg.slice(0, atIdx);
|
|
161
|
+
} else {
|
|
162
|
+
tag = DEFAULT_TAG;
|
|
163
|
+
}
|
|
164
|
+
const colonIdx = arg.lastIndexOf(":");
|
|
165
|
+
const slashIdx = arg.lastIndexOf("/");
|
|
166
|
+
if (colonIdx !== -1 && colonIdx > slashIdx) {
|
|
167
|
+
tag = arg.slice(colonIdx + 1);
|
|
168
|
+
arg = arg.slice(0, colonIdx);
|
|
169
|
+
}
|
|
170
|
+
const repo = parseRepo(arg, defaultIndex);
|
|
171
|
+
return {
|
|
172
|
+
...repo,
|
|
173
|
+
digest,
|
|
174
|
+
tag,
|
|
175
|
+
canonicalRef: [
|
|
176
|
+
repo.canonicalName,
|
|
177
|
+
tag ? `:${tag}` : "",
|
|
178
|
+
digest ? `@${digest}` : ""
|
|
179
|
+
].join("")
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ../shard-lib/dist/utils/DigestUtils.js
|
|
184
|
+
function encodeHex(data) {
|
|
185
|
+
return [...new Uint8Array(data)].map((x) => x.toString(16).padStart(2, "0")).join("");
|
|
186
|
+
}
|
|
187
|
+
async function digestFromManifestStr(manifestStr) {
|
|
188
|
+
let manifest;
|
|
189
|
+
try {
|
|
190
|
+
manifest = JSON.parse(manifestStr);
|
|
191
|
+
} catch (thrown) {
|
|
192
|
+
const err = thrown;
|
|
193
|
+
throw new Error(`could not parse manifest: ${err.message}
|
|
194
|
+
${manifestStr}`);
|
|
195
|
+
}
|
|
196
|
+
if (manifest.schemaVersion === 1) {
|
|
197
|
+
throw new Error(`schemaVersion 1 is not supported by /x/docker_registry_client.`);
|
|
198
|
+
}
|
|
199
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(manifestStr));
|
|
200
|
+
return `sha256:${encodeHex(hash)}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ../shard-lib/dist/types/ManifestTypes.js
|
|
204
|
+
var MEDIATYPE_MANIFEST_V2 = "application/vnd.docker.distribution.manifest.v2+json";
|
|
205
|
+
var MEDIATYPE_MANIFEST_LIST_V2 = "application/vnd.docker.distribution.manifest.list.v2+json";
|
|
206
|
+
var MEDIATYPE_OCI_MANIFEST_V1 = "application/vnd.oci.image.manifest.v1+json";
|
|
207
|
+
var MEDIATYPE_OCI_MANIFEST_INDEX_V1 = "application/vnd.oci.image.index.v1+json";
|
|
208
|
+
var MEDIATYPE_OBSIDIAN_PLUGIN_CONFIG_V1 = "application/vnd.obsidianmd.plugin-manifest.v1+json";
|
|
209
|
+
|
|
210
|
+
// ../shard-lib/dist/ghcr/GhcrConstants.js
|
|
211
|
+
var REALM = "https://ghcr.io/token";
|
|
212
|
+
var SERVICE = "ghcr.io";
|
|
213
|
+
|
|
214
|
+
// ../shard-lib/dist/errors/RegistryErrors.js
|
|
215
|
+
var ApiError = class extends Error {
|
|
216
|
+
constructor(message) {
|
|
217
|
+
super(message);
|
|
218
|
+
this.name = new.target.name;
|
|
219
|
+
Error.captureStackTrace?.(this, new.target);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
var BadDigestError = class extends ApiError {
|
|
223
|
+
name = "BadDigestError";
|
|
224
|
+
};
|
|
225
|
+
var BlobReadError = class extends ApiError {
|
|
226
|
+
name = "BlobReadError";
|
|
227
|
+
};
|
|
228
|
+
var TooManyRedirectsError = class extends ApiError {
|
|
229
|
+
name = "TooManyRedirectsError";
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// ../shard-lib/dist/parsing/LinkHeaderParser.js
|
|
233
|
+
var linkRegex = /^<([^>]+)>(?:\s*;\s*(.+))?$/;
|
|
234
|
+
function parseLinkHeader(rawHeader) {
|
|
235
|
+
if (!rawHeader)
|
|
236
|
+
return [];
|
|
237
|
+
return rawHeader.split(",").slice(0, 5).flatMap((piece) => {
|
|
238
|
+
const matches = piece.trim().match(linkRegex);
|
|
239
|
+
if (!matches)
|
|
240
|
+
return [];
|
|
241
|
+
const { rel, ...params } = matches[2]?.split(";").map((param) => param.trim().split("=")).reduce((acc, [key, value]) => {
|
|
242
|
+
if (!value)
|
|
243
|
+
return acc;
|
|
244
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
245
|
+
value = value.slice(1, -1);
|
|
246
|
+
}
|
|
247
|
+
acc[key] = value;
|
|
248
|
+
return acc;
|
|
249
|
+
}, {}) ?? {};
|
|
250
|
+
if (!rel)
|
|
251
|
+
return [];
|
|
252
|
+
return [
|
|
253
|
+
{
|
|
254
|
+
rel,
|
|
255
|
+
url: matches[1],
|
|
256
|
+
params
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ../shard-lib/dist/client/OciRegistryClient.js
|
|
263
|
+
var DEFAULT_USERAGENT = `open-obsidian-plugin-spec/0.1.0`;
|
|
264
|
+
var getCrypto = () => {
|
|
265
|
+
if (!globalThis.crypto) {
|
|
266
|
+
throw new Error("crypto API not available. This library requires Node.js 18+ or a modern browser environment.");
|
|
267
|
+
}
|
|
268
|
+
return globalThis.crypto;
|
|
269
|
+
};
|
|
270
|
+
function _setAuthHeaderFromAuthInfo(headers, authInfo) {
|
|
271
|
+
if (authInfo?.type === "Bearer") {
|
|
272
|
+
headers["authorization"] = "Bearer " + authInfo.token;
|
|
273
|
+
} else if (authInfo?.type === "Basic") {
|
|
274
|
+
const credentials = `${authInfo.username ?? ""}:${authInfo.password ?? ""}`;
|
|
275
|
+
headers["authorization"] = "Basic " + btoa(credentials);
|
|
276
|
+
} else {
|
|
277
|
+
delete headers["authorization"];
|
|
278
|
+
}
|
|
279
|
+
return headers;
|
|
280
|
+
}
|
|
281
|
+
function _getRegistryErrorMessage(err) {
|
|
282
|
+
const e = err;
|
|
283
|
+
if (e.body && typeof e.body === "object" && e.body !== null) {
|
|
284
|
+
const body = e.body;
|
|
285
|
+
if (Array.isArray(body.errors) && body.errors[0]) {
|
|
286
|
+
return body.errors[0].message;
|
|
287
|
+
} else if (body.details) {
|
|
288
|
+
return body.details;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (Array.isArray(e.errors) && e.errors[0]) {
|
|
292
|
+
return e.errors[0].message;
|
|
293
|
+
} else if (e.message) {
|
|
294
|
+
return e.message;
|
|
295
|
+
} else if (e.details) {
|
|
296
|
+
return e.details;
|
|
297
|
+
}
|
|
298
|
+
return String(err);
|
|
299
|
+
}
|
|
300
|
+
function _makeAuthScope(resource, name, actions) {
|
|
301
|
+
return `${resource}:${name}:${actions.join(",")}`;
|
|
302
|
+
}
|
|
303
|
+
function _parseDockerContentDigest(dcd) {
|
|
304
|
+
if (!dcd)
|
|
305
|
+
throw new BadDigestError('missing "Docker-Content-Digest" header');
|
|
306
|
+
const errPre = `could not parse Docker-Content-Digest header "${dcd}": `;
|
|
307
|
+
const parts = splitIntoTwo(dcd, ":");
|
|
308
|
+
if (parts.length !== 2)
|
|
309
|
+
throw new BadDigestError(errPre + JSON.stringify(dcd));
|
|
310
|
+
if (parts[0] !== "sha256")
|
|
311
|
+
throw new BadDigestError(errPre + "Unsupported hash algorithm " + JSON.stringify(parts[0]));
|
|
312
|
+
return {
|
|
313
|
+
raw: dcd,
|
|
314
|
+
algorithm: parts[0],
|
|
315
|
+
expectedDigest: parts[1],
|
|
316
|
+
async validate(buffer) {
|
|
317
|
+
switch (this.algorithm) {
|
|
318
|
+
case "sha256": {
|
|
319
|
+
const hashBuffer = await getCrypto().subtle.digest("SHA-256", buffer);
|
|
320
|
+
const digest = encodeHex(hashBuffer);
|
|
321
|
+
if (this.expectedDigest !== digest) {
|
|
322
|
+
throw new BadDigestError(`Docker-Content-Digest mismatch (expected: ${this.expectedDigest}, got: ${digest})`);
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
throw new BadDigestError(`Unsupported hash algorithm ${this.algorithm}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
var OciRegistryClient = class {
|
|
333
|
+
version = 2;
|
|
334
|
+
insecure;
|
|
335
|
+
repo;
|
|
336
|
+
acceptOCIManifests;
|
|
337
|
+
acceptManifestLists;
|
|
338
|
+
username;
|
|
339
|
+
password;
|
|
340
|
+
scopes;
|
|
341
|
+
_loggedIn;
|
|
342
|
+
_loggedInScope;
|
|
343
|
+
_authInfo;
|
|
344
|
+
_headers;
|
|
345
|
+
_url;
|
|
346
|
+
_userAgent;
|
|
347
|
+
_adapter;
|
|
348
|
+
/**
|
|
349
|
+
* Create a new GHCR client for a particular repository.
|
|
350
|
+
*
|
|
351
|
+
* @param opts.insecure {Boolean} Optional. Default false. Set to true
|
|
352
|
+
* to *not* fail on an invalid or this-signed server certificate.
|
|
353
|
+
* @param opts.adapter {FetchAdapter} Required. HTTP adapter for making requests.
|
|
354
|
+
* ... TODO: lots more to document
|
|
355
|
+
*
|
|
356
|
+
*/
|
|
357
|
+
constructor(opts) {
|
|
358
|
+
this.insecure = Boolean(opts.insecure);
|
|
359
|
+
if (opts.repo) {
|
|
360
|
+
this.repo = opts.repo;
|
|
361
|
+
} else if (opts.name) {
|
|
362
|
+
this.repo = parseRepo(opts.name);
|
|
363
|
+
} else
|
|
364
|
+
throw new Error(`name or repo required`);
|
|
365
|
+
this.acceptOCIManifests = opts.acceptOCIManifests ?? true;
|
|
366
|
+
this.acceptManifestLists = opts.acceptManifestLists ?? false;
|
|
367
|
+
this.username = opts.username;
|
|
368
|
+
this.password = opts.password;
|
|
369
|
+
this.scopes = opts.scopes ?? ["pull"];
|
|
370
|
+
this._loggedIn = false;
|
|
371
|
+
this._loggedInScope = null;
|
|
372
|
+
this._authInfo = null;
|
|
373
|
+
this._headers = {};
|
|
374
|
+
if (opts.token) {
|
|
375
|
+
_setAuthHeaderFromAuthInfo(this._headers, {
|
|
376
|
+
type: "Bearer",
|
|
377
|
+
token: opts.token
|
|
378
|
+
});
|
|
379
|
+
} else if (opts.username || opts.password) {
|
|
380
|
+
_setAuthHeaderFromAuthInfo(this._headers, {
|
|
381
|
+
type: "Basic",
|
|
382
|
+
username: opts.username ?? "",
|
|
383
|
+
password: opts.password ?? ""
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
_setAuthHeaderFromAuthInfo(this._headers, {
|
|
387
|
+
type: "None"
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
this._url = urlFromIndex(this.repo.index, opts.scheme);
|
|
391
|
+
this._userAgent = opts.userAgent || DEFAULT_USERAGENT;
|
|
392
|
+
this._adapter = opts.adapter;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Login V2
|
|
396
|
+
*
|
|
397
|
+
* Typically one does not need to call this function directly because most
|
|
398
|
+
* methods of a `GHCRClient` will automatically login as necessary.
|
|
399
|
+
*
|
|
400
|
+
* @param opts {Object}
|
|
401
|
+
* - opts.scope {String} Optional. A scope string passed in for
|
|
402
|
+
* bearer/token auth. If this is just a login request where the token
|
|
403
|
+
* won't be used, then the empty string (the default) is sufficient.
|
|
404
|
+
* // JSSTYLED
|
|
405
|
+
* See <https://github.com/docker/distribution/blob/master/docs/spec/auth/token.md#requesting-a-token>
|
|
406
|
+
* @return an object with authentication info
|
|
407
|
+
*/
|
|
408
|
+
async performLogin(opts) {
|
|
409
|
+
return {
|
|
410
|
+
type: "Bearer",
|
|
411
|
+
token: await this._getToken({
|
|
412
|
+
realm: REALM,
|
|
413
|
+
service: SERVICE,
|
|
414
|
+
scopes: opts.scope ? [opts.scope] : []
|
|
415
|
+
})
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get an auth token.
|
|
420
|
+
*
|
|
421
|
+
* See: docker/docker.git:registry/token.go
|
|
422
|
+
*/
|
|
423
|
+
async _getToken(opts) {
|
|
424
|
+
let tokenUrl = opts.realm;
|
|
425
|
+
const match = /^(\w+):\/\//.exec(tokenUrl);
|
|
426
|
+
if (!match) {
|
|
427
|
+
tokenUrl = (this.insecure ? "http" : "https") + "://" + tokenUrl;
|
|
428
|
+
} else if (match[1] && ["http", "https"].indexOf(match[1]) === -1) {
|
|
429
|
+
throw new Error(`unsupported scheme for WWW-Authenticate realm "${opts.realm}": "${match[1]}"`);
|
|
430
|
+
}
|
|
431
|
+
const headers = {};
|
|
432
|
+
const query = new URLSearchParams();
|
|
433
|
+
if (opts.service) {
|
|
434
|
+
query.set("service", opts.service);
|
|
435
|
+
}
|
|
436
|
+
if (opts.scopes && opts.scopes.length) {
|
|
437
|
+
for (const scope of opts.scopes) {
|
|
438
|
+
query.append("scope", scope);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (this.username) {
|
|
442
|
+
query.set("account", this.username);
|
|
443
|
+
_setAuthHeaderFromAuthInfo(headers, {
|
|
444
|
+
type: "Basic",
|
|
445
|
+
username: this.username,
|
|
446
|
+
password: this.password ?? ""
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (query.toString()) {
|
|
450
|
+
tokenUrl += "?" + query.toString();
|
|
451
|
+
}
|
|
452
|
+
headers["user-agent"] = this._userAgent;
|
|
453
|
+
const resp = await this._adapter.fetch(tokenUrl, {
|
|
454
|
+
method: "GET",
|
|
455
|
+
headers
|
|
456
|
+
});
|
|
457
|
+
if (resp.status === 401) {
|
|
458
|
+
const body2 = await resp.json();
|
|
459
|
+
const errMsg = _getRegistryErrorMessage(body2);
|
|
460
|
+
throw new Error(`Registry auth failed: ${errMsg}`);
|
|
461
|
+
}
|
|
462
|
+
if (resp.status !== 200) {
|
|
463
|
+
throw new Error(`Unexpected HTTP ${resp.status} from ${tokenUrl}`);
|
|
464
|
+
}
|
|
465
|
+
const body = await resp.json();
|
|
466
|
+
if (typeof body?.token !== "string") {
|
|
467
|
+
console.error("TODO: auth resp:", body);
|
|
468
|
+
throw new Error("authorization server did not include a token in the response");
|
|
469
|
+
}
|
|
470
|
+
return body.token;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get a registry session (i.e. login to the registry).
|
|
474
|
+
*
|
|
475
|
+
* Typically one does not need to call this method directly because most
|
|
476
|
+
* methods of a client will automatically login as necessary.
|
|
477
|
+
*
|
|
478
|
+
* @param opts {Object} Optional.
|
|
479
|
+
* - opts.scope {String} Optional. Scope to use in the auth Bearer token.
|
|
480
|
+
*
|
|
481
|
+
* Side-effects:
|
|
482
|
+
* - On success, all of `this._loggedIn*`, `this._authInfo`, and
|
|
483
|
+
* `this._headers.authorization` are set.
|
|
484
|
+
*/
|
|
485
|
+
async login(opts = {}) {
|
|
486
|
+
const scope = opts.scope || _makeAuthScope("repository", this.repo.remoteName, this.scopes);
|
|
487
|
+
if (this._loggedIn && this._loggedInScope === scope) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
const authInfo = await this.performLogin({
|
|
491
|
+
scope
|
|
492
|
+
});
|
|
493
|
+
this._loggedIn = true;
|
|
494
|
+
this._loggedInScope = scope;
|
|
495
|
+
this._authInfo = authInfo;
|
|
496
|
+
_setAuthHeaderFromAuthInfo(this._headers, authInfo);
|
|
497
|
+
}
|
|
498
|
+
async listTags(props = {}) {
|
|
499
|
+
const searchParams = new URLSearchParams();
|
|
500
|
+
if (props.pageSize != null)
|
|
501
|
+
searchParams.set("n", `${props.pageSize}`);
|
|
502
|
+
if (props.startingAfter != null)
|
|
503
|
+
searchParams.set("last", props.startingAfter);
|
|
504
|
+
await this.login();
|
|
505
|
+
const url = new URL(`/v2/${encodeURI(this.repo.remoteName)}/tags/list`, this._url);
|
|
506
|
+
url.search = searchParams.toString();
|
|
507
|
+
const headers = { ...this._headers, "user-agent": this._userAgent };
|
|
508
|
+
const resp = await this._adapter.fetch(url.toString(), {
|
|
509
|
+
method: "GET",
|
|
510
|
+
headers
|
|
511
|
+
});
|
|
512
|
+
if (!resp.ok) {
|
|
513
|
+
throw new Error(`Unexpected HTTP ${resp.status} from ${url.toString()}`);
|
|
514
|
+
}
|
|
515
|
+
return await resp.json();
|
|
516
|
+
}
|
|
517
|
+
async listAllTags(props = {}) {
|
|
518
|
+
const pages = [];
|
|
519
|
+
for await (const page of this.listTagsPaginated(props)) {
|
|
520
|
+
pages.push(page);
|
|
521
|
+
}
|
|
522
|
+
const firstPage = pages.shift();
|
|
523
|
+
for (const nextPage of pages) {
|
|
524
|
+
firstPage.tags = [...firstPage.tags, ...nextPage.tags];
|
|
525
|
+
}
|
|
526
|
+
return firstPage;
|
|
527
|
+
}
|
|
528
|
+
async *listTagsPaginated(props = {}) {
|
|
529
|
+
await this.login();
|
|
530
|
+
let path4 = `/v2/${encodeURI(this.repo.remoteName)}/tags/list`;
|
|
531
|
+
if (props.pageSize != null) {
|
|
532
|
+
path4 += `?n=${props.pageSize}`;
|
|
533
|
+
}
|
|
534
|
+
while (path4) {
|
|
535
|
+
const url = new URL(path4, this._url);
|
|
536
|
+
const headers = { ...this._headers, "user-agent": this._userAgent };
|
|
537
|
+
const resp = await this._adapter.fetch(url.toString(), {
|
|
538
|
+
method: "GET",
|
|
539
|
+
headers
|
|
540
|
+
});
|
|
541
|
+
if (!resp.ok) {
|
|
542
|
+
throw new Error(`Unexpected HTTP ${resp.status} from ${url.toString()}`);
|
|
543
|
+
}
|
|
544
|
+
const linkHeader = resp.headers.get("link");
|
|
545
|
+
const links = parseLinkHeader(linkHeader ?? null);
|
|
546
|
+
const nextLink = links.find((x) => x.rel == "next");
|
|
547
|
+
path4 = nextLink?.url ?? null;
|
|
548
|
+
yield await resp.json();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/*
|
|
552
|
+
* Get an image manifest. `ref` is either a tag or a digest.
|
|
553
|
+
* <https://docs.docker.com/registry/spec/api/#pulling-an-image-manifest>
|
|
554
|
+
*
|
|
555
|
+
* Note that docker-content-digest header can be undefined, so if you
|
|
556
|
+
* need a manifest digest, use the `digestFromManifestStr` function.
|
|
557
|
+
*/
|
|
558
|
+
async getManifest(opts) {
|
|
559
|
+
const acceptOCIManifests = opts.acceptOCIManifests ?? this.acceptOCIManifests;
|
|
560
|
+
const acceptManifestLists = opts.acceptManifestLists ?? this.acceptManifestLists;
|
|
561
|
+
await this.login();
|
|
562
|
+
const headers = {
|
|
563
|
+
...this._headers,
|
|
564
|
+
"user-agent": this._userAgent
|
|
565
|
+
};
|
|
566
|
+
const acceptTypes = [MEDIATYPE_MANIFEST_V2];
|
|
567
|
+
if (acceptManifestLists) {
|
|
568
|
+
acceptTypes.push(MEDIATYPE_MANIFEST_LIST_V2);
|
|
569
|
+
}
|
|
570
|
+
if (acceptOCIManifests) {
|
|
571
|
+
acceptTypes.push(MEDIATYPE_OCI_MANIFEST_V1);
|
|
572
|
+
if (acceptManifestLists) {
|
|
573
|
+
acceptTypes.push(MEDIATYPE_OCI_MANIFEST_INDEX_V1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
headers["accept"] = acceptTypes.join(", ");
|
|
577
|
+
const url = new URL(`/v2/${encodeURI(this.repo.remoteName ?? "")}/manifests/${encodeURI(opts.ref)}`, this._url);
|
|
578
|
+
const resp = await this._adapter.fetch(url.toString(), {
|
|
579
|
+
method: "GET",
|
|
580
|
+
headers,
|
|
581
|
+
redirect: opts.followRedirects == false ? "manual" : "follow"
|
|
582
|
+
});
|
|
583
|
+
if (resp.status === 401) {
|
|
584
|
+
const body = await resp.json();
|
|
585
|
+
const errMsg = _getRegistryErrorMessage(body);
|
|
586
|
+
throw new Error(`Manifest ${JSON.stringify(opts.ref)} Not Found: ${errMsg}`);
|
|
587
|
+
}
|
|
588
|
+
if (!resp.ok) {
|
|
589
|
+
throw new Error(`Unexpected HTTP ${resp.status} from ${url.toString()}`);
|
|
590
|
+
}
|
|
591
|
+
const manifest = await resp.json();
|
|
592
|
+
if (manifest.schemaVersion === 1) {
|
|
593
|
+
throw new Error(`schemaVersion 1 is not supported by /x/docker_registry_client.`);
|
|
594
|
+
}
|
|
595
|
+
return { resp, manifest };
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Makes a http request to the given url, following any redirects, then fires
|
|
599
|
+
* the callback(err, req, responses) with the result.
|
|
600
|
+
*
|
|
601
|
+
* Note that 'responses' is an *array* of Response objects, with
|
|
602
|
+
* the last response being at the end of the array. When there is more than
|
|
603
|
+
* one response, it means a redirect has been followed.
|
|
604
|
+
*/
|
|
605
|
+
async _makeHttpRequest(opts) {
|
|
606
|
+
const followRedirects = opts.followRedirects ?? true;
|
|
607
|
+
const maxRedirects = opts.maxRedirects ?? 3;
|
|
608
|
+
let numRedirs = 0;
|
|
609
|
+
const req = {
|
|
610
|
+
path: opts.path,
|
|
611
|
+
headers: opts.headers
|
|
612
|
+
};
|
|
613
|
+
const ress = new Array();
|
|
614
|
+
while (numRedirs < maxRedirects) {
|
|
615
|
+
numRedirs += 1;
|
|
616
|
+
const url = new URL(req.path, this._url);
|
|
617
|
+
const headers = {
|
|
618
|
+
...req.headers,
|
|
619
|
+
"user-agent": this._userAgent
|
|
620
|
+
};
|
|
621
|
+
const resp = await this._adapter.fetch(url.toString(), {
|
|
622
|
+
method: opts.method,
|
|
623
|
+
headers,
|
|
624
|
+
redirect: "manual"
|
|
625
|
+
});
|
|
626
|
+
ress.push(resp);
|
|
627
|
+
if (!followRedirects)
|
|
628
|
+
return ress;
|
|
629
|
+
if (!(resp.status === 302 || resp.status === 307))
|
|
630
|
+
return ress;
|
|
631
|
+
const location = resp.headers.get("location");
|
|
632
|
+
if (!location)
|
|
633
|
+
return ress;
|
|
634
|
+
const loc = new URL(location, url);
|
|
635
|
+
req.path = loc.toString();
|
|
636
|
+
req.headers = {};
|
|
637
|
+
}
|
|
638
|
+
throw new TooManyRedirectsError(`maximum number of redirects (${maxRedirects}) hit`);
|
|
639
|
+
}
|
|
640
|
+
async _headOrGetBlob(method, digest) {
|
|
641
|
+
await this.login();
|
|
642
|
+
return await this._makeHttpRequest({
|
|
643
|
+
method,
|
|
644
|
+
path: `/v2/${encodeURI(this.repo.remoteName ?? "")}/blobs/${encodeURI(digest)}`,
|
|
645
|
+
headers: this._headers
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
/*
|
|
649
|
+
* Get an image file blob -- just the headers. See `getBlob`.
|
|
650
|
+
*
|
|
651
|
+
* <https://docs.docker.com/registry/spec/api/#get-blob>
|
|
652
|
+
* <https://docs.docker.com/registry/spec/api/#pulling-an-image-manifest>
|
|
653
|
+
*
|
|
654
|
+
* This endpoint can return 3xx redirects. The first response often redirects
|
|
655
|
+
* to an object CDN, which would then return the raw data.
|
|
656
|
+
*
|
|
657
|
+
* Interesting headers:
|
|
658
|
+
* - `ress[0].headers.get('docker-content-digest')` is the digest of the
|
|
659
|
+
* content to be downloaded
|
|
660
|
+
* - `ress[-1].headers.get('content-length')` is the number of bytes to download
|
|
661
|
+
* - `ress[-1].headers[*]` as appropriate for HTTP caching, range gets, etc.
|
|
662
|
+
*/
|
|
663
|
+
async headBlob(opts) {
|
|
664
|
+
const resp = await this._headOrGetBlob("HEAD", opts.digest);
|
|
665
|
+
return resp;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Download a blob and return its ArrayBuffer.
|
|
669
|
+
* <https://docs.docker.com/registry/spec/api/#get-blob>
|
|
670
|
+
*
|
|
671
|
+
* @return
|
|
672
|
+
* The `buffer` is the blob's content as an ArrayBuffer.
|
|
673
|
+
* `ress` (plural of 'res') is an array of responses
|
|
674
|
+
* after following redirects. The full set of responses are returned mainly because
|
|
675
|
+
* headers on both the first, e.g. 'Docker-Content-Digest', and last,
|
|
676
|
+
* e.g. 'Content-Length', might be interesting.
|
|
677
|
+
*/
|
|
678
|
+
async downloadBlob(opts) {
|
|
679
|
+
const ress = await this._headOrGetBlob("GET", opts.digest);
|
|
680
|
+
const lastResp = ress[ress.length - 1];
|
|
681
|
+
if (!lastResp) {
|
|
682
|
+
throw new BlobReadError(`No response available for blob ${opts.digest}`);
|
|
683
|
+
}
|
|
684
|
+
const buffer = await lastResp.arrayBuffer();
|
|
685
|
+
const dcdHeader = ress[0]?.headers.get("docker-content-digest");
|
|
686
|
+
if (dcdHeader) {
|
|
687
|
+
const dcdInfo = _parseDockerContentDigest(dcdHeader);
|
|
688
|
+
if (dcdInfo.raw !== opts.digest) {
|
|
689
|
+
throw new BadDigestError(`Docker-Content-Digest header, ${dcdInfo.raw}, does not match given digest, ${opts.digest}`);
|
|
690
|
+
}
|
|
691
|
+
await dcdInfo.validate(buffer);
|
|
692
|
+
}
|
|
693
|
+
return { ress, buffer };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Upload a blob using POST then PUT workflow.
|
|
697
|
+
* <https://github.com/opencontainers/distribution-spec/blob/main/spec.md#post-then-put>
|
|
698
|
+
*
|
|
699
|
+
* @param opts.data The blob data as ArrayBuffer or Uint8Array
|
|
700
|
+
* @param opts.digest Optional digest. If not provided, it will be calculated.
|
|
701
|
+
* @returns Object with digest and size of the uploaded blob
|
|
702
|
+
*/
|
|
703
|
+
async pushBlob(opts) {
|
|
704
|
+
await this.login();
|
|
705
|
+
const buffer = opts.data instanceof Uint8Array ? new Uint8Array(opts.data).buffer : opts.data;
|
|
706
|
+
const hashBuffer = await getCrypto().subtle.digest("SHA-256", buffer);
|
|
707
|
+
const digest = `sha256:${encodeHex(hashBuffer)}`;
|
|
708
|
+
const postUrl = new URL(`/v2/${encodeURI(this.repo.remoteName)}/blobs/uploads/`, this._url);
|
|
709
|
+
const postHeaders = {
|
|
710
|
+
...this._headers,
|
|
711
|
+
"user-agent": this._userAgent,
|
|
712
|
+
"content-length": "0"
|
|
713
|
+
};
|
|
714
|
+
const postResp = await this._adapter.fetch(postUrl.toString(), {
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: postHeaders
|
|
717
|
+
});
|
|
718
|
+
if (postResp.status !== 202) {
|
|
719
|
+
throw new Error(`Failed to initiate blob upload: HTTP ${postResp.status}`);
|
|
720
|
+
}
|
|
721
|
+
const uploadLocation = postResp.headers.get("location");
|
|
722
|
+
if (!uploadLocation) {
|
|
723
|
+
throw new Error("No Location header in POST response");
|
|
724
|
+
}
|
|
725
|
+
const uploadUrl = new URL(uploadLocation, this._url);
|
|
726
|
+
uploadUrl.searchParams.set("digest", digest);
|
|
727
|
+
const putHeaders = {
|
|
728
|
+
...this._headers,
|
|
729
|
+
"user-agent": this._userAgent,
|
|
730
|
+
"content-type": "application/octet-stream",
|
|
731
|
+
"content-length": buffer.byteLength.toString()
|
|
732
|
+
};
|
|
733
|
+
const putResp = await this._adapter.fetch(uploadUrl.toString(), {
|
|
734
|
+
method: "PUT",
|
|
735
|
+
headers: putHeaders,
|
|
736
|
+
body: buffer
|
|
737
|
+
});
|
|
738
|
+
if (putResp.status !== 201) {
|
|
739
|
+
throw new Error(`Failed to upload blob: HTTP ${putResp.status}`);
|
|
740
|
+
}
|
|
741
|
+
const returnedDigest = putResp.headers.get("docker-content-digest");
|
|
742
|
+
if (returnedDigest && returnedDigest !== digest) {
|
|
743
|
+
throw new BadDigestError(`Digest mismatch: expected ${digest}, got ${returnedDigest}`);
|
|
744
|
+
}
|
|
745
|
+
return { digest, size: buffer.byteLength };
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Upload an image manifest.
|
|
749
|
+
* <https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pushing-manifests>
|
|
750
|
+
*
|
|
751
|
+
* @param opts.ref The tag or digest to push to
|
|
752
|
+
* @param opts.manifest The manifest object to upload
|
|
753
|
+
* @param opts.mediaType Optional media type (defaults to OCI manifest type)
|
|
754
|
+
* @returns Object with digest and size of the uploaded manifest
|
|
755
|
+
*/
|
|
756
|
+
async pushManifest(opts) {
|
|
757
|
+
await this.login();
|
|
758
|
+
const manifestStr = JSON.stringify(opts.manifest);
|
|
759
|
+
const manifestBuffer = new TextEncoder().encode(manifestStr);
|
|
760
|
+
const digest = await digestFromManifestStr(manifestStr);
|
|
761
|
+
const url = new URL(`/v2/${encodeURI(this.repo.remoteName)}/manifests/${encodeURI(opts.ref)}`, this._url);
|
|
762
|
+
const headers = {
|
|
763
|
+
...this._headers,
|
|
764
|
+
"user-agent": this._userAgent,
|
|
765
|
+
"content-type": opts.mediaType || "application/vnd.oci.image.manifest.v1+json",
|
|
766
|
+
"content-length": manifestBuffer.byteLength.toString()
|
|
767
|
+
};
|
|
768
|
+
const resp = await this._adapter.fetch(url.toString(), {
|
|
769
|
+
method: "PUT",
|
|
770
|
+
headers,
|
|
771
|
+
body: manifestBuffer
|
|
772
|
+
});
|
|
773
|
+
if (resp.status !== 201) {
|
|
774
|
+
throw new Error(`Failed to push manifest: HTTP ${resp.status}`);
|
|
775
|
+
}
|
|
776
|
+
const returnedDigest = resp.headers.get("docker-content-digest");
|
|
777
|
+
if (returnedDigest && returnedDigest !== digest) {
|
|
778
|
+
throw new BadDigestError(`Digest mismatch: expected ${digest}, got ${returnedDigest}`);
|
|
779
|
+
}
|
|
780
|
+
return { digest, size: manifestBuffer.byteLength };
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Push a plugin manifest as a config blob and create an OCI manifest.
|
|
784
|
+
* This follows the OCI spec where the Obsidian manifest is stored as the config.
|
|
785
|
+
*
|
|
786
|
+
* @param opts.ref The tag or digest to push to
|
|
787
|
+
* @param opts.pluginManifest The Obsidian plugin manifest
|
|
788
|
+
* @param opts.layers The layer descriptors (main.js, styles.css, etc.)
|
|
789
|
+
* @param opts.annotations Optional annotations for the OCI manifest
|
|
790
|
+
* @returns Object with digest, configDigest, and the created manifest
|
|
791
|
+
*/
|
|
792
|
+
async pushPluginManifest(opts) {
|
|
793
|
+
const manifestStr = JSON.stringify(opts.pluginManifest);
|
|
794
|
+
const manifestBuffer = new TextEncoder().encode(manifestStr);
|
|
795
|
+
const configResult = await this.pushBlob({
|
|
796
|
+
data: manifestBuffer
|
|
797
|
+
});
|
|
798
|
+
const manifest = {
|
|
799
|
+
schemaVersion: 2,
|
|
800
|
+
mediaType: MEDIATYPE_OCI_MANIFEST_V1,
|
|
801
|
+
artifactType: "application/vnd.obsidian.plugin.v1+json",
|
|
802
|
+
config: {
|
|
803
|
+
mediaType: MEDIATYPE_OBSIDIAN_PLUGIN_CONFIG_V1,
|
|
804
|
+
digest: configResult.digest,
|
|
805
|
+
size: configResult.size
|
|
806
|
+
},
|
|
807
|
+
layers: opts.layers,
|
|
808
|
+
annotations: opts.annotations
|
|
809
|
+
};
|
|
810
|
+
const manifestResult = await this.pushManifest({
|
|
811
|
+
ref: opts.ref,
|
|
812
|
+
manifest,
|
|
813
|
+
mediaType: MEDIATYPE_OCI_MANIFEST_V1
|
|
814
|
+
});
|
|
815
|
+
return {
|
|
816
|
+
digest: manifestResult.digest,
|
|
817
|
+
configDigest: configResult.digest,
|
|
818
|
+
manifest
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Pull a plugin manifest by extracting it from the OCI config blob.
|
|
823
|
+
* This follows the OCI spec where the Obsidian manifest is stored as the config.
|
|
824
|
+
*
|
|
825
|
+
* @param opts.ref The tag or digest to pull
|
|
826
|
+
* @returns Object with the plugin manifest, OCI manifest, and digests
|
|
827
|
+
*/
|
|
828
|
+
async pullPluginManifest(opts) {
|
|
829
|
+
const manifestResult = await this.getManifest({ ref: opts.ref });
|
|
830
|
+
const manifest = manifestResult.manifest;
|
|
831
|
+
if (!("config" in manifest) || !manifest.config) {
|
|
832
|
+
throw new Error("Manifest does not contain a config");
|
|
833
|
+
}
|
|
834
|
+
const ociManifest = manifest;
|
|
835
|
+
const manifestDigest = manifestResult.resp.headers.get("docker-content-digest") || "";
|
|
836
|
+
const { buffer: configBuffer } = await this.downloadBlob({
|
|
837
|
+
digest: ociManifest.config.digest
|
|
838
|
+
});
|
|
839
|
+
const configText = new TextDecoder().decode(configBuffer);
|
|
840
|
+
const pluginManifest = JSON.parse(configText);
|
|
841
|
+
return {
|
|
842
|
+
pluginManifest,
|
|
843
|
+
manifest: ociManifest,
|
|
844
|
+
manifestDigest,
|
|
845
|
+
configDigest: ociManifest.config.digest
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
// src/lib/plugin.ts
|
|
851
|
+
import * as fs from "node:fs/promises";
|
|
852
|
+
import * as path from "node:path";
|
|
853
|
+
async function discoverPlugin(directory) {
|
|
854
|
+
const absDirectory = path.resolve(directory);
|
|
855
|
+
try {
|
|
856
|
+
const stat3 = await fs.stat(absDirectory);
|
|
857
|
+
if (!stat3.isDirectory()) {
|
|
858
|
+
throw new Error(`Not a directory: ${directory}`);
|
|
859
|
+
}
|
|
860
|
+
} catch (err) {
|
|
861
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
862
|
+
throw new Error(`Directory not found: ${directory}`);
|
|
863
|
+
}
|
|
864
|
+
throw err;
|
|
865
|
+
}
|
|
866
|
+
const manifestPath = path.join(absDirectory, "manifest.json");
|
|
867
|
+
let manifestContent;
|
|
868
|
+
let manifestParsed;
|
|
869
|
+
try {
|
|
870
|
+
const buffer = await fs.readFile(manifestPath);
|
|
871
|
+
manifestContent = buffer.buffer.slice(
|
|
872
|
+
buffer.byteOffset,
|
|
873
|
+
buffer.byteOffset + buffer.byteLength
|
|
874
|
+
);
|
|
875
|
+
const text = new TextDecoder().decode(manifestContent);
|
|
876
|
+
const parsed = JSON.parse(text);
|
|
877
|
+
if (!parsed.version || typeof parsed.version !== "string") {
|
|
878
|
+
throw new Error('manifest.json missing required "version" field');
|
|
879
|
+
}
|
|
880
|
+
if (!parsed.id || typeof parsed.id !== "string") {
|
|
881
|
+
throw new Error('manifest.json missing required "id" field');
|
|
882
|
+
}
|
|
883
|
+
if (!parsed.name || typeof parsed.name !== "string") {
|
|
884
|
+
throw new Error('manifest.json missing required "name" field');
|
|
885
|
+
}
|
|
886
|
+
if (!parsed.minAppVersion || typeof parsed.minAppVersion !== "string") {
|
|
887
|
+
throw new Error('manifest.json missing required "minAppVersion" field');
|
|
888
|
+
}
|
|
889
|
+
if (!parsed.description || typeof parsed.description !== "string") {
|
|
890
|
+
throw new Error('manifest.json missing required "description" field');
|
|
891
|
+
}
|
|
892
|
+
if (!parsed.author || typeof parsed.author !== "string") {
|
|
893
|
+
throw new Error('manifest.json missing required "author" field');
|
|
894
|
+
}
|
|
895
|
+
manifestParsed = parsed;
|
|
896
|
+
} catch (err) {
|
|
897
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
898
|
+
throw new Error(`manifest.json not found in ${directory}`);
|
|
899
|
+
}
|
|
900
|
+
if (err instanceof SyntaxError) {
|
|
901
|
+
throw new Error(`Could not parse manifest.json: ${err.message}`);
|
|
902
|
+
}
|
|
903
|
+
throw err;
|
|
904
|
+
}
|
|
905
|
+
const mainJsPath = path.join(absDirectory, "main.js");
|
|
906
|
+
let mainJsContent;
|
|
907
|
+
try {
|
|
908
|
+
const buffer = await fs.readFile(mainJsPath);
|
|
909
|
+
mainJsContent = buffer.buffer.slice(
|
|
910
|
+
buffer.byteOffset,
|
|
911
|
+
buffer.byteOffset + buffer.byteLength
|
|
912
|
+
);
|
|
913
|
+
} catch (err) {
|
|
914
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
915
|
+
throw new Error(`main.js not found in ${directory}`);
|
|
916
|
+
}
|
|
917
|
+
throw err;
|
|
918
|
+
}
|
|
919
|
+
const stylesCssPath = path.join(absDirectory, "styles.css");
|
|
920
|
+
let stylesCssContent;
|
|
921
|
+
try {
|
|
922
|
+
const buffer = await fs.readFile(stylesCssPath);
|
|
923
|
+
stylesCssContent = buffer.buffer.slice(
|
|
924
|
+
buffer.byteOffset,
|
|
925
|
+
buffer.byteOffset + buffer.byteLength
|
|
926
|
+
);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
929
|
+
stylesCssContent = void 0;
|
|
930
|
+
} else {
|
|
931
|
+
throw err;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return {
|
|
935
|
+
directory: absDirectory,
|
|
936
|
+
manifest: {
|
|
937
|
+
path: manifestPath,
|
|
938
|
+
content: manifestContent,
|
|
939
|
+
parsed: manifestParsed
|
|
940
|
+
},
|
|
941
|
+
mainJs: {
|
|
942
|
+
path: mainJsPath,
|
|
943
|
+
content: mainJsContent
|
|
944
|
+
},
|
|
945
|
+
...stylesCssContent && {
|
|
946
|
+
stylesCss: {
|
|
947
|
+
path: stylesCssPath,
|
|
948
|
+
content: stylesCssContent
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// src/commands/push.ts
|
|
955
|
+
function deriveGitHubUrl(remoteName) {
|
|
956
|
+
const parts = remoteName.split("/");
|
|
957
|
+
if (parts.length < 2) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Cannot derive GitHub URL from ${remoteName}. Need at least owner/repo format.`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
const owner = parts[0];
|
|
963
|
+
const repo = parts[1];
|
|
964
|
+
return `https://github.com/${owner}/${repo}`;
|
|
965
|
+
}
|
|
966
|
+
async function pushCommand(opts) {
|
|
967
|
+
const { directory, repository, token, logger, adapter } = opts;
|
|
968
|
+
logger.log(`Discovering plugin files in ${directory}...`);
|
|
969
|
+
const plugin = await discoverPlugin(directory);
|
|
970
|
+
const version = plugin.manifest.parsed.version;
|
|
971
|
+
logger.log(`Found plugin version ${version}`);
|
|
972
|
+
const fullRef = repository.includes(":") ? repository : `${repository}:${version}`;
|
|
973
|
+
logger.log(`Pushing to ${fullRef}...`);
|
|
974
|
+
const ref = parseRepoAndRef(fullRef);
|
|
975
|
+
const client = new OciRegistryClient({
|
|
976
|
+
repo: ref,
|
|
977
|
+
username: "github",
|
|
978
|
+
password: token,
|
|
979
|
+
adapter,
|
|
980
|
+
scopes: ["push", "pull"]
|
|
981
|
+
});
|
|
982
|
+
const layers = [];
|
|
983
|
+
logger.log("Pushing main.js...");
|
|
984
|
+
const mainJsResult = await client.pushBlob({
|
|
985
|
+
data: plugin.mainJs.content
|
|
986
|
+
});
|
|
987
|
+
layers.push({
|
|
988
|
+
mediaType: "application/javascript",
|
|
989
|
+
digest: mainJsResult.digest,
|
|
990
|
+
size: mainJsResult.size,
|
|
991
|
+
annotations: {
|
|
992
|
+
"org.opencontainers.image.title": "main.js"
|
|
993
|
+
}
|
|
994
|
+
});
|
|
995
|
+
logger.log(
|
|
996
|
+
`Pushed main.js: ${mainJsResult.digest.slice(0, 19)}... (${mainJsResult.size} bytes)`
|
|
997
|
+
);
|
|
998
|
+
if (plugin.stylesCss) {
|
|
999
|
+
logger.log("Pushing styles.css...");
|
|
1000
|
+
const stylesCssResult = await client.pushBlob({
|
|
1001
|
+
data: plugin.stylesCss.content
|
|
1002
|
+
});
|
|
1003
|
+
layers.push({
|
|
1004
|
+
mediaType: "text/css",
|
|
1005
|
+
digest: stylesCssResult.digest,
|
|
1006
|
+
size: stylesCssResult.size,
|
|
1007
|
+
annotations: {
|
|
1008
|
+
"org.opencontainers.image.title": "styles.css"
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
logger.log(
|
|
1012
|
+
`Pushed styles.css: ${stylesCssResult.digest.slice(0, 19)}... (${stylesCssResult.size} bytes)`
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
const githubUrl = deriveGitHubUrl(ref.remoteName);
|
|
1016
|
+
const manifest = plugin.manifest.parsed;
|
|
1017
|
+
const annotations = {
|
|
1018
|
+
"org.opencontainers.image.created": (/* @__PURE__ */ new Date()).toISOString(),
|
|
1019
|
+
"org.opencontainers.image.source": githubUrl,
|
|
1020
|
+
"org.opencontainers.image.version": manifest.version,
|
|
1021
|
+
"org.opencontainers.image.description": manifest.description,
|
|
1022
|
+
"org.opencontainers.image.authors": manifest.author
|
|
1023
|
+
};
|
|
1024
|
+
if (manifest.authorUrl) {
|
|
1025
|
+
annotations["org.opencontainers.image.url"] = manifest.authorUrl;
|
|
1026
|
+
}
|
|
1027
|
+
logger.log("Pushing plugin manifest...");
|
|
1028
|
+
const manifestPushResult = await client.pushPluginManifest({
|
|
1029
|
+
ref: ref.tag || version,
|
|
1030
|
+
pluginManifest: manifest,
|
|
1031
|
+
layers,
|
|
1032
|
+
annotations
|
|
1033
|
+
});
|
|
1034
|
+
logger.success(`Successfully pushed ${fullRef}`);
|
|
1035
|
+
logger.log(`Manifest digest: ${manifestPushResult.digest}`);
|
|
1036
|
+
logger.log(`GitHub repository: ${githubUrl}`);
|
|
1037
|
+
const totalSize = manifestPushResult.manifest.layers.reduce(
|
|
1038
|
+
(sum, layer) => sum + layer.size,
|
|
1039
|
+
0
|
|
1040
|
+
);
|
|
1041
|
+
return {
|
|
1042
|
+
digest: manifestPushResult.digest,
|
|
1043
|
+
tag: ref.tag || version,
|
|
1044
|
+
size: totalSize,
|
|
1045
|
+
repository: fullRef
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// src/commands/pull.ts
|
|
1050
|
+
import * as fs2 from "node:fs/promises";
|
|
1051
|
+
import * as path2 from "node:path";
|
|
1052
|
+
async function pullCommand(opts) {
|
|
1053
|
+
const { repository, output, token, logger, adapter } = opts;
|
|
1054
|
+
logger.log(`Pulling ${repository}...`);
|
|
1055
|
+
const ref = parseRepoAndRef(repository);
|
|
1056
|
+
if (!ref.tag && !ref.digest) {
|
|
1057
|
+
throw new Error("Repository reference must include tag or digest");
|
|
1058
|
+
}
|
|
1059
|
+
const client = new OciRegistryClient({
|
|
1060
|
+
repo: ref,
|
|
1061
|
+
username: "github",
|
|
1062
|
+
password: token,
|
|
1063
|
+
adapter
|
|
1064
|
+
});
|
|
1065
|
+
logger.log("Fetching manifest...");
|
|
1066
|
+
const refString = ref.tag || ref.digest || "";
|
|
1067
|
+
const pullResult = await client.pullPluginManifest({ ref: refString });
|
|
1068
|
+
const manifest = pullResult.manifest;
|
|
1069
|
+
logger.log(`Manifest digest: ${pullResult.manifestDigest}`);
|
|
1070
|
+
const absOutput = path2.resolve(output);
|
|
1071
|
+
logger.log(`Creating output directory: ${absOutput}`);
|
|
1072
|
+
await fs2.mkdir(absOutput, { recursive: true });
|
|
1073
|
+
const manifestPath = path2.join(absOutput, "manifest.json");
|
|
1074
|
+
const manifestJson = JSON.stringify(pullResult.pluginManifest, null, 2);
|
|
1075
|
+
await fs2.writeFile(manifestPath, manifestJson, "utf-8");
|
|
1076
|
+
logger.log(`Wrote manifest.json (${manifestJson.length} bytes)`);
|
|
1077
|
+
const files = ["manifest.json"];
|
|
1078
|
+
for (const layer of manifest.layers) {
|
|
1079
|
+
const filename = layer.annotations?.["org.opencontainers.image.title"];
|
|
1080
|
+
if (!filename) {
|
|
1081
|
+
throw new Error(
|
|
1082
|
+
`Layer ${layer.digest} missing required filename annotation`
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
logger.log(`Downloading ${filename}...`);
|
|
1086
|
+
const blobResult = await client.downloadBlob({
|
|
1087
|
+
digest: layer.digest
|
|
1088
|
+
});
|
|
1089
|
+
const filePath = path2.join(absOutput, filename);
|
|
1090
|
+
const buffer = Buffer.from(blobResult.buffer);
|
|
1091
|
+
await fs2.writeFile(filePath, buffer);
|
|
1092
|
+
logger.log(`Wrote ${filename} (${buffer.length} bytes)`);
|
|
1093
|
+
files.push(filename);
|
|
1094
|
+
}
|
|
1095
|
+
logger.success(`Successfully pulled ${repository}`);
|
|
1096
|
+
logger.log(`Files extracted to: ${absOutput}`);
|
|
1097
|
+
return {
|
|
1098
|
+
files,
|
|
1099
|
+
output: absOutput,
|
|
1100
|
+
digest: pullResult.manifestDigest
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/lib/community-plugins.ts
|
|
1105
|
+
var COMMUNITY_PLUGINS_URL = "https://raw.githubusercontent.com/obsidianmd/obsidian-releases/refs/heads/master/community-plugins.json";
|
|
1106
|
+
|
|
1107
|
+
// src/lib/community-cache.ts
|
|
1108
|
+
var CommunityPluginsCache = class {
|
|
1109
|
+
adapter;
|
|
1110
|
+
plugins = null;
|
|
1111
|
+
constructor(adapter) {
|
|
1112
|
+
this.adapter = adapter;
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Fetch the list of community plugins.
|
|
1116
|
+
* Results are cached for subsequent calls.
|
|
1117
|
+
*
|
|
1118
|
+
* @returns Array of community plugins
|
|
1119
|
+
* @throws Error if fetch fails
|
|
1120
|
+
*/
|
|
1121
|
+
async fetch() {
|
|
1122
|
+
if (this.plugins !== null) {
|
|
1123
|
+
return this.plugins;
|
|
1124
|
+
}
|
|
1125
|
+
const response = await this.adapter.fetch(COMMUNITY_PLUGINS_URL);
|
|
1126
|
+
if (!response.ok) {
|
|
1127
|
+
throw new Error(`Failed to fetch community plugins: ${response.status}`);
|
|
1128
|
+
}
|
|
1129
|
+
const plugins = await response.json();
|
|
1130
|
+
this.plugins = plugins;
|
|
1131
|
+
return plugins;
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Find a plugin by ID.
|
|
1135
|
+
* Fetches the plugin list if not already cached.
|
|
1136
|
+
*
|
|
1137
|
+
* @param id - Plugin ID to search for
|
|
1138
|
+
* @returns Plugin if found, undefined otherwise
|
|
1139
|
+
*/
|
|
1140
|
+
async findPlugin(id) {
|
|
1141
|
+
const plugins = await this.fetch();
|
|
1142
|
+
return plugins.find((plugin) => plugin.id === id);
|
|
1143
|
+
}
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
// src/lib/github-release.ts
|
|
1147
|
+
var GitHubReleaseFetcher = class {
|
|
1148
|
+
adapter;
|
|
1149
|
+
constructor(adapter) {
|
|
1150
|
+
this.adapter = adapter;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Fetch the latest release from a GitHub repository.
|
|
1154
|
+
*
|
|
1155
|
+
* @param repo - Repository in format "owner/repo"
|
|
1156
|
+
* @param token - Optional GitHub token for authentication
|
|
1157
|
+
* @returns Latest release information
|
|
1158
|
+
* @throws Error if fetch fails
|
|
1159
|
+
*/
|
|
1160
|
+
async fetchLatestRelease(repo, token) {
|
|
1161
|
+
const url = `https://api.github.com/repos/${repo}/releases/latest`;
|
|
1162
|
+
const headers = {
|
|
1163
|
+
Accept: "application/vnd.github.v3+json"
|
|
1164
|
+
};
|
|
1165
|
+
if (token) {
|
|
1166
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1167
|
+
}
|
|
1168
|
+
const response = await this.adapter.fetch(url, { headers });
|
|
1169
|
+
if (!response.ok) {
|
|
1170
|
+
throw new Error(`Failed to fetch latest release: ${response.status}`);
|
|
1171
|
+
}
|
|
1172
|
+
return await response.json();
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Fetch a specific release by tag from a GitHub repository.
|
|
1176
|
+
*
|
|
1177
|
+
* @param repo - Repository in format "owner/repo"
|
|
1178
|
+
* @param tag - Tag name (e.g., "1.2.3")
|
|
1179
|
+
* @param token - Optional GitHub token for authentication
|
|
1180
|
+
* @returns Release information
|
|
1181
|
+
* @throws Error if fetch fails
|
|
1182
|
+
*/
|
|
1183
|
+
async fetchReleaseByTag(repo, tag, token) {
|
|
1184
|
+
const url = `https://api.github.com/repos/${repo}/releases/tags/${tag}`;
|
|
1185
|
+
const headers = {
|
|
1186
|
+
Accept: "application/vnd.github.v3+json"
|
|
1187
|
+
};
|
|
1188
|
+
if (token) {
|
|
1189
|
+
headers.Authorization = `Bearer ${token}`;
|
|
1190
|
+
}
|
|
1191
|
+
const response = await this.adapter.fetch(url, { headers });
|
|
1192
|
+
if (!response.ok) {
|
|
1193
|
+
throw new Error(`Failed to fetch release ${tag}: ${response.status}`);
|
|
1194
|
+
}
|
|
1195
|
+
return await response.json();
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
// src/lib/converter.ts
|
|
1200
|
+
var PluginConverter = class {
|
|
1201
|
+
adapter;
|
|
1202
|
+
communityCache;
|
|
1203
|
+
releaseFetcher;
|
|
1204
|
+
constructor(adapter) {
|
|
1205
|
+
this.adapter = adapter;
|
|
1206
|
+
this.communityCache = new CommunityPluginsCache(adapter);
|
|
1207
|
+
this.releaseFetcher = new GitHubReleaseFetcher(adapter);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Convert a plugin from GitHub releases to OCI format.
|
|
1211
|
+
*
|
|
1212
|
+
* @param options - Conversion options
|
|
1213
|
+
* @returns Conversion result with plugin data
|
|
1214
|
+
* @throws Error if plugin not found or conversion fails
|
|
1215
|
+
*/
|
|
1216
|
+
async convertPlugin(options) {
|
|
1217
|
+
const { pluginId, version, repository, token } = options;
|
|
1218
|
+
const plugin = await this.communityCache.findPlugin(pluginId);
|
|
1219
|
+
if (!plugin) {
|
|
1220
|
+
throw new Error(`Plugin "${pluginId}" not found in community plugins`);
|
|
1221
|
+
}
|
|
1222
|
+
const release = version ? await this.releaseFetcher.fetchReleaseByTag(plugin.repo, version, token) : await this.releaseFetcher.fetchLatestRelease(plugin.repo, token);
|
|
1223
|
+
const manifestAsset = release.assets.find(
|
|
1224
|
+
(a) => a.name === "manifest.json"
|
|
1225
|
+
);
|
|
1226
|
+
const mainJsAsset = release.assets.find((a) => a.name === "main.js");
|
|
1227
|
+
const stylesCssAsset = release.assets.find((a) => a.name === "styles.css");
|
|
1228
|
+
if (!manifestAsset) {
|
|
1229
|
+
throw new Error("manifest.json not found in release");
|
|
1230
|
+
}
|
|
1231
|
+
if (!mainJsAsset) {
|
|
1232
|
+
throw new Error("main.js not found in release");
|
|
1233
|
+
}
|
|
1234
|
+
const manifestResponse = await this.adapter.fetch(
|
|
1235
|
+
manifestAsset.browser_download_url
|
|
1236
|
+
);
|
|
1237
|
+
if (!manifestResponse.ok) {
|
|
1238
|
+
throw new Error(
|
|
1239
|
+
`Failed to download manifest.json: ${manifestResponse.status}`
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
const manifestJson = await manifestResponse.text();
|
|
1243
|
+
const manifest = JSON.parse(manifestJson);
|
|
1244
|
+
const mainJsResponse = await this.adapter.fetch(
|
|
1245
|
+
mainJsAsset.browser_download_url
|
|
1246
|
+
);
|
|
1247
|
+
if (!mainJsResponse.ok) {
|
|
1248
|
+
throw new Error(`Failed to download main.js: ${mainJsResponse.status}`);
|
|
1249
|
+
}
|
|
1250
|
+
const mainJs = await mainJsResponse.text();
|
|
1251
|
+
let stylesCss;
|
|
1252
|
+
if (stylesCssAsset) {
|
|
1253
|
+
const stylesCssResponse = await this.adapter.fetch(
|
|
1254
|
+
stylesCssAsset.browser_download_url
|
|
1255
|
+
);
|
|
1256
|
+
if (!stylesCssResponse.ok) {
|
|
1257
|
+
throw new Error(
|
|
1258
|
+
`Failed to download styles.css: ${stylesCssResponse.status}`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
stylesCss = await stylesCssResponse.text();
|
|
1262
|
+
}
|
|
1263
|
+
const fullRepository = repository.includes(":") ? repository : `${repository}:${release.tag_name}`;
|
|
1264
|
+
return {
|
|
1265
|
+
pluginId,
|
|
1266
|
+
version: release.tag_name,
|
|
1267
|
+
repository: fullRepository,
|
|
1268
|
+
githubRepo: plugin.repo,
|
|
1269
|
+
manifest,
|
|
1270
|
+
mainJs,
|
|
1271
|
+
stylesCss
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Push plugin data to OCI registry.
|
|
1276
|
+
*
|
|
1277
|
+
* @param options - Push options
|
|
1278
|
+
* @returns Push result
|
|
1279
|
+
* @throws Error if push fails
|
|
1280
|
+
*/
|
|
1281
|
+
async pushToRegistry(options) {
|
|
1282
|
+
const { repository, githubRepo, token, pluginData } = options;
|
|
1283
|
+
const ref = parseRepoAndRef(repository);
|
|
1284
|
+
const client = new OciRegistryClient({
|
|
1285
|
+
repo: ref,
|
|
1286
|
+
username: "github",
|
|
1287
|
+
password: token,
|
|
1288
|
+
adapter: this.adapter,
|
|
1289
|
+
scopes: ["push", "pull"]
|
|
1290
|
+
});
|
|
1291
|
+
const layers = [];
|
|
1292
|
+
const mainJsResult = await client.pushBlob({
|
|
1293
|
+
data: new TextEncoder().encode(pluginData.mainJs)
|
|
1294
|
+
});
|
|
1295
|
+
layers.push({
|
|
1296
|
+
mediaType: "application/javascript",
|
|
1297
|
+
digest: mainJsResult.digest,
|
|
1298
|
+
size: mainJsResult.size,
|
|
1299
|
+
annotations: {
|
|
1300
|
+
"org.opencontainers.image.title": "main.js"
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
if (pluginData.stylesCss) {
|
|
1304
|
+
const stylesCssResult = await client.pushBlob({
|
|
1305
|
+
data: new TextEncoder().encode(pluginData.stylesCss)
|
|
1306
|
+
});
|
|
1307
|
+
layers.push({
|
|
1308
|
+
mediaType: "text/css",
|
|
1309
|
+
digest: stylesCssResult.digest,
|
|
1310
|
+
size: stylesCssResult.size,
|
|
1311
|
+
annotations: {
|
|
1312
|
+
"org.opencontainers.image.title": "styles.css"
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
const annotations = {
|
|
1317
|
+
"org.opencontainers.image.created": (/* @__PURE__ */ new Date()).toISOString(),
|
|
1318
|
+
"org.opencontainers.image.source": githubRepo,
|
|
1319
|
+
"org.opencontainers.image.version": pluginData.manifest.version,
|
|
1320
|
+
"org.opencontainers.image.description": pluginData.manifest.description,
|
|
1321
|
+
"org.opencontainers.image.authors": pluginData.manifest.author
|
|
1322
|
+
};
|
|
1323
|
+
if (pluginData.manifest.authorUrl) {
|
|
1324
|
+
annotations["org.opencontainers.image.url"] = pluginData.manifest.authorUrl;
|
|
1325
|
+
}
|
|
1326
|
+
const manifestPushResult = await client.pushPluginManifest({
|
|
1327
|
+
ref: ref.tag || pluginData.manifest.version,
|
|
1328
|
+
pluginManifest: pluginData.manifest,
|
|
1329
|
+
layers,
|
|
1330
|
+
annotations
|
|
1331
|
+
});
|
|
1332
|
+
const totalSize = manifestPushResult.manifest.layers.reduce(
|
|
1333
|
+
(sum, layer) => sum + layer.size,
|
|
1334
|
+
0
|
|
1335
|
+
);
|
|
1336
|
+
return {
|
|
1337
|
+
digest: manifestPushResult.digest,
|
|
1338
|
+
tag: ref.tag || pluginData.manifest.version,
|
|
1339
|
+
size: totalSize,
|
|
1340
|
+
repository
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
// src/commands/convert.ts
|
|
1346
|
+
async function convertCommand(opts) {
|
|
1347
|
+
const { pluginId, repository, version, token, logger, adapter } = opts;
|
|
1348
|
+
const converter = new PluginConverter(adapter);
|
|
1349
|
+
logger.log(`Converting plugin "${pluginId}"...`);
|
|
1350
|
+
if (version) {
|
|
1351
|
+
logger.log(`Using specific version: ${version}`);
|
|
1352
|
+
} else {
|
|
1353
|
+
logger.log("Using latest version");
|
|
1354
|
+
}
|
|
1355
|
+
const convertResult = await converter.convertPlugin({
|
|
1356
|
+
pluginId,
|
|
1357
|
+
version,
|
|
1358
|
+
repository,
|
|
1359
|
+
token
|
|
1360
|
+
});
|
|
1361
|
+
logger.log(
|
|
1362
|
+
`Downloaded plugin ${convertResult.pluginId} v${convertResult.version}`
|
|
1363
|
+
);
|
|
1364
|
+
logger.log(` - manifest.json: ${convertResult.manifest.name}`);
|
|
1365
|
+
logger.log(` - main.js: ${convertResult.mainJs.length} bytes`);
|
|
1366
|
+
if (convertResult.stylesCss) {
|
|
1367
|
+
logger.log(` - styles.css: ${convertResult.stylesCss.length} bytes`);
|
|
1368
|
+
}
|
|
1369
|
+
logger.log(`
|
|
1370
|
+
Pushing to ${convertResult.repository}...`);
|
|
1371
|
+
const pushResult = await converter.pushToRegistry({
|
|
1372
|
+
repository: convertResult.repository,
|
|
1373
|
+
githubRepo: convertResult.githubRepo,
|
|
1374
|
+
token,
|
|
1375
|
+
pluginData: {
|
|
1376
|
+
manifest: convertResult.manifest,
|
|
1377
|
+
mainJs: convertResult.mainJs,
|
|
1378
|
+
stylesCss: convertResult.stylesCss
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
logger.success(
|
|
1382
|
+
`Successfully converted and pushed ${convertResult.pluginId} v${convertResult.version}`
|
|
1383
|
+
);
|
|
1384
|
+
logger.log(`Manifest digest: ${pushResult.digest}`);
|
|
1385
|
+
logger.log(`Repository: ${pushResult.repository}`);
|
|
1386
|
+
return {
|
|
1387
|
+
pluginId: convertResult.pluginId,
|
|
1388
|
+
version: convertResult.version,
|
|
1389
|
+
repository: pushResult.repository,
|
|
1390
|
+
digest: pushResult.digest,
|
|
1391
|
+
size: pushResult.size
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
// src/lib/marketplace-client.ts
|
|
1396
|
+
var DEFAULT_MARKETPLACE_URL = "https://shard-for-obsidian.github.io/shard/plugins.json";
|
|
1397
|
+
var MarketplaceClient = class {
|
|
1398
|
+
constructor(adapter, marketplaceUrl = DEFAULT_MARKETPLACE_URL) {
|
|
1399
|
+
this.adapter = adapter;
|
|
1400
|
+
this.marketplaceUrl = marketplaceUrl;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Fetch all plugins from the marketplace.
|
|
1404
|
+
*/
|
|
1405
|
+
async fetchPlugins() {
|
|
1406
|
+
const response = await this.adapter.fetch(this.marketplaceUrl);
|
|
1407
|
+
if (!response.ok) {
|
|
1408
|
+
throw new Error(
|
|
1409
|
+
`Failed to fetch marketplace data: ${response.status} ${response.statusText}`
|
|
1410
|
+
);
|
|
1411
|
+
}
|
|
1412
|
+
const data = await response.json();
|
|
1413
|
+
if (!data.plugins || !Array.isArray(data.plugins)) {
|
|
1414
|
+
throw new Error("Invalid marketplace data format");
|
|
1415
|
+
}
|
|
1416
|
+
return data.plugins;
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Find a plugin by ID.
|
|
1420
|
+
*/
|
|
1421
|
+
async findPluginById(pluginId) {
|
|
1422
|
+
const plugins = await this.fetchPlugins();
|
|
1423
|
+
return plugins.find((p) => p.id === pluginId) || null;
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Search plugins by keyword (searches in name, description, author, tags).
|
|
1427
|
+
*/
|
|
1428
|
+
async searchPlugins(keyword) {
|
|
1429
|
+
const plugins = await this.fetchPlugins();
|
|
1430
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
1431
|
+
return plugins.filter(
|
|
1432
|
+
(p) => p.name.toLowerCase().includes(lowerKeyword) || p.description.toLowerCase().includes(lowerKeyword) || p.author.toLowerCase().includes(lowerKeyword) || p.id.toLowerCase().includes(lowerKeyword) || p.tags?.some((tag) => tag.toLowerCase().includes(lowerKeyword))
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// src/commands/marketplace.ts
|
|
1438
|
+
import * as fs3 from "node:fs/promises";
|
|
1439
|
+
import * as path3 from "node:path";
|
|
1440
|
+
async function marketplaceRegisterCommand(opts) {
|
|
1441
|
+
const { repository, token, logger, adapter } = opts;
|
|
1442
|
+
logger.log(`Fetching plugin metadata from ${repository}...`);
|
|
1443
|
+
const ref = parseRepoAndRef(repository);
|
|
1444
|
+
const client = new OciRegistryClient({
|
|
1445
|
+
repo: ref,
|
|
1446
|
+
username: "github",
|
|
1447
|
+
password: token,
|
|
1448
|
+
adapter,
|
|
1449
|
+
scopes: ["pull"]
|
|
1450
|
+
});
|
|
1451
|
+
const manifestResult = await client.pullPluginManifest({
|
|
1452
|
+
ref: ref.tag || "latest"
|
|
1453
|
+
});
|
|
1454
|
+
const pluginManifest = manifestResult.pluginManifest;
|
|
1455
|
+
const ociManifest = manifestResult.manifest;
|
|
1456
|
+
const pluginId = pluginManifest.id;
|
|
1457
|
+
const name = pluginManifest.name;
|
|
1458
|
+
const author = pluginManifest.author;
|
|
1459
|
+
const description = pluginManifest.description || "";
|
|
1460
|
+
const version = pluginManifest.version;
|
|
1461
|
+
const minObsidianVersion = pluginManifest.minAppVersion;
|
|
1462
|
+
const authorUrl = pluginManifest.authorUrl;
|
|
1463
|
+
let gitHubRepoUrl;
|
|
1464
|
+
if (ociManifest.annotations && ociManifest.annotations["org.opencontainers.image.source"]) {
|
|
1465
|
+
const source = ociManifest.annotations["org.opencontainers.image.source"];
|
|
1466
|
+
if (source.startsWith("http")) {
|
|
1467
|
+
gitHubRepoUrl = source;
|
|
1468
|
+
} else {
|
|
1469
|
+
gitHubRepoUrl = `https://github.com/${source}`;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
const registryUrl = ref.canonicalName;
|
|
1473
|
+
logger.log(`Plugin ID: ${pluginId}`);
|
|
1474
|
+
logger.log(`Name: ${name}`);
|
|
1475
|
+
logger.log(`Author: ${author}`);
|
|
1476
|
+
logger.log(`Version: ${version}`);
|
|
1477
|
+
logger.log(`Registry URL: ${registryUrl}`);
|
|
1478
|
+
if (gitHubRepoUrl) {
|
|
1479
|
+
logger.log(`Repository: ${gitHubRepoUrl}`);
|
|
1480
|
+
}
|
|
1481
|
+
let yamlContent = `id: ${pluginId}
|
|
1482
|
+
registryUrl: ${registryUrl}
|
|
1483
|
+
name: ${name}
|
|
1484
|
+
author: ${author}
|
|
1485
|
+
description: ${description}
|
|
1486
|
+
version: ${version}
|
|
1487
|
+
`;
|
|
1488
|
+
if (gitHubRepoUrl) {
|
|
1489
|
+
yamlContent += `repository: ${gitHubRepoUrl}
|
|
1490
|
+
`;
|
|
1491
|
+
}
|
|
1492
|
+
if (minObsidianVersion) {
|
|
1493
|
+
yamlContent += `minObsidianVersion: ${minObsidianVersion}
|
|
1494
|
+
`;
|
|
1495
|
+
}
|
|
1496
|
+
if (authorUrl) {
|
|
1497
|
+
yamlContent += `authorUrl: ${authorUrl}
|
|
1498
|
+
`;
|
|
1499
|
+
}
|
|
1500
|
+
yamlContent += `updatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1501
|
+
`;
|
|
1502
|
+
const marketplacePath = await findMarketplaceDir();
|
|
1503
|
+
const pluginsDir = path3.join(marketplacePath, "plugins");
|
|
1504
|
+
const yamlPath = path3.join(pluginsDir, `${pluginId}.yml`);
|
|
1505
|
+
await fs3.mkdir(pluginsDir, { recursive: true });
|
|
1506
|
+
await fs3.writeFile(yamlPath, yamlContent, "utf-8");
|
|
1507
|
+
logger.success(`Successfully registered plugin to ${yamlPath}`);
|
|
1508
|
+
logger.log(`
|
|
1509
|
+
Next steps:`);
|
|
1510
|
+
logger.log(`1. Review the generated YAML file`);
|
|
1511
|
+
logger.log(`2. Commit and push to the marketplace repository`);
|
|
1512
|
+
logger.log(`3. Submit a pull request to add your plugin to the marketplace`);
|
|
1513
|
+
return {
|
|
1514
|
+
pluginId,
|
|
1515
|
+
name,
|
|
1516
|
+
author,
|
|
1517
|
+
description,
|
|
1518
|
+
version,
|
|
1519
|
+
registryUrl,
|
|
1520
|
+
repository: gitHubRepoUrl,
|
|
1521
|
+
minObsidianVersion,
|
|
1522
|
+
authorUrl,
|
|
1523
|
+
yamlPath
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
async function findMarketplaceDir() {
|
|
1527
|
+
let currentDir = process.cwd();
|
|
1528
|
+
const root = path3.parse(currentDir).root;
|
|
1529
|
+
while (currentDir !== root) {
|
|
1530
|
+
const marketplacePath = path3.join(currentDir, "marketplace");
|
|
1531
|
+
try {
|
|
1532
|
+
const stat3 = await fs3.stat(marketplacePath);
|
|
1533
|
+
if (stat3.isDirectory()) {
|
|
1534
|
+
return marketplacePath;
|
|
1535
|
+
}
|
|
1536
|
+
} catch {
|
|
1537
|
+
}
|
|
1538
|
+
currentDir = path3.dirname(currentDir);
|
|
1539
|
+
}
|
|
1540
|
+
throw new Error(
|
|
1541
|
+
"Could not find marketplace directory. Please run this command from within the marketplace repository."
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
async function marketplaceListCommand(opts) {
|
|
1545
|
+
const { logger, adapter, marketplaceUrl } = opts;
|
|
1546
|
+
const client = new MarketplaceClient(adapter, marketplaceUrl);
|
|
1547
|
+
logger.log("Fetching marketplace plugins...");
|
|
1548
|
+
const plugins = await client.fetchPlugins();
|
|
1549
|
+
logger.log(`
|
|
1550
|
+
Found ${plugins.length} plugins:
|
|
1551
|
+
`);
|
|
1552
|
+
for (const plugin of plugins) {
|
|
1553
|
+
logger.log(`${plugin.name} (${plugin.id})`);
|
|
1554
|
+
logger.log(` Author: ${plugin.author}`);
|
|
1555
|
+
logger.log(` Version: ${plugin.version}`);
|
|
1556
|
+
logger.log(` Registry: ${plugin.registryUrl}`);
|
|
1557
|
+
if (plugin.description) {
|
|
1558
|
+
logger.log(` Description: ${plugin.description}`);
|
|
1559
|
+
}
|
|
1560
|
+
logger.log("");
|
|
1561
|
+
}
|
|
1562
|
+
return plugins;
|
|
1563
|
+
}
|
|
1564
|
+
async function marketplaceSearchCommand(opts) {
|
|
1565
|
+
const { keyword, logger, adapter, marketplaceUrl } = opts;
|
|
1566
|
+
const client = new MarketplaceClient(adapter, marketplaceUrl);
|
|
1567
|
+
logger.log(`Searching for "${keyword}"...`);
|
|
1568
|
+
const plugins = await client.searchPlugins(keyword);
|
|
1569
|
+
if (plugins.length === 0) {
|
|
1570
|
+
logger.log(`
|
|
1571
|
+
No plugins found matching "${keyword}"`);
|
|
1572
|
+
return [];
|
|
1573
|
+
}
|
|
1574
|
+
logger.log(`
|
|
1575
|
+
Found ${plugins.length} matching plugin(s):
|
|
1576
|
+
`);
|
|
1577
|
+
for (const plugin of plugins) {
|
|
1578
|
+
logger.log(`${plugin.name} (${plugin.id})`);
|
|
1579
|
+
logger.log(` Author: ${plugin.author}`);
|
|
1580
|
+
logger.log(` Version: ${plugin.version}`);
|
|
1581
|
+
logger.log(` Registry: ${plugin.registryUrl}`);
|
|
1582
|
+
if (plugin.description) {
|
|
1583
|
+
logger.log(` Description: ${plugin.description}`);
|
|
1584
|
+
}
|
|
1585
|
+
logger.log("");
|
|
1586
|
+
}
|
|
1587
|
+
return plugins;
|
|
1588
|
+
}
|
|
1589
|
+
async function marketplaceInfoCommand(opts) {
|
|
1590
|
+
const { pluginId, logger, adapter, marketplaceUrl } = opts;
|
|
1591
|
+
const client = new MarketplaceClient(adapter, marketplaceUrl);
|
|
1592
|
+
logger.log(`Fetching plugin "${pluginId}"...`);
|
|
1593
|
+
const plugin = await client.findPluginById(pluginId);
|
|
1594
|
+
if (!plugin) {
|
|
1595
|
+
logger.error(`Plugin "${pluginId}" not found in marketplace`);
|
|
1596
|
+
return null;
|
|
1597
|
+
}
|
|
1598
|
+
logger.log("\n" + "=".repeat(60));
|
|
1599
|
+
logger.log(`Plugin: ${plugin.name}`);
|
|
1600
|
+
logger.log("=".repeat(60) + "\n");
|
|
1601
|
+
logger.log(`ID: ${plugin.id}`);
|
|
1602
|
+
logger.log(`Version: ${plugin.version}`);
|
|
1603
|
+
logger.log(`Author: ${plugin.author}`);
|
|
1604
|
+
if (plugin.authorUrl) {
|
|
1605
|
+
logger.log(`Author URL: ${plugin.authorUrl}`);
|
|
1606
|
+
}
|
|
1607
|
+
logger.log(`Description: ${plugin.description}`);
|
|
1608
|
+
logger.log(`
|
|
1609
|
+
Registry URL: ${plugin.registryUrl}`);
|
|
1610
|
+
if (plugin.repository) {
|
|
1611
|
+
logger.log(`Repository: ${plugin.repository}`);
|
|
1612
|
+
}
|
|
1613
|
+
if (plugin.license) {
|
|
1614
|
+
logger.log(`License: ${plugin.license}`);
|
|
1615
|
+
}
|
|
1616
|
+
if (plugin.minObsidianVersion) {
|
|
1617
|
+
logger.log(`Min Obsidian Version: ${plugin.minObsidianVersion}`);
|
|
1618
|
+
}
|
|
1619
|
+
if (plugin.tags && plugin.tags.length > 0) {
|
|
1620
|
+
logger.log(`Tags: ${plugin.tags.join(", ")}`);
|
|
1621
|
+
}
|
|
1622
|
+
logger.log(`Last Updated: ${plugin.updatedAt}`);
|
|
1623
|
+
logger.log("\n" + "=".repeat(60));
|
|
1624
|
+
logger.log("Installation:");
|
|
1625
|
+
logger.log("=".repeat(60) + "\n");
|
|
1626
|
+
logger.log(`shard pull ${plugin.registryUrl}:${plugin.version} --output <path>`);
|
|
1627
|
+
logger.log(
|
|
1628
|
+
`shard marketplace install ${plugin.id} # (coming soon)`
|
|
1629
|
+
);
|
|
1630
|
+
return plugin;
|
|
1631
|
+
}
|
|
1632
|
+
async function marketplaceInstallCommand(opts) {
|
|
1633
|
+
const { pluginId, output, version, token, logger, adapter, marketplaceUrl } = opts;
|
|
1634
|
+
const client = new MarketplaceClient(adapter, marketplaceUrl);
|
|
1635
|
+
logger.log(`Looking up plugin "${pluginId}" in marketplace...`);
|
|
1636
|
+
const plugin = await client.findPluginById(pluginId);
|
|
1637
|
+
if (!plugin) {
|
|
1638
|
+
throw new Error(`Plugin "${pluginId}" not found in marketplace`);
|
|
1639
|
+
}
|
|
1640
|
+
logger.log(`Found: ${plugin.name} v${plugin.version} by ${plugin.author}`);
|
|
1641
|
+
const versionToInstall = version || plugin.version;
|
|
1642
|
+
const repository = `${plugin.registryUrl}:${versionToInstall}`;
|
|
1643
|
+
logger.log(`Installing from ${repository}...`);
|
|
1644
|
+
const pullResult = await pullCommand({
|
|
1645
|
+
repository,
|
|
1646
|
+
output,
|
|
1647
|
+
token,
|
|
1648
|
+
logger,
|
|
1649
|
+
adapter
|
|
1650
|
+
});
|
|
1651
|
+
logger.success(
|
|
1652
|
+
`Successfully installed ${plugin.name} v${versionToInstall} to ${output}`
|
|
1653
|
+
);
|
|
1654
|
+
return { plugin, pullResult };
|
|
1655
|
+
}
|
|
1656
|
+
async function marketplaceUpdateCommand(opts) {
|
|
1657
|
+
const { logger } = opts;
|
|
1658
|
+
logger.log("Updating marketplace entry...");
|
|
1659
|
+
logger.log(
|
|
1660
|
+
"Note: This will overwrite the existing YAML file with fresh metadata from GHCR\n"
|
|
1661
|
+
);
|
|
1662
|
+
return marketplaceRegisterCommand(opts);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/lib/auth.ts
|
|
1666
|
+
function resolveAuthToken(cliToken) {
|
|
1667
|
+
if (cliToken) {
|
|
1668
|
+
return cliToken;
|
|
1669
|
+
}
|
|
1670
|
+
const githubToken = process.env.GITHUB_TOKEN;
|
|
1671
|
+
if (githubToken) {
|
|
1672
|
+
return githubToken;
|
|
1673
|
+
}
|
|
1674
|
+
const ghToken = process.env.GH_TOKEN;
|
|
1675
|
+
if (ghToken) {
|
|
1676
|
+
return ghToken;
|
|
1677
|
+
}
|
|
1678
|
+
throw new Error(
|
|
1679
|
+
"GitHub token required. Use --token flag or set GITHUB_TOKEN environment variable"
|
|
1680
|
+
);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
// src/lib/logger.ts
|
|
1684
|
+
var Logger = class {
|
|
1685
|
+
silent;
|
|
1686
|
+
constructor(silent = false) {
|
|
1687
|
+
this.silent = silent;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Log a message to stderr
|
|
1691
|
+
*/
|
|
1692
|
+
log(message) {
|
|
1693
|
+
if (!this.silent) {
|
|
1694
|
+
process.stderr.write(message + "\n");
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Log an error message to stderr
|
|
1699
|
+
*/
|
|
1700
|
+
error(message) {
|
|
1701
|
+
if (!this.silent) {
|
|
1702
|
+
process.stderr.write(`Error: ${message}
|
|
1703
|
+
`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Log a success message to stderr
|
|
1708
|
+
*/
|
|
1709
|
+
success(message) {
|
|
1710
|
+
if (!this.silent) {
|
|
1711
|
+
process.stderr.write(`Success: ${message}
|
|
1712
|
+
`);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
|
|
1717
|
+
// src/adapters/node-fetch-adapter.ts
|
|
1718
|
+
var NodeFetchAdapter = class {
|
|
1719
|
+
async fetch(input, init) {
|
|
1720
|
+
return fetch(input, init);
|
|
1721
|
+
}
|
|
1722
|
+
};
|
|
1723
|
+
|
|
1724
|
+
// src/index.ts
|
|
1725
|
+
var USAGE = `
|
|
1726
|
+
Usage: shard <command> [options]
|
|
1727
|
+
|
|
1728
|
+
Commands:
|
|
1729
|
+
push <directory> <repository> Push a plugin to GHCR
|
|
1730
|
+
pull <repository> Pull a plugin from GHCR
|
|
1731
|
+
convert <plugin-id> <repository> Convert legacy plugin to OCI format
|
|
1732
|
+
marketplace <subcommand> Manage marketplace plugins
|
|
1733
|
+
|
|
1734
|
+
Marketplace Subcommands:
|
|
1735
|
+
list List all marketplace plugins
|
|
1736
|
+
search <keyword> Search for plugins
|
|
1737
|
+
info <plugin-id> Show detailed plugin information
|
|
1738
|
+
install <plugin-id> Install a plugin by ID
|
|
1739
|
+
register <repository> Register plugin to marketplace
|
|
1740
|
+
update <repository> Update marketplace entry
|
|
1741
|
+
|
|
1742
|
+
Push Options:
|
|
1743
|
+
<directory> Path to plugin build output (e.g., ./dist)
|
|
1744
|
+
<repository> GHCR repository (e.g., ghcr.io/user/plugin)
|
|
1745
|
+
--token <pat> GitHub Personal Access Token
|
|
1746
|
+
--json Output JSON result to stdout
|
|
1747
|
+
--help Show help
|
|
1748
|
+
|
|
1749
|
+
Pull Options:
|
|
1750
|
+
<repository> Full reference with tag (e.g., ghcr.io/user/plugin:1.0.0)
|
|
1751
|
+
--output <dir> Where to extract files (required)
|
|
1752
|
+
--token <pat> GitHub Personal Access Token
|
|
1753
|
+
--json Output JSON result to stdout
|
|
1754
|
+
--help Show help
|
|
1755
|
+
|
|
1756
|
+
Convert Options:
|
|
1757
|
+
<plugin-id> Plugin ID from community list (e.g., obsidian-git)
|
|
1758
|
+
<repository> GHCR repository (e.g., ghcr.io/user/plugin)
|
|
1759
|
+
--version <version> Specific version to convert (defaults to latest)
|
|
1760
|
+
--token <pat> GitHub Personal Access Token
|
|
1761
|
+
--json Output JSON result to stdout
|
|
1762
|
+
--help Show help
|
|
1763
|
+
|
|
1764
|
+
Marketplace Options:
|
|
1765
|
+
--output <dir> Output directory for install command
|
|
1766
|
+
--version <version> Specific version to install (defaults to latest)
|
|
1767
|
+
--token <pat> GitHub Personal Access Token
|
|
1768
|
+
--json Output JSON result to stdout
|
|
1769
|
+
--help Show help
|
|
1770
|
+
|
|
1771
|
+
Environment Variables:
|
|
1772
|
+
GITHUB_TOKEN GitHub token (alternative to --token)
|
|
1773
|
+
GH_TOKEN GitHub token (gh CLI compatibility)
|
|
1774
|
+
|
|
1775
|
+
Examples:
|
|
1776
|
+
shard push ./dist ghcr.io/user/my-plugin
|
|
1777
|
+
shard pull ghcr.io/user/my-plugin:1.0.0 --output ./plugin
|
|
1778
|
+
shard convert obsidian-git ghcr.io/user/obsidian-git
|
|
1779
|
+
shard convert calendar ghcr.io/user/calendar --version 1.5.3
|
|
1780
|
+
shard marketplace list
|
|
1781
|
+
shard marketplace search "calendar"
|
|
1782
|
+
shard marketplace info obsidian-git
|
|
1783
|
+
shard marketplace install obsidian-git --output ./plugins/obsidian-git
|
|
1784
|
+
shard marketplace register ghcr.io/user/my-plugin:1.0.0
|
|
1785
|
+
shard marketplace update ghcr.io/user/my-plugin:1.0.1
|
|
1786
|
+
`;
|
|
1787
|
+
async function main() {
|
|
1788
|
+
let args;
|
|
1789
|
+
try {
|
|
1790
|
+
args = parseArgs({
|
|
1791
|
+
options: {
|
|
1792
|
+
token: { type: "string" },
|
|
1793
|
+
json: { type: "boolean", default: false },
|
|
1794
|
+
help: { type: "boolean", default: false },
|
|
1795
|
+
output: { type: "string" },
|
|
1796
|
+
version: { type: "string" }
|
|
1797
|
+
},
|
|
1798
|
+
allowPositionals: true
|
|
1799
|
+
});
|
|
1800
|
+
} catch (err) {
|
|
1801
|
+
console.error(
|
|
1802
|
+
`Error parsing arguments: ${err instanceof Error ? err.message : String(err)}`
|
|
1803
|
+
);
|
|
1804
|
+
console.error(USAGE);
|
|
1805
|
+
process.exit(1);
|
|
1806
|
+
}
|
|
1807
|
+
if (args.values.help || args.positionals.length === 0) {
|
|
1808
|
+
console.log(USAGE);
|
|
1809
|
+
process.exit(0);
|
|
1810
|
+
}
|
|
1811
|
+
const command = args.positionals[0];
|
|
1812
|
+
const logger = new Logger(args.values.json);
|
|
1813
|
+
const adapter = new NodeFetchAdapter();
|
|
1814
|
+
try {
|
|
1815
|
+
if (command === "push") {
|
|
1816
|
+
if (args.positionals.length < 3) {
|
|
1817
|
+
throw new Error("Push command requires <directory> and <repository>");
|
|
1818
|
+
}
|
|
1819
|
+
const directory = args.positionals[1];
|
|
1820
|
+
const repository = args.positionals[2];
|
|
1821
|
+
const token = resolveAuthToken(args.values.token);
|
|
1822
|
+
const result = await pushCommand({
|
|
1823
|
+
directory,
|
|
1824
|
+
repository,
|
|
1825
|
+
token,
|
|
1826
|
+
logger,
|
|
1827
|
+
adapter
|
|
1828
|
+
});
|
|
1829
|
+
if (args.values.json) {
|
|
1830
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1831
|
+
}
|
|
1832
|
+
process.exit(0);
|
|
1833
|
+
} else if (command === "pull") {
|
|
1834
|
+
if (args.positionals.length < 2) {
|
|
1835
|
+
throw new Error("Pull command requires <repository>");
|
|
1836
|
+
}
|
|
1837
|
+
if (!args.values.output) {
|
|
1838
|
+
throw new Error("Pull command requires --output flag");
|
|
1839
|
+
}
|
|
1840
|
+
const repository = args.positionals[1];
|
|
1841
|
+
const output = args.values.output;
|
|
1842
|
+
const token = resolveAuthToken(args.values.token);
|
|
1843
|
+
const result = await pullCommand({
|
|
1844
|
+
repository,
|
|
1845
|
+
output,
|
|
1846
|
+
token,
|
|
1847
|
+
logger,
|
|
1848
|
+
adapter
|
|
1849
|
+
});
|
|
1850
|
+
if (args.values.json) {
|
|
1851
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1852
|
+
}
|
|
1853
|
+
process.exit(0);
|
|
1854
|
+
} else if (command === "convert") {
|
|
1855
|
+
if (args.positionals.length < 3) {
|
|
1856
|
+
throw new Error(
|
|
1857
|
+
"Convert command requires <plugin-id> and <repository>"
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
const pluginId = args.positionals[1];
|
|
1861
|
+
const repository = args.positionals[2];
|
|
1862
|
+
const version = args.values.version;
|
|
1863
|
+
const token = resolveAuthToken(args.values.token);
|
|
1864
|
+
const result = await convertCommand({
|
|
1865
|
+
pluginId,
|
|
1866
|
+
repository,
|
|
1867
|
+
version,
|
|
1868
|
+
token,
|
|
1869
|
+
logger,
|
|
1870
|
+
adapter
|
|
1871
|
+
});
|
|
1872
|
+
if (args.values.json) {
|
|
1873
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1874
|
+
}
|
|
1875
|
+
process.exit(0);
|
|
1876
|
+
} else if (command === "marketplace") {
|
|
1877
|
+
const subcommand = args.positionals[1];
|
|
1878
|
+
if (!subcommand) {
|
|
1879
|
+
throw new Error(
|
|
1880
|
+
"Marketplace command requires a subcommand. Available: list, search, info, install, register, update"
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
const token = resolveAuthToken(args.values.token);
|
|
1884
|
+
if (subcommand === "list") {
|
|
1885
|
+
const result = await marketplaceListCommand({
|
|
1886
|
+
logger,
|
|
1887
|
+
adapter
|
|
1888
|
+
});
|
|
1889
|
+
if (args.values.json) {
|
|
1890
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1891
|
+
}
|
|
1892
|
+
process.exit(0);
|
|
1893
|
+
} else if (subcommand === "search") {
|
|
1894
|
+
if (args.positionals.length < 3) {
|
|
1895
|
+
throw new Error("Marketplace search command requires <keyword>");
|
|
1896
|
+
}
|
|
1897
|
+
const keyword = args.positionals[2];
|
|
1898
|
+
const result = await marketplaceSearchCommand({
|
|
1899
|
+
keyword,
|
|
1900
|
+
logger,
|
|
1901
|
+
adapter
|
|
1902
|
+
});
|
|
1903
|
+
if (args.values.json) {
|
|
1904
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1905
|
+
}
|
|
1906
|
+
process.exit(0);
|
|
1907
|
+
} else if (subcommand === "info") {
|
|
1908
|
+
if (args.positionals.length < 3) {
|
|
1909
|
+
throw new Error("Marketplace info command requires <plugin-id>");
|
|
1910
|
+
}
|
|
1911
|
+
const pluginId = args.positionals[2];
|
|
1912
|
+
const result = await marketplaceInfoCommand({
|
|
1913
|
+
pluginId,
|
|
1914
|
+
logger,
|
|
1915
|
+
adapter
|
|
1916
|
+
});
|
|
1917
|
+
if (args.values.json) {
|
|
1918
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1919
|
+
}
|
|
1920
|
+
process.exit(0);
|
|
1921
|
+
} else if (subcommand === "install") {
|
|
1922
|
+
if (args.positionals.length < 3) {
|
|
1923
|
+
throw new Error("Marketplace install command requires <plugin-id>");
|
|
1924
|
+
}
|
|
1925
|
+
if (!args.values.output) {
|
|
1926
|
+
throw new Error("Marketplace install command requires --output flag");
|
|
1927
|
+
}
|
|
1928
|
+
const pluginId = args.positionals[2];
|
|
1929
|
+
const output = args.values.output;
|
|
1930
|
+
const version = args.values.version;
|
|
1931
|
+
const result = await marketplaceInstallCommand({
|
|
1932
|
+
pluginId,
|
|
1933
|
+
output,
|
|
1934
|
+
version,
|
|
1935
|
+
token,
|
|
1936
|
+
logger,
|
|
1937
|
+
adapter
|
|
1938
|
+
});
|
|
1939
|
+
if (args.values.json) {
|
|
1940
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1941
|
+
}
|
|
1942
|
+
process.exit(0);
|
|
1943
|
+
} else if (subcommand === "register") {
|
|
1944
|
+
if (args.positionals.length < 3) {
|
|
1945
|
+
throw new Error("Marketplace register command requires <repository>");
|
|
1946
|
+
}
|
|
1947
|
+
const repository = args.positionals[2];
|
|
1948
|
+
const result = await marketplaceRegisterCommand({
|
|
1949
|
+
repository,
|
|
1950
|
+
token,
|
|
1951
|
+
logger,
|
|
1952
|
+
adapter
|
|
1953
|
+
});
|
|
1954
|
+
if (args.values.json) {
|
|
1955
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1956
|
+
}
|
|
1957
|
+
process.exit(0);
|
|
1958
|
+
} else if (subcommand === "update") {
|
|
1959
|
+
if (args.positionals.length < 3) {
|
|
1960
|
+
throw new Error("Marketplace update command requires <repository>");
|
|
1961
|
+
}
|
|
1962
|
+
const repository = args.positionals[2];
|
|
1963
|
+
const result = await marketplaceUpdateCommand({
|
|
1964
|
+
repository,
|
|
1965
|
+
token,
|
|
1966
|
+
logger,
|
|
1967
|
+
adapter
|
|
1968
|
+
});
|
|
1969
|
+
if (args.values.json) {
|
|
1970
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1971
|
+
}
|
|
1972
|
+
process.exit(0);
|
|
1973
|
+
} else {
|
|
1974
|
+
throw new Error(
|
|
1975
|
+
`Unknown marketplace subcommand: ${subcommand}. Available: list, search, info, install, register, update`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
} else {
|
|
1979
|
+
throw new Error(`Unknown command: ${command}`);
|
|
1980
|
+
}
|
|
1981
|
+
} catch (err) {
|
|
1982
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
1983
|
+
if (args.values.json) {
|
|
1984
|
+
console.log(
|
|
1985
|
+
JSON.stringify(
|
|
1986
|
+
{
|
|
1987
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1988
|
+
},
|
|
1989
|
+
null,
|
|
1990
|
+
2
|
|
1991
|
+
)
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
process.exit(1);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
void main();
|
|
1998
|
+
//# sourceMappingURL=index.js.map
|