Merge remote-tracking branch 'vso/master'
This commit is contained in:
commit
d28cdad3d7
8 changed files with 182 additions and 168 deletions
95
src/login.ts
95
src/login.ts
|
@ -1,95 +0,0 @@
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
import { Promise, nfcall, resolve, reject } from 'q';
|
|
||||||
import { home } from 'osenv';
|
|
||||||
import { read } from './util';
|
|
||||||
import { WebApi, getBasicHandler } from 'vso-node-api/WebApi';
|
|
||||||
|
|
||||||
const credentialsPath = path.join(home(), '.vsce');
|
|
||||||
|
|
||||||
export interface ICredentials {
|
|
||||||
account: string;
|
|
||||||
publisher: string;
|
|
||||||
pat: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGetCredentialsOptions {
|
|
||||||
promptToOverwrite?: boolean;
|
|
||||||
promptIfMissing?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function readCredentials(): Promise<ICredentials> {
|
|
||||||
return nfcall<string>(fs.readFile, credentialsPath, 'utf8')
|
|
||||||
.catch<string>(err => err.code !== 'ENOENT' ? reject(err) : resolve('null'))
|
|
||||||
.then<ICredentials>(credentialsStr => {
|
|
||||||
try {
|
|
||||||
return resolve(JSON.parse(credentialsStr));
|
|
||||||
} catch (e) {
|
|
||||||
return reject(`Error parsing credentials: ${ credentialsPath }`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeCredentials(credentials: ICredentials): Promise<ICredentials> {
|
|
||||||
return nfcall<void>(fs.writeFile, credentialsPath, JSON.stringify(credentials))
|
|
||||||
.then(() => credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearCredentials(): Promise<any> {
|
|
||||||
return nfcall(fs.unlink, credentialsPath)
|
|
||||||
.catch(err => err.code !== 'ENOENT' ? reject(err) : resolve('null'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function promptForCredentials(): Promise<ICredentials> {
|
|
||||||
return read('Account name:').then(account => {
|
|
||||||
if (!/^https?:\/\//.test(account)) {
|
|
||||||
account = `https://${ account }.visualstudio.com`;
|
|
||||||
console.log(`Assuming account name '${ account }'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return read('Publisher:').then(publisher => {
|
|
||||||
return read('Personal Access Token:', { silent: true, replace: '*' })
|
|
||||||
.then(pat => ({ account, publisher, pat }));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCredentials(options: IGetCredentialsOptions = {}): Promise<ICredentials> {
|
|
||||||
return readCredentials()
|
|
||||||
.then(credentials => {
|
|
||||||
if (!credentials || !options.promptToOverwrite) {
|
|
||||||
return resolve(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Existing credentials found: { account: ${ credentials.account }, publisher: ${ credentials.publisher } }`);
|
|
||||||
return read('Do you want to overwrite existing credentials? [y/N] ')
|
|
||||||
.then<ICredentials>(answer => /^y$/i.test(answer) ? null : credentials);
|
|
||||||
})
|
|
||||||
.then(credentials => {
|
|
||||||
if (credentials || !options.promptIfMissing) {
|
|
||||||
return resolve(credentials);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promptForCredentials()
|
|
||||||
.then(writeCredentials);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const galleryUrl = 'https://app.market.visualstudio.com';
|
|
||||||
|
|
||||||
export function login(): Promise<ICredentials> {
|
|
||||||
return getCredentials({ promptIfMissing: true, promptToOverwrite: true }).then(credentials => {
|
|
||||||
const authHandler = getBasicHandler('oauth', credentials.pat);
|
|
||||||
const vsoapi = new WebApi(credentials.account, authHandler);
|
|
||||||
const api = vsoapi.getQGalleryApi(galleryUrl);
|
|
||||||
|
|
||||||
return api.getPublisher(credentials.publisher).then(publisher => {
|
|
||||||
console.log(`Authentication successful. Found publisher '${ publisher.displayName }'.`);
|
|
||||||
return credentials;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logout(): Promise<any> {
|
|
||||||
return clearCredentials();
|
|
||||||
}
|
|
37
src/main.ts
37
src/main.ts
|
@ -2,7 +2,7 @@ import * as minimist from 'minimist';
|
||||||
import { pack } from './package';
|
import { pack } from './package';
|
||||||
import { publish } from './publish';
|
import { publish } from './publish';
|
||||||
import { fatal } from './util';
|
import { fatal } from './util';
|
||||||
import { login, logout } from './login';
|
import { publisher } from './store';
|
||||||
const packagejson = require('../package.json');
|
const packagejson = require('../package.json');
|
||||||
|
|
||||||
function helpCommand(): void {
|
function helpCommand(): void {
|
||||||
|
@ -11,8 +11,9 @@ function helpCommand(): void {
|
||||||
Commands:
|
Commands:
|
||||||
package [vsix path] Packages the extension into a .vsix package
|
package [vsix path] Packages the extension into a .vsix package
|
||||||
publish Publishes the extension
|
publish Publishes the extension
|
||||||
login Logs in to the extension service
|
publisher add [publisher] Add a publisher
|
||||||
logout Logs out of the extension service
|
publisher rm [publisher] Remove a publisher
|
||||||
|
publisher list List all added publishers
|
||||||
|
|
||||||
Global options:
|
Global options:
|
||||||
--help, -h Display help
|
--help, -h Display help
|
||||||
|
@ -27,22 +28,26 @@ function versionCommand(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function command(args: minimist.ParsedArgs): boolean {
|
function command(args: minimist.ParsedArgs): boolean {
|
||||||
const promise = (() => {
|
try {
|
||||||
switch (args._[0]) {
|
const promise = (() => {
|
||||||
case 'package': return pack(args._[1]).then(({ packagePath }) => console.log(`Package created: ${ packagePath }`));
|
switch (args._[0]) {
|
||||||
case 'login': return login();
|
case 'package': return pack(args._[1]).then(({ packagePath }) => console.log(`Package created: ${ packagePath }`));
|
||||||
case 'logout': return logout();
|
case 'publisher': return publisher(args._[1], args._[2]);
|
||||||
case 'publish': return publish(args._[1]);
|
case 'publish': return publish(args._[1]);
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
if (promise) {
|
if (promise) {
|
||||||
promise.catch(fatal);
|
promise.catch(fatal);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
fatal(e);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = function (argv: string[]): void {
|
module.exports = function (argv: string[]): void {
|
||||||
|
|
|
@ -11,6 +11,7 @@ export interface Manifest {
|
||||||
engines: { vscode: string; [name: string]: string; };
|
engines: { vscode: string; [name: string]: string; };
|
||||||
|
|
||||||
// vscode
|
// vscode
|
||||||
|
publisher: string;
|
||||||
contributes?: { [contributionType: string]: any; };
|
contributes?: { [contributionType: string]: any; };
|
||||||
activationEvents?: string[];
|
activationEvents?: string[];
|
||||||
extensionDependencies?: string[];
|
extensionDependencies?: string[];
|
||||||
|
|
|
@ -3,11 +3,9 @@ import * as path from 'path';
|
||||||
import * as _ from 'lodash';
|
import * as _ from 'lodash';
|
||||||
import * as yazl from 'yazl';
|
import * as yazl from 'yazl';
|
||||||
import { Manifest } from './manifest';
|
import { Manifest } from './manifest';
|
||||||
import { getCredentials } from './login';
|
|
||||||
import { nfcall, Promise, reject, resolve, all } from 'q';
|
import { nfcall, Promise, reject, resolve, all } from 'q';
|
||||||
import * as glob from 'glob';
|
import * as glob from 'glob';
|
||||||
import * as minimatch from 'minimatch';
|
import * as minimatch from 'minimatch';
|
||||||
import { read } from './util';
|
|
||||||
import { exec } from 'child_process';
|
import { exec } from 'child_process';
|
||||||
|
|
||||||
const resourcesPath = path.join(path.dirname(__dirname), 'resources');
|
const resourcesPath = path.join(path.dirname(__dirname), 'resources');
|
||||||
|
@ -28,6 +26,10 @@ function readManifest(cwd: string): Promise<Manifest> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateManifest(manifest: Manifest): Promise<Manifest> {
|
function validateManifest(manifest: Manifest): Promise<Manifest> {
|
||||||
|
if (!manifest.publisher) {
|
||||||
|
return reject<Manifest>('Manifest missing field: publisher');
|
||||||
|
}
|
||||||
|
|
||||||
if (!manifest.name) {
|
if (!manifest.name) {
|
||||||
return reject<Manifest>('Manifest missing field: name');
|
return reject<Manifest>('Manifest missing field: name');
|
||||||
}
|
}
|
||||||
|
@ -48,11 +50,11 @@ function validateManifest(manifest: Manifest): Promise<Manifest> {
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepublish(cwd: string, manifest: Manifest): Promise<Manifest> {
|
function prepublish(cwd: string, manifest: Manifest): Promise<Manifest> {
|
||||||
if (!manifest.scripts || !manifest.scripts['prepublish']) {
|
if (!manifest.scripts || !manifest.scripts['vscode:prepublish']) {
|
||||||
return resolve(manifest);
|
return resolve(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
const script = manifest.scripts['prepublish'];
|
const script = manifest.scripts['vscode:prepublish'];
|
||||||
console.log(`Executing prepublish script '${ script }'...`);
|
console.log(`Executing prepublish script '${ script }'...`);
|
||||||
|
|
||||||
return nfcall<string>(exec, script, { cwd })
|
return nfcall<string>(exec, script, { cwd })
|
||||||
|
@ -66,29 +68,14 @@ function prepublish(cwd: string, manifest: Manifest): Promise<Manifest> {
|
||||||
function toVsixManifest(manifest: Manifest): Promise<string> {
|
function toVsixManifest(manifest: Manifest): Promise<string> {
|
||||||
return nfcall<string>(fs.readFile, vsixManifestTemplatePath, 'utf8')
|
return nfcall<string>(fs.readFile, vsixManifestTemplatePath, 'utf8')
|
||||||
.then(vsixManifestTemplateStr => _.template(vsixManifestTemplateStr))
|
.then(vsixManifestTemplateStr => _.template(vsixManifestTemplateStr))
|
||||||
.then(vsixManifestTemplate => {
|
.then(vsixManifestTemplate => vsixManifestTemplate({
|
||||||
return getCredentials().then(credentials => {
|
id: manifest.name,
|
||||||
if (credentials) {
|
displayName: manifest.name,
|
||||||
return resolve(credentials.publisher);
|
version: manifest.version,
|
||||||
}
|
publisher: manifest.publisher,
|
||||||
|
description: manifest.description || '',
|
||||||
console.log(`A publisher name is required. Run '${ path.basename(process.argv[1]) } login' to avoid setting it every time.`);
|
tags: (manifest.keywords || []).concat('vscode').join(';')
|
||||||
return read('Publisher name: ');
|
}));
|
||||||
}).then(publisher => {
|
|
||||||
if (!publisher) {
|
|
||||||
return reject<string>('Packaging requires a publisher name.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return vsixManifestTemplate({
|
|
||||||
id: manifest.name,
|
|
||||||
displayName: manifest.name,
|
|
||||||
version: manifest.version,
|
|
||||||
publisher,
|
|
||||||
description: manifest.description || '',
|
|
||||||
tags: (manifest.keywords || []).concat('vscode').join(';')
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultIgnore = ['.vscodeignore', '**/*.vsix', '**/.DS_Store'];
|
const defaultIgnore = ['.vscodeignore', '**/*.vsix', '**/.DS_Store'];
|
||||||
|
|
|
@ -1,43 +1,39 @@
|
||||||
import { readFile } from 'fs';
|
import { readFile } from 'fs';
|
||||||
import { WebApi, getBasicHandler } from 'vso-node-api/WebApi';
|
|
||||||
import { ExtensionQueryFlags, PublishedExtension } from 'vso-node-api/interfaces/GalleryInterfaces';
|
import { ExtensionQueryFlags, PublishedExtension } from 'vso-node-api/interfaces/GalleryInterfaces';
|
||||||
import { nfcall, Promise, reject, resolve, all } from 'q';
|
import { nfcall, Promise, reject } from 'q';
|
||||||
import { pack, IPackageResult } from './package';
|
import { pack } from './package';
|
||||||
import { tmpName } from 'tmp';
|
import { tmpName } from 'tmp';
|
||||||
import { getCredentials } from './login';
|
import { get } from './store';
|
||||||
|
import { getGalleryAPI } from './util';
|
||||||
|
|
||||||
const galleryUrl = 'https://app.market.visualstudio.com';
|
const galleryUrl = 'https://app.market.visualstudio.com';
|
||||||
|
|
||||||
export function publish(cwd = process.cwd()): Promise<any> {
|
export function publish(cwd = process.cwd()): Promise<any> {
|
||||||
return getCredentials({ promptIfMissing: true })
|
return nfcall<string>(tmpName)
|
||||||
.then(credentials => {
|
.then(packagePath => pack(packagePath, cwd))
|
||||||
const authHandler = getBasicHandler('oauth', credentials.pat);
|
.then(result => {
|
||||||
const vsoapi = new WebApi(credentials.account, authHandler);
|
const { manifest, packagePath } = result;
|
||||||
const api = vsoapi.getQGalleryApi(galleryUrl);
|
|
||||||
|
|
||||||
return nfcall<string>(tmpName)
|
return get(manifest.publisher).then(getGalleryAPI).then(api => {
|
||||||
.then(packagePath => pack(packagePath, cwd))
|
return nfcall<string>(readFile, packagePath, 'base64').then(extensionManifest => {
|
||||||
.then(result => {
|
|
||||||
const { manifest, packagePath } = result;
|
|
||||||
const fullName = `${ manifest.name }@${ manifest.version }`;
|
const fullName = `${ manifest.name }@${ manifest.version }`;
|
||||||
|
console.log(`Publishing ${ fullName }...`);
|
||||||
|
|
||||||
return nfcall<string>(readFile, packagePath, 'base64').then(extensionManifest => {
|
return api.getExtension(manifest.publisher, manifest.name, null, ExtensionQueryFlags.IncludeVersions)
|
||||||
console.log(`Publishing ${ fullName }...`);
|
.catch<PublishedExtension>(err => err.statusCode === 404 ? null : reject(err))
|
||||||
return api.getExtension(credentials.publisher, manifest.name, null, ExtensionQueryFlags.IncludeVersions)
|
.then(extension => {
|
||||||
.catch<PublishedExtension>(err => err.statusCode === 404 ? null : reject(err))
|
if (extension && extension.versions.some(v => v.version === manifest.version)) {
|
||||||
.then(extension => {
|
return reject<void>(`${ fullName } already exists.`);
|
||||||
if (extension && extension.versions.some(v => v.version === manifest.version)) {
|
}
|
||||||
return reject<void>(`${ fullName } already exists.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
var promise = extension
|
var promise = extension
|
||||||
? api.updateExtension({ extensionManifest }, credentials.publisher, manifest.name)
|
? api.updateExtension({ extensionManifest }, manifest.publisher, manifest.name)
|
||||||
: api.createExtension({ extensionManifest });
|
: api.createExtension({ extensionManifest });
|
||||||
|
|
||||||
return promise
|
return promise
|
||||||
.catch(err => reject(err.statusCode === 409 ? `${ fullName } already exists.` : err))
|
.catch(err => reject(err.statusCode === 409 ? `${ fullName } already exists.` : err))
|
||||||
.then(() => console.log(`Successfully published ${ fullName }!`));
|
.then(() => console.log(`Successfully published ${ fullName }!`));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
103
src/store.ts
Normal file
103
src/store.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import { Promise, nfcall, resolve, reject } from 'q';
|
||||||
|
import { home } from 'osenv';
|
||||||
|
import { read, getGalleryAPI } from './util';
|
||||||
|
import { validatePublisher } from './validation';
|
||||||
|
|
||||||
|
const storePath = path.join(home(), '.vsce');
|
||||||
|
|
||||||
|
export interface IStore {
|
||||||
|
[publisher: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGetOptions {
|
||||||
|
promptToOverwrite?: boolean;
|
||||||
|
promptIfMissing?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(): Promise<IStore> {
|
||||||
|
return nfcall<string>(fs.readFile, storePath, 'utf8')
|
||||||
|
.catch<string>(err => err.code !== 'ENOENT' ? reject(err) : resolve('{}'))
|
||||||
|
.then<IStore>(rawStore => {
|
||||||
|
try {
|
||||||
|
return resolve(JSON.parse(rawStore));
|
||||||
|
} catch (e) {
|
||||||
|
return reject(`Error parsing store: ${ storePath }`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function save(store: IStore): Promise<IStore> {
|
||||||
|
return nfcall<void>(fs.writeFile, storePath, JSON.stringify(store))
|
||||||
|
.then(() => store);
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPAT(store: IStore, publisher: string): Promise<string> {
|
||||||
|
return read(`Personal Access Token for publisher '${ publisher }':`, { silent: true, replace: '*' })
|
||||||
|
.then(pat => {
|
||||||
|
const api = getGalleryAPI(pat);
|
||||||
|
|
||||||
|
return api.getPublisher(publisher).then(p => {
|
||||||
|
console.log(`Authentication successful. Found publisher '${ p.displayName }'.`);
|
||||||
|
return pat;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(pat => {
|
||||||
|
store[publisher] = pat;
|
||||||
|
return save(store).then(() => pat);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function get(publisher: string): Promise<string> {
|
||||||
|
validatePublisher(publisher);
|
||||||
|
|
||||||
|
return load().then(store => {
|
||||||
|
if (store[publisher]) {
|
||||||
|
return resolve(store[publisher]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return requestPAT(store, publisher);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(publisher: string): Promise<string> {
|
||||||
|
validatePublisher(publisher);
|
||||||
|
|
||||||
|
return load()
|
||||||
|
.then<IStore>(store => {
|
||||||
|
if (store[publisher]) {
|
||||||
|
console.log(`Publisher '${ publisher }' is already known`);
|
||||||
|
return read('Do you want to overwrite its PAT? [y/N] ')
|
||||||
|
.then(answer => /^y$/i.test(answer) ? store : reject('Aborted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolve(store);
|
||||||
|
})
|
||||||
|
.then(store => requestPAT(store, publisher));
|
||||||
|
}
|
||||||
|
|
||||||
|
function rm(publisher: string): Promise<any> {
|
||||||
|
validatePublisher(publisher);
|
||||||
|
|
||||||
|
return load().then(store => {
|
||||||
|
if (!store[publisher]) {
|
||||||
|
return reject(`Unknown publisher '${ publisher }'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete store[publisher];
|
||||||
|
return save(store);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function list(): Promise<string[]> {
|
||||||
|
return load().then(store => Object.keys(store));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function publisher(action: string, publisher: string): Promise<any> {
|
||||||
|
switch (action) {
|
||||||
|
case 'add': return add(publisher);
|
||||||
|
case 'rm': return rm(publisher);
|
||||||
|
case 'list': default: return list().then(publishers => publishers.forEach(p => console.log(p)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,8 @@
|
||||||
import { Promise, nfcall } from 'q';
|
import { Promise, nfcall } from 'q';
|
||||||
import { assign } from 'lodash';
|
import { assign } from 'lodash';
|
||||||
import _read = require('read');
|
import _read = require('read');
|
||||||
|
import { WebApi, getBasicHandler } from 'vso-node-api/WebApi';
|
||||||
|
import { IQGalleryApi } from 'vso-node-api/GalleryApi';
|
||||||
|
|
||||||
export function fatal(message: any, ...args: any[]) {
|
export function fatal(message: any, ...args: any[]) {
|
||||||
if (message instanceof Error) {
|
if (message instanceof Error) {
|
||||||
|
@ -19,3 +21,9 @@ export function read(prompt: string, options: _read.Options = {}): Promise<strin
|
||||||
return nfcall<string>(_read, assign({ prompt }, options))
|
return nfcall<string>(_read, assign({ prompt }, options))
|
||||||
.spread(r => r);
|
.spread(r => r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getGalleryAPI(pat: string): IQGalleryApi {
|
||||||
|
const authHandler = getBasicHandler('oauth', pat);
|
||||||
|
const vsoapi = new WebApi('oauth', authHandler);
|
||||||
|
return vsoapi.getQGalleryApi('https://app.market.visualstudio.com');
|
||||||
|
}
|
9
src/validation.ts
Normal file
9
src/validation.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
export function validatePublisher(publisher: string): void {
|
||||||
|
if (!publisher) {
|
||||||
|
throw new Error(`Missing publisher name`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-z0-9\-]+$/i.test(publisher)) {
|
||||||
|
throw new Error(`Invalid publisher '${ publisher }'`);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue