ovsx 0.1.0-next.e000fdb → 0.3.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/src/main.ts CHANGED
@@ -19,31 +19,27 @@ const pkg = require('../package.json');
19
19
 
20
20
  module.exports = function (argv: string[]): void {
21
21
  const program = new commander.Command();
22
- program.usage('<command> [options]');
23
- program.option('--debug', 'include debug information on error');
24
-
25
- const versionCmd = program.command('version');
26
- versionCmd.description('Output the version number.')
27
- .action(() => console.log(`Eclipse Open VSX CLI version ${pkg.version}`));
22
+ program.usage('<command> [options]')
23
+ .option('-r, --registryUrl <url>', 'Use the registry API at this base URL.')
24
+ .option('-p, --pat <token>', 'Personal access token.')
25
+ .option('--debug', 'Include debug information on error')
26
+ .version(pkg.version, '-V, --version', 'Print the Eclipse Open VSX CLI version');
28
27
 
29
28
  const createNamespaceCmd = program.command('create-namespace <name>');
30
29
  createNamespaceCmd.description('Create a new namespace')
31
- .option('-r, --registryUrl <url>', 'Use the registry API at this base URL.')
32
- .option('-p, --pat <token>', 'Personal access token (required).')
33
- .action((name: string, { registryUrl, pat }) => {
30
+ .action((name: string) => {
31
+ const { registryUrl, pat } = program.opts();
34
32
  createNamespace({ name, registryUrl, pat })
35
33
  .catch(handleError(program.debug));
36
34
  });
37
35
 
38
36
  const publishCmd = program.command('publish [extension.vsix]');
39
37
  publishCmd.description('Publish an extension, packaging it first if necessary.')
40
- .option('-r, --registryUrl <url>', 'Use the registry API at this base URL.')
41
- .option('-p, --pat <token>', 'Personal access token (required).')
42
- .option('--packagePath <path>', 'Package and publish the extension at the specified path.')
38
+ .option('-i, --packagePath <paths...>', 'Publish the provided VSIX packages.')
43
39
  .option('--baseContentUrl <url>', 'Prepend all relative links in README.md with this URL.')
44
40
  .option('--baseImagesUrl <url>', 'Prepend all relative image links in README.md with this URL.')
45
41
  .option('--yarn', 'Use yarn instead of npm while packing extension files.')
46
- .action((extensionFile: string, { registryUrl, pat, packagePath, baseContentUrl, baseImagesUrl, yarn }) => {
42
+ .action((extensionFile: string, { packagePath, baseContentUrl, baseImagesUrl, yarn }) => {
47
43
  if (extensionFile !== undefined && packagePath !== undefined) {
48
44
  console.error('\u274c Please specify either a package file or a package path, but not both.\n');
49
45
  publishCmd.help();
@@ -54,32 +50,39 @@ module.exports = function (argv: string[]): void {
54
50
  console.warn("Ignoring option '--baseImagesUrl' for prepackaged extension.");
55
51
  if (extensionFile !== undefined && yarn !== undefined)
56
52
  console.warn("Ignoring option '--yarn' for prepackaged extension.");
57
- publish({ extensionFile, registryUrl, pat, packagePath, baseContentUrl, baseImagesUrl, yarn })
58
- .catch(handleError(program.debug));
53
+ const { registryUrl, pat } = program.opts();
54
+ publish({ extensionFile, registryUrl, pat, packagePath: typeof packagePath === 'string' ? [packagePath] : packagePath, baseContentUrl, baseImagesUrl, yarn })
55
+ .catch(handleError(program.debug,
56
+ 'See the documentation for more information:\n'
57
+ + 'https://github.com/eclipse/openvsx/wiki/Publishing-Extensions'
58
+ ));
59
59
  });
60
60
 
61
61
  const getCmd = program.command('get <namespace.extension>');
62
62
  getCmd.description('Download an extension or its metadata.')
63
- .option('-v, --version <version>', 'Specify an exact version or a version range.')
64
- .option('-r, --registryUrl <url>', 'Use the registry API at this base URL.')
63
+ .option('-v, --versionRange <version>', 'Specify an exact version or a version range.')
65
64
  .option('-o, --output <path>', 'Save the output in the specified file or directory.')
66
65
  .option('--metadata', 'Print the extension\'s metadata instead of downloading it.')
67
- .action((extensionId: string, { version, registryUrl, output, metadata }) => {
68
- if (typeof version === 'function') // If not specified, `version` yields a function
69
- version = undefined;
70
- getExtension({ extensionId, version, registryUrl, output, metadata })
66
+ .action((extensionId: string, { versionRange, output, metadata }) => {
67
+ const { registryUrl } = program.opts();
68
+ getExtension({ extensionId, version: versionRange, registryUrl, output, metadata })
71
69
  .catch(handleError(program.debug));
72
70
  });
73
71
 
74
72
  program
75
73
  .command('*', '', { noHelp: true })
76
- .action((cmd: string) => {
74
+ .action((cmd: commander.Command) => {
77
75
  const availableCommands = program.commands.map((c: any) => c._name) as string[];
78
- const suggestion = availableCommands.find(c => leven(c, cmd) < c.length * 0.4);
79
- if (suggestion)
80
- console.error(`Unknown command '${cmd}', did you mean '${suggestion}'?\n`);
81
- else
82
- console.error(`Unknown command '${cmd}'`);
76
+ const actualCommand = cmd.args[0];
77
+ if (actualCommand) {
78
+ const suggestion = availableCommands.find(c => leven(c, actualCommand) < c.length * 0.4);
79
+ if (suggestion)
80
+ console.error(`Unknown command '${actualCommand}', did you mean '${suggestion}'?\n`);
81
+ else
82
+ console.error(`Unknown command '${actualCommand}'.\n`);
83
+ } else {
84
+ console.error('Unknown command.');
85
+ }
83
86
  program.help();
84
87
  });
85
88
 
package/src/ovsx CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- if (global.URL === undefined) {
4
- console.error('ovsx requires at least NodeJS version 10. Check your installed version with `node --version`.');
3
+ const semver = require('semver');
4
+
5
+ if (semver.lt(process.versions.node, '14.0.0')) {
6
+ console.error('ovsx requires at least NodeJS version 14. Check your installed version with `node --version`.');
5
7
  process.exit(1);
6
8
  }
7
9
 
package/src/publish.ts CHANGED
@@ -8,60 +8,53 @@
8
8
  * SPDX-License-Identifier: EPL-2.0
9
9
  ********************************************************************************/
10
10
 
11
- import { createVSIX } from 'vsce';
12
- import { createTempFile } from './util';
13
- import { Registry } from './registry';
11
+ import { createVSIX, ICreateVSIXOptions } from 'vsce';
12
+ import { createTempFile, addEnvOptions } from './util';
13
+ import { Registry, RegistryOptions } from './registry';
14
+ import { checkLicense } from './check-license';
14
15
 
15
16
  /**
16
17
  * Publishes an extension.
17
18
  */
18
19
  export async function publish(options: PublishOptions = {}): Promise<void> {
19
- if (!options.registryUrl) {
20
- options.registryUrl = process.env.OVSX_REGISTRY_URL;
21
- }
22
- if (!options.pat) {
23
- options.pat = process.env.OVSX_PAT;
24
- if (!options.pat) {
25
- throw new Error("A personal access token must be given with the option '--pat'.");
20
+ addEnvOptions(options);
21
+ if (options.packagePath) {
22
+ // call the publish command for every package path
23
+ await Promise.all(options.packagePath.map(path => doPublish({ ...options, packagePath: path })));
24
+ } else {
25
+ return doPublish({ ... options, packagePath: undefined });
26
26
  }
27
+ }
28
+
29
+ async function doPublish(options: InternalPublishOptions = {}): Promise<void> {
30
+ if (!options.pat) {
31
+ throw new Error("A personal access token must be given with the option '--pat'.");
27
32
  }
33
+
34
+ // if the packagePath is a link to a vsix, don't need to package it
35
+ if (options.packagePath && options.packagePath.endsWith('.vsix')) {
36
+ options.extensionFile = options.packagePath;
37
+ delete options.packagePath;
38
+ }
39
+ const registry = new Registry(options);
28
40
  if (!options.extensionFile) {
29
- options.extensionFile = await createTempFile({ postfix: '.vsix' });
30
- await createVSIX({
31
- cwd: options.packagePath,
32
- packagePath: options.extensionFile,
33
- baseContentUrl: options.baseContentUrl,
34
- baseImagesUrl: options.baseImagesUrl,
35
- useYarn: options.yarn
36
- });
41
+ await packageExtension(options, registry);
37
42
  console.log(); // new line
38
43
  }
39
- const registry = new Registry({ url: options.registryUrl });
40
- const extension = await registry.publish(options.extensionFile, options.pat);
44
+
45
+ const extension = await registry.publish(options.extensionFile!, options.pat);
41
46
  if (extension.error) {
42
47
  throw new Error(extension.error);
43
48
  }
44
49
  console.log(`\ud83d\ude80 Published ${extension.namespace}.${extension.name} v${extension.version}`);
45
50
  }
46
51
 
47
- export interface PublishOptions {
48
- /**
49
- * The base URL of the registry API.
50
- */
51
- registryUrl?: string;
52
- /**
53
- * Personal access token.
54
- */
55
- pat?: string;
52
+ interface PublishCommonOptions extends RegistryOptions {
56
53
  /**
57
54
  * Path to the vsix file to be published. Cannot be used together with `packagePath`.
58
55
  */
59
56
  extensionFile?: string;
60
- /**
61
- * Path to the extension to be packaged and published. Cannot be used together
62
- * with `extensionFile`.
63
- */
64
- packagePath?: string;
57
+
65
58
  /**
66
59
  * The base URL for links detected in Markdown files. Only valid with `packagePath`.
67
60
  */
@@ -75,3 +68,40 @@ export interface PublishOptions {
75
68
  */
76
69
  yarn?: boolean;
77
70
  }
71
+
72
+ // Interface used by top level CLI
73
+ export interface PublishOptions extends PublishCommonOptions {
74
+
75
+ /**
76
+ * Paths to the extension to be packaged and published. Cannot be used together
77
+ * with `extensionFile`.
78
+ */
79
+ packagePath?: string[];
80
+ }
81
+
82
+ // Interface used internally by the doPublish method
83
+ interface InternalPublishOptions extends PublishCommonOptions {
84
+
85
+ /**
86
+ * Only one path for our internal command.
87
+ * Path to the extension to be packaged and published. Cannot be used together
88
+ * with `extensionFile`.
89
+ */
90
+ packagePath?: string;
91
+ }
92
+
93
+ async function packageExtension(options: InternalPublishOptions, registry: Registry): Promise<void> {
94
+ if (registry.requiresLicense) {
95
+ await checkLicense(options.packagePath!);
96
+ }
97
+
98
+ options.extensionFile = await createTempFile({ postfix: '.vsix' });
99
+ const createVSIXOptions: ICreateVSIXOptions = {
100
+ cwd: options.packagePath,
101
+ packagePath: options.extensionFile,
102
+ baseContentUrl: options.baseContentUrl,
103
+ baseImagesUrl: options.baseImagesUrl,
104
+ useYarn: options.yarn
105
+ };
106
+ await createVSIX(createVSIXOptions);
107
+ }
package/src/registry.ts CHANGED
@@ -24,16 +24,25 @@ export class Registry {
24
24
  readonly url: string;
25
25
  readonly maxNamespaceSize: number;
26
26
  readonly maxPublishSize: number;
27
+ readonly username?: string;
28
+ readonly password?: string;
27
29
 
28
30
  constructor(options: RegistryOptions = {}) {
29
- if (options.url && options.url.endsWith('/'))
30
- this.url = options.url.substring(0, options.url.length - 1);
31
- else if (options.url)
32
- this.url = options.url;
31
+ if (options.registryUrl && options.registryUrl.endsWith('/'))
32
+ this.url = options.registryUrl.substring(0, options.registryUrl.length - 1);
33
+ else if (options.registryUrl)
34
+ this.url = options.registryUrl;
33
35
  else
34
36
  this.url = DEFAULT_URL;
35
37
  this.maxNamespaceSize = options.maxNamespaceSize || DEFAULT_NAMESPACE_SIZE;
36
38
  this.maxPublishSize = options.maxPublishSize || DEFAULT_PUBLISH_SIZE;
39
+ this.username = options.username;
40
+ this.password = options.password;
41
+ }
42
+
43
+ get requiresLicense(): boolean {
44
+ const url = new URL(this.url);
45
+ return url.hostname === 'open-vsx.org' || url.hostname.endsWith('.open-vsx.org');
37
46
  }
38
47
 
39
48
  createNamespace(name: string, pat: string): Promise<Response> {
@@ -73,8 +82,9 @@ export class Registry {
73
82
  download(file: string, url: URL): Promise<void> {
74
83
  return new Promise((resolve, reject) => {
75
84
  const stream = fs.createWriteStream(file);
85
+ const requestOptions = this.getRequestOptions();
76
86
  const request = this.getProtocol(url)
77
- .request(url, response => {
87
+ .request(url, requestOptions, response => {
78
88
  response.on('end', () => {
79
89
  if (response.statusCode !== undefined && (response.statusCode < 200 || response.statusCode > 299)) {
80
90
  reject(statusError(response));
@@ -98,8 +108,9 @@ export class Registry {
98
108
 
99
109
  getJson<T extends Response>(url: URL): Promise<T> {
100
110
  return new Promise((resolve, reject) => {
111
+ const requestOptions = this.getRequestOptions();
101
112
  const request = this.getProtocol(url)
102
- .request(url, this.getJsonResponse<T>(resolve, reject));
113
+ .request(url, requestOptions, this.getJsonResponse<T>(resolve, reject));
103
114
  request.on('error', reject);
104
115
  request.end();
105
116
  });
@@ -107,7 +118,7 @@ export class Registry {
107
118
 
108
119
  post<T extends Response>(content: string | Buffer | Uint8Array, url: URL, headers?: http.OutgoingHttpHeaders, maxBodyLength?: number): Promise<T> {
109
120
  return new Promise((resolve, reject) => {
110
- const requestOptions = { method: 'POST', headers, maxBodyLength } as http.RequestOptions;
121
+ const requestOptions = this.getRequestOptions('POST', headers, maxBodyLength);
111
122
  const request = this.getProtocol(url)
112
123
  .request(url, requestOptions, this.getJsonResponse<T>(resolve, reject));
113
124
  request.on('error', reject);
@@ -119,7 +130,7 @@ export class Registry {
119
130
  postFile<T extends Response>(file: string, url: URL, headers?: http.OutgoingHttpHeaders, maxBodyLength?: number): Promise<T> {
120
131
  return new Promise((resolve, reject) => {
121
132
  const stream = fs.createReadStream(file);
122
- const requestOptions = { method: 'POST', headers, maxBodyLength } as http.RequestOptions;
133
+ const requestOptions = this.getRequestOptions('POST', headers, maxBodyLength);
123
134
  const request = this.getProtocol(url)
124
135
  .request(url, requestOptions, this.getJsonResponse<T>(resolve, reject));
125
136
  stream.on('error', err => {
@@ -150,6 +161,21 @@ export class Registry {
150
161
  return followRedirects.http as typeof http;
151
162
  }
152
163
 
164
+ private getRequestOptions(method?: string, headers?: http.OutgoingHttpHeaders, maxBodyLength?: number): http.RequestOptions {
165
+ if (this.username && this.password) {
166
+ if (!headers) {
167
+ headers = {};
168
+ }
169
+ const credentials = Buffer.from(this.username + ':' + this.password).toString('base64');
170
+ headers['Authorization'] = 'Basic ' + credentials;
171
+ }
172
+ return {
173
+ method,
174
+ headers,
175
+ maxBodyLength
176
+ } as http.RequestOptions;
177
+ }
178
+
153
179
  private getJsonResponse<T extends Response>(resolve: (value: T) => void, reject: (reason: any) => void): (res: http.IncomingMessage) => void {
154
180
  return response => {
155
181
  response.setEncoding('UTF-8');
@@ -186,8 +212,29 @@ export class Registry {
186
212
  }
187
213
 
188
214
  export interface RegistryOptions {
189
- url?: string;
215
+ /**
216
+ * The base URL of the registry API.
217
+ */
218
+ registryUrl?: string;
219
+ /**
220
+ * Personal access token.
221
+ */
222
+ pat?: string;
223
+ /**
224
+ * User name for basic authentication.
225
+ */
226
+ username?: string;
227
+ /**
228
+ * Password for basic authentication.
229
+ */
230
+ password?: string;
231
+ /**
232
+ * Maximal request body size for creating namespaces.
233
+ */
190
234
  maxNamespaceSize?: number;
235
+ /**
236
+ * Maximal request body size for publishing.
237
+ */
191
238
  maxPublishSize?: number;
192
239
  }
193
240
 
@@ -206,8 +253,7 @@ export interface Extension extends Response {
206
253
  namespace: string;
207
254
  version: string;
208
255
  publishedBy: UserData;
209
- unrelatedPublisher: boolean;
210
- namespaceAccess: 'public' | 'restricted';
256
+ verified: boolean;
211
257
  // key: version, value: url
212
258
  allVersions: { [version: string]: string };
213
259
 
package/src/util.ts CHANGED
@@ -9,11 +9,29 @@
9
9
  ********************************************************************************/
10
10
 
11
11
  import * as fs from 'fs';
12
+ import * as path from 'path';
12
13
  import * as tmp from 'tmp';
13
14
  import * as http from 'http';
15
+ import * as readline from 'readline';
16
+ import { RegistryOptions } from './registry';
14
17
 
15
18
  export { promisify } from 'util';
16
19
 
20
+ export function addEnvOptions(options: RegistryOptions): void {
21
+ if (!options.registryUrl) {
22
+ options.registryUrl = process.env.OVSX_REGISTRY_URL;
23
+ }
24
+ if (!options.pat) {
25
+ options.pat = process.env.OVSX_PAT;
26
+ }
27
+ if (!options.username) {
28
+ options.username = process.env.OVSX_USERNAME;
29
+ }
30
+ if (!options.password) {
31
+ options.password = process.env.OVSX_PASSWORD;
32
+ }
33
+ }
34
+
17
35
  export function matchExtensionId(id: string): RegExpExecArray | null {
18
36
  return /^([\w-]+)(?:\.|\/)([\w-]+)$/.exec(id);
19
37
  }
@@ -50,10 +68,13 @@ export function createTempFile(options: tmp.TmpNameOptions): Promise<string> {
50
68
  });
51
69
  }
52
70
 
53
- export function handleError(debug?: boolean): (reason: any) => void {
71
+ export function handleError(debug?: boolean, additionalMessage?: string): (reason: any) => void {
54
72
  return reason => {
55
73
  if (reason instanceof Error && !debug) {
56
74
  console.error(`\u274c ${reason.message}`);
75
+ if (additionalMessage) {
76
+ console.error(additionalMessage);
77
+ }
57
78
  } else if (typeof reason === 'string') {
58
79
  console.error(`\u274c ${reason}`);
59
80
  } else if (reason !== undefined) {
@@ -71,3 +92,91 @@ export function statusError(response: http.IncomingMessage): Error {
71
92
  else
72
93
  return new Error(`The server responded with status ${response.statusCode}.`);
73
94
  }
95
+
96
+ export function readFile(name: string, packagePath?: string, encoding = 'utf-8'): Promise<string> {
97
+ return new Promise((resolve, reject) => {
98
+ fs.readFile(
99
+ path.join(packagePath || process.cwd(), name),
100
+ { encoding },
101
+ (err, content) => {
102
+ if (err) {
103
+ reject(err);
104
+ } else {
105
+ resolve(content);
106
+ }
107
+ }
108
+ );
109
+ });
110
+ }
111
+
112
+ export async function readManifest(packagePath?: string): Promise<Manifest> {
113
+ const content = await readFile('package.json', packagePath);
114
+ return JSON.parse(content);
115
+ }
116
+
117
+ export function validateManifest(manifest: Manifest): void {
118
+ if (!manifest.publisher) {
119
+ throw new Error("Missing required field 'publisher'.");
120
+ }
121
+ if (!manifest.name) {
122
+ throw new Error("Missing required field 'name'.");
123
+ }
124
+ if (!manifest.version) {
125
+ throw new Error("Missing required field 'version'.");
126
+ }
127
+ }
128
+
129
+ export function writeFile(name: string, content: string, packagePath?: string, encoding = 'utf-8'): Promise<void> {
130
+ return new Promise((resolve, reject) => {
131
+ fs.writeFile(
132
+ path.join(packagePath || process.cwd(), name),
133
+ content,
134
+ { encoding },
135
+ err => {
136
+ if (err) {
137
+ reject(err);
138
+ } else {
139
+ resolve();
140
+ }
141
+ }
142
+ );
143
+ });
144
+ }
145
+
146
+ export function writeManifest(manifest: Manifest, packagePath?: string): Promise<void> {
147
+ const content = JSON.stringify(manifest, null, 4);
148
+ return writeFile('package.json', content, packagePath);
149
+ }
150
+
151
+ export interface Manifest {
152
+ publisher: string;
153
+ name: string;
154
+ version: string;
155
+ license?: string;
156
+ }
157
+
158
+ export function getUserInput(text: string): Promise<string> {
159
+ return new Promise(resolve => {
160
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
161
+ rl.question(text, answer => {
162
+ resolve(answer);
163
+ rl.close();
164
+ });
165
+ });
166
+ }
167
+
168
+ export async function getUserChoice<R extends string>(text: string, values: R[],
169
+ defaultValue: R, lowerCase = true): Promise<R> {
170
+ const prompt = text + '\n' + values.map(v => v === defaultValue ? `[${v}]` : v).join('/') + ': ';
171
+ const answer = await getUserInput(prompt);
172
+ if (!answer) {
173
+ return defaultValue;
174
+ }
175
+ const lcAnswer = lowerCase ? answer.toLowerCase() : answer;
176
+ for (const value of values) {
177
+ if (value.startsWith(lcAnswer)) {
178
+ return value;
179
+ }
180
+ }
181
+ return defaultValue;
182
+ }