ovsx 1.1.0 → 1.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/README.md +71 -1
- package/lib/get.js +1 -1
- package/lib/get.js.map +1 -1
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -0
- package/lib/index.js.map +1 -1
- package/lib/list-options.d.ts +24 -0
- package/lib/list-options.d.ts.map +1 -0
- package/lib/list-options.js +15 -0
- package/lib/list-options.js.map +1 -0
- package/lib/list.d.ts +18 -0
- package/lib/list.d.ts.map +1 -0
- package/lib/list.js +46 -0
- package/lib/list.js.map +1 -0
- package/lib/main.js +78 -2
- package/lib/main.js.map +1 -1
- package/lib/oidc.d.ts +22 -0
- package/lib/oidc.d.ts.map +1 -0
- package/lib/oidc.js +101 -0
- package/lib/oidc.js.map +1 -0
- package/lib/publish-options.d.ts +8 -1
- package/lib/publish-options.d.ts.map +1 -1
- package/lib/publish.d.ts.map +1 -1
- package/lib/publish.js +73 -4
- package/lib/publish.js.map +1 -1
- package/lib/registry-options.d.ts +8 -0
- package/lib/registry-options.d.ts.map +1 -1
- package/lib/registry.d.ts +119 -1
- package/lib/registry.d.ts.map +1 -1
- package/lib/registry.js +187 -19
- package/lib/registry.js.map +1 -1
- package/lib/search-options.d.ts +48 -0
- package/lib/search-options.d.ts.map +1 -0
- package/lib/search-options.js +15 -0
- package/lib/search-options.js.map +1 -0
- package/lib/search.d.ts +22 -0
- package/lib/search.d.ts.map +1 -0
- package/lib/search.js +86 -0
- package/lib/search.js.map +1 -0
- package/lib/show-options.d.ts +33 -0
- package/lib/show-options.d.ts.map +1 -0
- package/lib/show-options.js +15 -0
- package/lib/show-options.js.map +1 -0
- package/lib/show.d.ts +18 -0
- package/lib/show.d.ts.map +1 -0
- package/lib/show.js +289 -0
- package/lib/show.js.map +1 -0
- package/lib/table.d.ts +27 -0
- package/lib/table.d.ts.map +1 -0
- package/lib/table.js +56 -0
- package/lib/table.js.map +1 -0
- package/lib/trusted-publishing-options.d.ts +30 -0
- package/lib/trusted-publishing-options.d.ts.map +1 -0
- package/lib/trusted-publishing-options.js +15 -0
- package/lib/trusted-publishing-options.js.map +1 -0
- package/lib/trusted-publishing.d.ts +35 -0
- package/lib/trusted-publishing.d.ts.map +1 -0
- package/lib/trusted-publishing.js +112 -0
- package/lib/trusted-publishing.js.map +1 -0
- package/lib/unpublish-options.d.ts +34 -0
- package/lib/unpublish-options.d.ts.map +1 -0
- package/lib/unpublish-options.js +15 -0
- package/lib/unpublish-options.js.map +1 -0
- package/lib/unpublish.d.ts +22 -0
- package/lib/unpublish.d.ts.map +1 -0
- package/lib/unpublish.js +137 -0
- package/lib/unpublish.js.map +1 -0
- package/lib/util.d.ts +39 -1
- package/lib/util.d.ts.map +1 -1
- package/lib/util.js +91 -5
- package/lib/util.js.map +1 -1
- package/lib/verify-options.d.ts +24 -0
- package/lib/verify-options.d.ts.map +1 -0
- package/lib/verify-options.js +15 -0
- package/lib/verify-options.js.map +1 -0
- package/lib/verify-signature-options.d.ts +32 -0
- package/lib/verify-signature-options.d.ts.map +1 -0
- package/lib/verify-signature-options.js +15 -0
- package/lib/verify-signature-options.js.map +1 -0
- package/lib/verify-signature.d.ts +28 -0
- package/lib/verify-signature.d.ts.map +1 -0
- package/lib/verify-signature.js +105 -0
- package/lib/verify-signature.js.map +1 -0
- package/lib/verify.d.ts +21 -0
- package/lib/verify.d.ts.map +1 -0
- package/lib/verify.js +97 -0
- package/lib/verify.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/lib/zip.d.ts +10 -0
- package/lib/zip.d.ts.map +1 -1
- package/lib/zip.js +31 -0
- package/lib/zip.js.map +1 -1
- package/package.json +9 -5
- package/src/get.ts +1 -1
- package/src/index.ts +4 -0
- package/src/list-options.ts +25 -0
- package/src/list.ts +48 -0
- package/src/main.ts +89 -3
- package/src/oidc.ts +107 -0
- package/src/publish-options.ts +8 -1
- package/src/publish.ts +97 -5
- package/src/registry-options.ts +8 -0
- package/src/registry.ts +292 -19
- package/src/search-options.ts +49 -0
- package/src/search.ts +95 -0
- package/src/show-options.ts +34 -0
- package/src/show.ts +328 -0
- package/src/table.ts +53 -0
- package/src/trusted-publishing-options.ts +30 -0
- package/src/trusted-publishing.ts +148 -0
- package/src/unpublish-options.ts +35 -0
- package/src/unpublish.ts +160 -0
- package/src/util.ts +102 -5
- package/src/verify-options.ts +25 -0
- package/src/verify-signature-options.ts +32 -0
- package/src/verify-signature.ts +124 -0
- package/src/verify.ts +107 -0
- package/src/version.ts +1 -1
- package/src/zip.ts +31 -1
package/src/registry.ts
CHANGED
|
@@ -10,19 +10,24 @@
|
|
|
10
10
|
|
|
11
11
|
import * as http from 'http';
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import { pipeline, Writable } from 'stream';
|
|
13
14
|
import * as followRedirects from 'follow-redirects';
|
|
14
15
|
import { RegistryOptions } from './registry-options';
|
|
15
|
-
import { rejectError, statusError } from './util';
|
|
16
|
+
import { DEFAULT_TIMEOUT, redactUrl, rejectError, statusError, withStatus } from './util';
|
|
16
17
|
|
|
17
18
|
export const DEFAULT_URL = 'https://open-vsx.org';
|
|
18
19
|
export const DEFAULT_NAMESPACE_SIZE = 1024;
|
|
19
20
|
export const DEFAULT_PUBLISH_SIZE = 512 * 1024 * 1024;
|
|
21
|
+
export { DEFAULT_TIMEOUT };
|
|
22
|
+
export const DEFAULT_TOKEN_REQUEST_SIZE = 8 * 1024;
|
|
23
|
+
export const DEFAULT_DELETE_SIZE = 64 * 1024;
|
|
20
24
|
|
|
21
25
|
export class Registry {
|
|
22
26
|
|
|
23
27
|
readonly url: string;
|
|
24
28
|
readonly maxNamespaceSize: number;
|
|
25
29
|
readonly maxPublishSize: number;
|
|
30
|
+
readonly timeout: number;
|
|
26
31
|
readonly username?: string;
|
|
27
32
|
readonly password?: string;
|
|
28
33
|
|
|
@@ -36,6 +41,7 @@ export class Registry {
|
|
|
36
41
|
|
|
37
42
|
this.maxNamespaceSize = options.maxNamespaceSize ?? DEFAULT_NAMESPACE_SIZE;
|
|
38
43
|
this.maxPublishSize = options.maxPublishSize ?? DEFAULT_PUBLISH_SIZE;
|
|
44
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
39
45
|
this.username = options.username;
|
|
40
46
|
this.password = options.password;
|
|
41
47
|
}
|
|
@@ -65,6 +71,14 @@ export class Registry {
|
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
|
|
74
|
+
getRegistryVersion(): Promise<RegistryVersion> {
|
|
75
|
+
try {
|
|
76
|
+
return this.getJson(this.getUrl(['api', 'version']));
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return rejectError(err);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
publish(file: string, pat: string): Promise<Extension> {
|
|
69
83
|
try {
|
|
70
84
|
const url = this.getUrl(['api', '-', 'publish'], { token: pat });
|
|
@@ -76,41 +90,180 @@ export class Registry {
|
|
|
76
90
|
}
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
|
|
93
|
+
requestTrustedPublishingToken(namespace: string, extension: string, idToken: string): Promise<AccessToken> {
|
|
94
|
+
try {
|
|
95
|
+
const url = this.getUrl(['api', '-', 'trusted-publishing', 'token']);
|
|
96
|
+
const request = { namespace, extension, token: idToken };
|
|
97
|
+
return this.post(JSON.stringify(request), url, {
|
|
98
|
+
'Content-Type': 'application/json'
|
|
99
|
+
}, DEFAULT_TOKEN_REQUEST_SIZE);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
return rejectError(err);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Deletes extension versions. Omitting `targetVersions` deletes the extension as a whole,
|
|
107
|
+
* i.e. all versions the personal access token's user is allowed to delete.
|
|
108
|
+
*/
|
|
109
|
+
deleteExtension(
|
|
110
|
+
namespace: string,
|
|
111
|
+
extension: string,
|
|
112
|
+
targetVersions: TargetPlatformVersion[] | undefined,
|
|
113
|
+
pat: string
|
|
114
|
+
): Promise<Response> {
|
|
115
|
+
try {
|
|
116
|
+
if (!targetVersions) {
|
|
117
|
+
const url = this.getUrl(['api', namespace, extension, 'delete'], { token: pat, allVersions: 'true' });
|
|
118
|
+
return this.post('', url, undefined, DEFAULT_DELETE_SIZE);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const url = this.getUrl(['api', namespace, extension, 'delete'], { token: pat });
|
|
122
|
+
return this.post(JSON.stringify(targetVersions), url, {
|
|
123
|
+
'Content-Type': 'application/json'
|
|
124
|
+
}, DEFAULT_DELETE_SIZE);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
return rejectError(err);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getMetadata(namespace: string, extension: string, target?: string, version?: string): Promise<Extension> {
|
|
80
131
|
try {
|
|
81
132
|
const segments = ['api', namespace, extension];
|
|
82
133
|
if (target) {
|
|
83
134
|
segments.push(target);
|
|
84
135
|
}
|
|
136
|
+
if (version) {
|
|
137
|
+
segments.push(version);
|
|
138
|
+
}
|
|
85
139
|
return this.getJson(this.getUrl(segments));
|
|
86
140
|
} catch (err) {
|
|
87
141
|
return rejectError(err);
|
|
88
142
|
}
|
|
89
143
|
}
|
|
90
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Returns a page of an extension's published versions, newest first, one entry per version and
|
|
147
|
+
* target platform. `allVersions` on the metadata response carries version numbers and links
|
|
148
|
+
* only, so this is what makes the target platforms of each version available.
|
|
149
|
+
*/
|
|
150
|
+
getVersionReferences(
|
|
151
|
+
namespace: string,
|
|
152
|
+
extension: string,
|
|
153
|
+
target: string | undefined,
|
|
154
|
+
size: number,
|
|
155
|
+
offset: number
|
|
156
|
+
): Promise<VersionReferences> {
|
|
157
|
+
try {
|
|
158
|
+
const segments = ['api', namespace, extension];
|
|
159
|
+
if (target) {
|
|
160
|
+
segments.push(target);
|
|
161
|
+
}
|
|
162
|
+
segments.push('version-references');
|
|
163
|
+
return this.getJson(this.getUrl(segments, {
|
|
164
|
+
size: String(size),
|
|
165
|
+
offset: String(offset)
|
|
166
|
+
}));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return rejectError(err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Full-text search across the registry. Returns the purpose-built summary shape rather than
|
|
174
|
+
* whole extension records, so a page of results stays small.
|
|
175
|
+
*/
|
|
176
|
+
search(options: SearchQuery): Promise<SearchResult> {
|
|
177
|
+
try {
|
|
178
|
+
const query: Record<string, string> = {
|
|
179
|
+
size: String(options.size),
|
|
180
|
+
offset: String(options.offset)
|
|
181
|
+
};
|
|
182
|
+
if (options.query) {
|
|
183
|
+
query.query = options.query;
|
|
184
|
+
}
|
|
185
|
+
if (options.category) {
|
|
186
|
+
query.category = options.category;
|
|
187
|
+
}
|
|
188
|
+
if (options.targetPlatform) {
|
|
189
|
+
query.targetPlatform = options.targetPlatform;
|
|
190
|
+
}
|
|
191
|
+
if (options.sortBy) {
|
|
192
|
+
query.sortBy = options.sortBy;
|
|
193
|
+
}
|
|
194
|
+
if (options.sortOrder) {
|
|
195
|
+
query.sortOrder = options.sortOrder;
|
|
196
|
+
}
|
|
197
|
+
return this.getJson(this.getUrl(['api', '-', 'search'], query));
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return rejectError(err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Returns a namespace and the extensions published in it. */
|
|
204
|
+
getNamespace(namespace: string): Promise<Namespace> {
|
|
205
|
+
try {
|
|
206
|
+
return this.getJson(this.getUrl(['api', namespace]));
|
|
207
|
+
} catch (err) {
|
|
208
|
+
return rejectError(err);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
91
212
|
download(file: string, url: URL): Promise<void> {
|
|
92
213
|
return new Promise((resolve, reject) => {
|
|
93
|
-
|
|
214
|
+
// Written beside the target and renamed into place on success, so the caller's path holds
|
|
215
|
+
// either what it held before or the whole download, never part of one. Deferring the open
|
|
216
|
+
// until the status is known is not enough on its own: a connection dropped mid-body has
|
|
217
|
+
// already truncated the file by then, and `get` is handed a path the user chose.
|
|
218
|
+
const partial = `${file}.part`;
|
|
219
|
+
let stream: fs.WriteStream | undefined;
|
|
220
|
+
|
|
221
|
+
// Claimed synchronously, before the cleanup that has to happen before rejecting. A timeout
|
|
222
|
+
// mid-body drives both the request's error handler and pipeline's callback, and each used
|
|
223
|
+
// to wait on its own fs.rm - so whichever landed first decided what the caller was told,
|
|
224
|
+
// and the partial was removed twice.
|
|
225
|
+
let failed = false;
|
|
226
|
+
const fail = (err: Error) => {
|
|
227
|
+
if (failed) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
failed = true;
|
|
231
|
+
stream?.destroy();
|
|
232
|
+
fs.rm(partial, { force: true }, () => reject(err));
|
|
233
|
+
};
|
|
234
|
+
|
|
94
235
|
const requestOptions = this.getRequestOptions();
|
|
95
236
|
const request = this.getProtocol(url)
|
|
96
237
|
.request(url, requestOptions, response => {
|
|
97
|
-
response.
|
|
98
|
-
|
|
99
|
-
|
|
238
|
+
if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
|
|
239
|
+
response.resume();
|
|
240
|
+
reject(statusError(response));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
stream = fs.createWriteStream(partial);
|
|
245
|
+
|
|
246
|
+
// pipeline rather than response.pipe: pipe installs its own error handler on the
|
|
247
|
+
// source, so a connection dropped mid-body is swallowed - the write stream is never
|
|
248
|
+
// ended, nothing settles, and the caller waits for a file that will never arrive.
|
|
249
|
+
// pipeline propagates that error and tears both ends down. Its callback also waits
|
|
250
|
+
// for the file to close, which matters in its own right: a write stream opens and
|
|
251
|
+
// flushes asynchronously, so the last byte having arrived says nothing about the
|
|
252
|
+
// file being on disk.
|
|
253
|
+
pipeline(response, stream, (err: NodeJS.ErrnoException | null) => {
|
|
254
|
+
if (err) {
|
|
255
|
+
fail(err);
|
|
256
|
+
} else if (!response.complete) {
|
|
257
|
+
// A body cut short still ends the stream cleanly; `complete` is what tells
|
|
258
|
+
// that apart from having received all of it.
|
|
259
|
+
fail(new Error(`The connection closed before the whole of ${redactUrl(url)} was received.`));
|
|
100
260
|
} else {
|
|
101
|
-
resolve();
|
|
261
|
+
fs.rename(partial, file, renameErr => renameErr ? fail(renameErr) : resolve());
|
|
102
262
|
}
|
|
103
263
|
});
|
|
104
|
-
response.pipe(stream);
|
|
105
|
-
});
|
|
106
|
-
stream.on('error', (err: Error) => {
|
|
107
|
-
request.abort();
|
|
108
|
-
reject(err);
|
|
109
|
-
});
|
|
110
|
-
request.on('error', (err: Error) => {
|
|
111
|
-
stream.close();
|
|
112
|
-
reject(err);
|
|
113
264
|
});
|
|
265
|
+
request.on('error', (err: Error) => fail(err));
|
|
266
|
+
this.failOnTimeout(request, url, fail);
|
|
114
267
|
request.end();
|
|
115
268
|
});
|
|
116
269
|
}
|
|
@@ -121,6 +274,7 @@ export class Registry {
|
|
|
121
274
|
const request = this.getProtocol(url)
|
|
122
275
|
.request(url, requestOptions, this.getJsonResponse<T>(resolve, reject));
|
|
123
276
|
request.on('error', reject);
|
|
277
|
+
this.failOnTimeout(request, url, reject);
|
|
124
278
|
request.end();
|
|
125
279
|
});
|
|
126
280
|
}
|
|
@@ -131,6 +285,7 @@ export class Registry {
|
|
|
131
285
|
const request = this.getProtocol(url)
|
|
132
286
|
.request(url, requestOptions, this.getJsonResponse<T>(resolve, reject));
|
|
133
287
|
request.on('error', reject);
|
|
288
|
+
this.failOnTimeout(request, url, reject);
|
|
134
289
|
request.write(content);
|
|
135
290
|
request.end();
|
|
136
291
|
});
|
|
@@ -143,13 +298,14 @@ export class Registry {
|
|
|
143
298
|
const request = this.getProtocol(url)
|
|
144
299
|
.request(url, requestOptions, this.getJsonResponse<T>(resolve, reject));
|
|
145
300
|
stream.on('error', (err: Error) => {
|
|
146
|
-
request.
|
|
301
|
+
request.destroy();
|
|
147
302
|
reject(err);
|
|
148
303
|
});
|
|
149
304
|
request.on('error', (err: Error) => {
|
|
150
305
|
stream.close();
|
|
151
306
|
reject(err);
|
|
152
307
|
});
|
|
308
|
+
this.failOnTimeout(request, url, reject);
|
|
153
309
|
stream.on('open', () => stream.pipe(request));
|
|
154
310
|
});
|
|
155
311
|
}
|
|
@@ -169,6 +325,28 @@ export class Registry {
|
|
|
169
325
|
return url.protocol === 'https:' ? followRedirects.https : followRedirects.http;
|
|
170
326
|
}
|
|
171
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Node's `timeout` option only raises an event - the request stays open, which is why a server
|
|
330
|
+
* that accepts a connection and then says nothing used to hold a command open indefinitely.
|
|
331
|
+
* Destroying the request with an error routes through the error handling each caller already
|
|
332
|
+
* has, so a stalled request rejects the way any other failure does. With a timeout of zero the
|
|
333
|
+
* event never fires and this does nothing.
|
|
334
|
+
*/
|
|
335
|
+
// Typed on Writable because both http.ClientRequest and follow-redirects' wrapper are ones, and
|
|
336
|
+
// all this needs is the timeout event and destroy.
|
|
337
|
+
private failOnTimeout(request: Writable, url: URL, fail: (err: Error) => void): void {
|
|
338
|
+
request.on('timeout', () => {
|
|
339
|
+
// Reported before the request is torn down, rather than by destroying it with the error.
|
|
340
|
+
// Destroying raises errors of its own - ECONNRESET on the response, a premature close
|
|
341
|
+
// from pipeline - and node guarantees no order between those and the request's own. In
|
|
342
|
+
// practice the request's arrives first, so this is belt and braces rather than the thing
|
|
343
|
+
// that stops the wrong error being reported; that is the caller claiming the failure
|
|
344
|
+
// before it does any asynchronous cleanup.
|
|
345
|
+
fail(new Error(`No response from ${redactUrl(url)} for ${this.timeout} ms.`));
|
|
346
|
+
request.destroy();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
172
350
|
private getRequestOptions(method?: string, headers?: http.OutgoingHttpHeaders, maxBodyLength?: number): http.RequestOptions {
|
|
173
351
|
if (this.username && this.password) {
|
|
174
352
|
headers ??= {};
|
|
@@ -178,7 +356,8 @@ export class Registry {
|
|
|
178
356
|
return {
|
|
179
357
|
method,
|
|
180
358
|
headers,
|
|
181
|
-
maxBodyLength
|
|
359
|
+
maxBodyLength,
|
|
360
|
+
timeout: this.timeout
|
|
182
361
|
} as http.RequestOptions;
|
|
183
362
|
}
|
|
184
363
|
|
|
@@ -186,6 +365,11 @@ export class Registry {
|
|
|
186
365
|
return response => {
|
|
187
366
|
response.setEncoding('utf-8');
|
|
188
367
|
let json = '';
|
|
368
|
+
// A connection lost after the headers have arrived ends the response without 'end' ever
|
|
369
|
+
// firing, so without this the promise is never settled and the command waits on a body
|
|
370
|
+
// that is not coming. The configured timeout would eventually rescue it, but rejecting
|
|
371
|
+
// here reports what actually happened instead of thirty seconds of silence.
|
|
372
|
+
response.on('error', reject);
|
|
189
373
|
response.on('data', chunk => json += chunk);
|
|
190
374
|
response.on('end', () => {
|
|
191
375
|
if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
|
|
@@ -194,7 +378,8 @@ export class Registry {
|
|
|
194
378
|
const parsed = JSON.parse(json) as ErrorResponse;
|
|
195
379
|
const message = parsed.message || parsed.error;
|
|
196
380
|
if (message) {
|
|
197
|
-
|
|
381
|
+
// keep the status: the message alone cannot say whether retrying is worth it
|
|
382
|
+
reject(withStatus(new Error(message), response.statusCode));
|
|
198
383
|
return;
|
|
199
384
|
}
|
|
200
385
|
} catch (err) {
|
|
@@ -245,8 +430,18 @@ export interface Extension extends Response {
|
|
|
245
430
|
versionAlias: string[];
|
|
246
431
|
timestamp: string;
|
|
247
432
|
preview?: boolean;
|
|
433
|
+
preRelease?: boolean;
|
|
248
434
|
displayName?: string;
|
|
435
|
+
namespaceDisplayName?: string;
|
|
249
436
|
description?: string;
|
|
437
|
+
deprecated?: boolean;
|
|
438
|
+
replacement?: ExtensionReplacement;
|
|
439
|
+
downloadable?: boolean;
|
|
440
|
+
publishedWithTrustedPublishing?: boolean;
|
|
441
|
+
namespaceOwnershipConflict?: boolean;
|
|
442
|
+
extensionKind?: string[];
|
|
443
|
+
localizedLanguages?: string[];
|
|
444
|
+
sponsorLink?: string;
|
|
250
445
|
|
|
251
446
|
// key: engine, value: version constraint
|
|
252
447
|
engines?: { [engine: string]: string };
|
|
@@ -265,6 +460,26 @@ export interface Extension extends Response {
|
|
|
265
460
|
bundledExtensions?: ExtensionReference[];
|
|
266
461
|
}
|
|
267
462
|
|
|
463
|
+
export interface RegistryVersion extends Response {
|
|
464
|
+
version: string;
|
|
465
|
+
maxExtensionSize: number;
|
|
466
|
+
trustedPublishingAudience?: string;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export interface AccessToken extends Response {
|
|
470
|
+
id: number;
|
|
471
|
+
value?: string;
|
|
472
|
+
description: string;
|
|
473
|
+
createdTimestamp: string;
|
|
474
|
+
accessedTimestamp?: string;
|
|
475
|
+
expiresTimestamp?: string;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
export interface TargetPlatformVersion {
|
|
479
|
+
version: string;
|
|
480
|
+
targetPlatform?: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
268
483
|
export interface UserData {
|
|
269
484
|
loginName: string;
|
|
270
485
|
fullName?: string;
|
|
@@ -278,6 +493,64 @@ export interface Badge {
|
|
|
278
493
|
description: string;
|
|
279
494
|
}
|
|
280
495
|
|
|
496
|
+
export interface ExtensionReplacement {
|
|
497
|
+
url: string;
|
|
498
|
+
displayName?: string;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface VersionReference {
|
|
502
|
+
url: string;
|
|
503
|
+
files: { [type: string]: string };
|
|
504
|
+
version: string;
|
|
505
|
+
targetPlatform?: string;
|
|
506
|
+
engines?: { [engine: string]: string };
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export interface VersionReferences extends Response {
|
|
510
|
+
offset: number;
|
|
511
|
+
totalSize: number;
|
|
512
|
+
versions?: VersionReference[];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export interface SearchQuery {
|
|
516
|
+
query?: string;
|
|
517
|
+
category?: string;
|
|
518
|
+
targetPlatform?: string;
|
|
519
|
+
sortBy?: string;
|
|
520
|
+
sortOrder?: string;
|
|
521
|
+
size: number;
|
|
522
|
+
offset: number;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
export interface SearchEntry {
|
|
526
|
+
url: string;
|
|
527
|
+
files: { [type: string]: string };
|
|
528
|
+
name: string;
|
|
529
|
+
namespace: string;
|
|
530
|
+
version: string;
|
|
531
|
+
timestamp: string;
|
|
532
|
+
verified?: boolean;
|
|
533
|
+
averageRating?: number;
|
|
534
|
+
reviewCount?: number;
|
|
535
|
+
downloadCount: number;
|
|
536
|
+
displayName?: string;
|
|
537
|
+
description?: string;
|
|
538
|
+
deprecated?: boolean;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
export interface SearchResult extends Response {
|
|
542
|
+
offset: number;
|
|
543
|
+
totalSize: number;
|
|
544
|
+
extensions?: SearchEntry[];
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export interface Namespace extends Response {
|
|
548
|
+
name: string;
|
|
549
|
+
verified?: boolean;
|
|
550
|
+
// key: extension name, value: url
|
|
551
|
+
extensions?: { [name: string]: string };
|
|
552
|
+
}
|
|
553
|
+
|
|
281
554
|
export interface ExtensionReference {
|
|
282
555
|
url: string;
|
|
283
556
|
namespace: string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
|
|
3
|
+
*
|
|
4
|
+
* See the NOTICE file(s) distributed with this work for additional
|
|
5
|
+
* information regarding copyright ownership.
|
|
6
|
+
*
|
|
7
|
+
* This program and the accompanying materials are made available under the
|
|
8
|
+
* terms of the Eclipse Public License 2.0 which is available at
|
|
9
|
+
* https://www.eclipse.org/legal/epl-2.0.
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
12
|
+
*****************************************************************************/
|
|
13
|
+
|
|
14
|
+
import { RegistryOptions } from './registry-options';
|
|
15
|
+
|
|
16
|
+
export interface SearchOptions extends RegistryOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Text to search for. Omit to browse, which is what makes `--category` useful on its own.
|
|
19
|
+
*/
|
|
20
|
+
text?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Restrict results to a category, e.g. `Programming Languages`.
|
|
23
|
+
*/
|
|
24
|
+
category?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Restrict results to extensions published for a target platform.
|
|
27
|
+
*/
|
|
28
|
+
target?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Sort key: `relevance`, `timestamp`, `rating` or `downloadCount`.
|
|
31
|
+
*/
|
|
32
|
+
sortBy?: string;
|
|
33
|
+
/**
|
|
34
|
+
* `asc` or `desc`.
|
|
35
|
+
*/
|
|
36
|
+
sortOrder?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Number of results to return.
|
|
39
|
+
*/
|
|
40
|
+
size?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Index of the first result, for paging through a large result set.
|
|
43
|
+
*/
|
|
44
|
+
offset?: number;
|
|
45
|
+
/**
|
|
46
|
+
* Print the raw results as JSON instead of a table.
|
|
47
|
+
*/
|
|
48
|
+
json?: boolean;
|
|
49
|
+
}
|
package/src/search.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
|
|
3
|
+
*
|
|
4
|
+
* See the NOTICE file(s) distributed with this work for additional
|
|
5
|
+
* information regarding copyright ownership.
|
|
6
|
+
*
|
|
7
|
+
* This program and the accompanying materials are made available under the
|
|
8
|
+
* terms of the Eclipse Public License 2.0 which is available at
|
|
9
|
+
* https://www.eclipse.org/legal/epl-2.0.
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
12
|
+
*****************************************************************************/
|
|
13
|
+
|
|
14
|
+
import { Registry, SearchEntry } from './registry';
|
|
15
|
+
import { SearchOptions } from './search-options';
|
|
16
|
+
import { formatNumber, printRows, truncate } from './table';
|
|
17
|
+
import { addEnvOptions } from './util';
|
|
18
|
+
|
|
19
|
+
/** Sort keys the registry accepts, for an up-front check with a better message than a 400. */
|
|
20
|
+
export const SORT_KEYS = ['relevance', 'timestamp', 'rating', 'downloadCount'];
|
|
21
|
+
|
|
22
|
+
export const SORT_ORDERS = ['asc', 'desc'];
|
|
23
|
+
|
|
24
|
+
export const DEFAULT_SEARCH_SIZE = 20;
|
|
25
|
+
|
|
26
|
+
/** Keeps a row from wrapping on a normal terminal, where the other columns take about half of it. */
|
|
27
|
+
const DESCRIPTION_WIDTH = 60;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Searches the registry for extensions.
|
|
31
|
+
*/
|
|
32
|
+
export async function search(options: SearchOptions): Promise<void> {
|
|
33
|
+
addEnvOptions(options);
|
|
34
|
+
if (options.sortBy && !SORT_KEYS.includes(options.sortBy)) {
|
|
35
|
+
throw new Error(`Sort key must be one of ${SORT_KEYS.join(', ')}.`);
|
|
36
|
+
}
|
|
37
|
+
if (options.sortOrder && !SORT_ORDERS.includes(options.sortOrder)) {
|
|
38
|
+
throw new Error(`Sort order must be one of ${SORT_ORDERS.join(', ')}.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const size = options.size ?? DEFAULT_SEARCH_SIZE;
|
|
42
|
+
const offset = options.offset ?? 0;
|
|
43
|
+
const registry = new Registry(options);
|
|
44
|
+
const result = await registry.search({
|
|
45
|
+
query: options.text,
|
|
46
|
+
category: options.category,
|
|
47
|
+
targetPlatform: options.target,
|
|
48
|
+
sortBy: options.sortBy,
|
|
49
|
+
sortOrder: options.sortOrder,
|
|
50
|
+
size,
|
|
51
|
+
offset
|
|
52
|
+
});
|
|
53
|
+
if (result.error) {
|
|
54
|
+
throw new Error(result.error);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (options.json) {
|
|
58
|
+
console.log(JSON.stringify(result, null, 4));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
printResults(result.extensions ?? [], result.totalSize, offset);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printResults(entries: SearchEntry[], totalSize: number, offset: number): void {
|
|
66
|
+
if (entries.length === 0) {
|
|
67
|
+
console.log('No extensions found.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rows = entries.map(entry => [
|
|
72
|
+
`${entry.namespace}.${entry.name}`,
|
|
73
|
+
entry.version,
|
|
74
|
+
formatNumber(entry.downloadCount),
|
|
75
|
+
entry.averageRating !== undefined ? entry.averageRating.toFixed(1) : '-',
|
|
76
|
+
describe(entry)
|
|
77
|
+
]);
|
|
78
|
+
printRows([['Extension', 'Version', 'Downloads', 'Rating', 'Description'], ...rows]);
|
|
79
|
+
|
|
80
|
+
const last = offset + entries.length;
|
|
81
|
+
console.log();
|
|
82
|
+
console.log(`Showing ${offset + 1}-${last} of ${formatNumber(totalSize)}.`);
|
|
83
|
+
if (last < totalSize) {
|
|
84
|
+
console.log(`Pass --offset ${last} for the next page.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The description, prefixed with anything a reader should weigh before installing. Deprecation is
|
|
90
|
+
* the one flag worth spending row width on here; `show` reports the rest.
|
|
91
|
+
*/
|
|
92
|
+
function describe(entry: SearchEntry): string {
|
|
93
|
+
const description = truncate(entry.description ?? '', DESCRIPTION_WIDTH);
|
|
94
|
+
return entry.deprecated ? `(deprecated) ${description}`.trimEnd() : description;
|
|
95
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/******************************************************************************
|
|
2
|
+
* Copyright (c) 2026 Contributors to the Eclipse Foundation.
|
|
3
|
+
*
|
|
4
|
+
* See the NOTICE file(s) distributed with this work for additional
|
|
5
|
+
* information regarding copyright ownership.
|
|
6
|
+
*
|
|
7
|
+
* This program and the accompanying materials are made available under the
|
|
8
|
+
* terms of the Eclipse Public License 2.0 which is available at
|
|
9
|
+
* https://www.eclipse.org/legal/epl-2.0.
|
|
10
|
+
*
|
|
11
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
12
|
+
*****************************************************************************/
|
|
13
|
+
|
|
14
|
+
import { RegistryOptions } from './registry-options';
|
|
15
|
+
|
|
16
|
+
export interface ShowOptions extends RegistryOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Identifier in the form `namespace.extension` or `namespace/extension`, optionally suffixed
|
|
19
|
+
* with `@<version>` - an exact version, or one of the `latest` / `pre-release` aliases.
|
|
20
|
+
*/
|
|
21
|
+
extensionId: string;
|
|
22
|
+
/**
|
|
23
|
+
* Target platform to report on. Defaults to whichever the registry considers current.
|
|
24
|
+
*/
|
|
25
|
+
target?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Print the raw metadata as JSON instead of a readable summary.
|
|
28
|
+
*/
|
|
29
|
+
json?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* List every published version rather than the most recent few.
|
|
32
|
+
*/
|
|
33
|
+
allVersions?: boolean;
|
|
34
|
+
}
|