Merge remote-tracking branch 'vso/master'

This commit is contained in:
Joao Moreno 2015-09-25 15:20:11 +02:00
commit d28cdad3d7
8 changed files with 182 additions and 168 deletions

View file

@ -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();
}

View file

@ -2,7 +2,7 @@ import * as minimist from 'minimist';
import { pack } from './package';
import { publish } from './publish';
import { fatal } from './util';
import { login, logout } from './login';
import { publisher } from './store';
const packagejson = require('../package.json');
function helpCommand(): void {
@ -11,8 +11,9 @@ function helpCommand(): void {
Commands:
package [vsix path] Packages the extension into a .vsix package
publish Publishes the extension
login Logs in to the extension service
logout Logs out of the extension service
publisher add [publisher] Add a publisher
publisher rm [publisher] Remove a publisher
publisher list List all added publishers
Global options:
--help, -h Display help
@ -27,22 +28,26 @@ function versionCommand(): void {
}
function command(args: minimist.ParsedArgs): boolean {
const promise = (() => {
switch (args._[0]) {
case 'package': return pack(args._[1]).then(({ packagePath }) => console.log(`Package created: ${ packagePath }`));
case 'login': return login();
case 'logout': return logout();
case 'publish': return publish(args._[1]);
default: return null;
try {
const promise = (() => {
switch (args._[0]) {
case 'package': return pack(args._[1]).then(({ packagePath }) => console.log(`Package created: ${ packagePath }`));
case 'publisher': return publisher(args._[1], args._[2]);
case 'publish': return publish(args._[1]);
default: return null;
}
})();
if (promise) {
promise.catch(fatal);
return true;
}
})();
if (promise) {
promise.catch(fatal);
return false;
} catch (e) {
fatal(e);
return true;
}
return false;
}
module.exports = function (argv: string[]): void {

View file

@ -11,6 +11,7 @@ export interface Manifest {
engines: { vscode: string; [name: string]: string; };
// vscode
publisher: string;
contributes?: { [contributionType: string]: any; };
activationEvents?: string[];
extensionDependencies?: string[];

View file

@ -3,11 +3,9 @@ import * as path from 'path';
import * as _ from 'lodash';
import * as yazl from 'yazl';
import { Manifest } from './manifest';
import { getCredentials } from './login';
import { nfcall, Promise, reject, resolve, all } from 'q';
import * as glob from 'glob';
import * as minimatch from 'minimatch';
import { read } from './util';
import { exec } from 'child_process';
const resourcesPath = path.join(path.dirname(__dirname), 'resources');
@ -28,6 +26,10 @@ function readManifest(cwd: string): Promise<Manifest> {
}
function validateManifest(manifest: Manifest): Promise<Manifest> {
if (!manifest.publisher) {
return reject<Manifest>('Manifest missing field: publisher');
}
if (!manifest.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> {
if (!manifest.scripts || !manifest.scripts['prepublish']) {
if (!manifest.scripts || !manifest.scripts['vscode:prepublish']) {
return resolve(manifest);
}
const script = manifest.scripts['prepublish'];
const script = manifest.scripts['vscode:prepublish'];
console.log(`Executing prepublish script '${ script }'...`);
return nfcall<string>(exec, script, { cwd })
@ -66,29 +68,14 @@ function prepublish(cwd: string, manifest: Manifest): Promise<Manifest> {
function toVsixManifest(manifest: Manifest): Promise<string> {
return nfcall<string>(fs.readFile, vsixManifestTemplatePath, 'utf8')
.then(vsixManifestTemplateStr => _.template(vsixManifestTemplateStr))
.then(vsixManifestTemplate => {
return getCredentials().then(credentials => {
if (credentials) {
return resolve(credentials.publisher);
}
console.log(`A publisher name is required. Run '${ path.basename(process.argv[1]) } login' to avoid setting it every time.`);
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(';')
});
});
});
.then(vsixManifestTemplate => vsixManifestTemplate({
id: manifest.name,
displayName: manifest.name,
version: manifest.version,
publisher: manifest.publisher,
description: manifest.description || '',
tags: (manifest.keywords || []).concat('vscode').join(';')
}));
}
const defaultIgnore = ['.vscodeignore', '**/*.vsix', '**/.DS_Store'];

View file

@ -1,43 +1,39 @@
import { readFile } from 'fs';
import { WebApi, getBasicHandler } from 'vso-node-api/WebApi';
import { ExtensionQueryFlags, PublishedExtension } from 'vso-node-api/interfaces/GalleryInterfaces';
import { nfcall, Promise, reject, resolve, all } from 'q';
import { pack, IPackageResult } from './package';
import { nfcall, Promise, reject } from 'q';
import { pack } from './package';
import { tmpName } from 'tmp';
import { getCredentials } from './login';
import { get } from './store';
import { getGalleryAPI } from './util';
const galleryUrl = 'https://app.market.visualstudio.com';
export function publish(cwd = process.cwd()): Promise<any> {
return getCredentials({ promptIfMissing: true })
.then(credentials => {
const authHandler = getBasicHandler('oauth', credentials.pat);
const vsoapi = new WebApi(credentials.account, authHandler);
const api = vsoapi.getQGalleryApi(galleryUrl);
return nfcall<string>(tmpName)
.then(packagePath => pack(packagePath, cwd))
.then(result => {
const { manifest, packagePath } = result;
return nfcall<string>(tmpName)
.then(packagePath => pack(packagePath, cwd))
.then(result => {
const { manifest, packagePath } = result;
return get(manifest.publisher).then(getGalleryAPI).then(api => {
return nfcall<string>(readFile, packagePath, 'base64').then(extensionManifest => {
const fullName = `${ manifest.name }@${ manifest.version }`;
console.log(`Publishing ${ fullName }...`);
return nfcall<string>(readFile, packagePath, 'base64').then(extensionManifest => {
console.log(`Publishing ${ fullName }...`);
return api.getExtension(credentials.publisher, manifest.name, null, ExtensionQueryFlags.IncludeVersions)
.catch<PublishedExtension>(err => err.statusCode === 404 ? null : reject(err))
.then(extension => {
if (extension && extension.versions.some(v => v.version === manifest.version)) {
return reject<void>(`${ fullName } already exists.`);
}
var promise = extension
? api.updateExtension({ extensionManifest }, credentials.publisher, manifest.name)
: api.createExtension({ extensionManifest });
return promise
.catch(err => reject(err.statusCode === 409 ? `${ fullName } already exists.` : err))
.then(() => console.log(`Successfully published ${ fullName }!`));
});
return api.getExtension(manifest.publisher, manifest.name, null, ExtensionQueryFlags.IncludeVersions)
.catch<PublishedExtension>(err => err.statusCode === 404 ? null : reject(err))
.then(extension => {
if (extension && extension.versions.some(v => v.version === manifest.version)) {
return reject<void>(`${ fullName } already exists.`);
}
var promise = extension
? api.updateExtension({ extensionManifest }, manifest.publisher, manifest.name)
: api.createExtension({ extensionManifest });
return promise
.catch(err => reject(err.statusCode === 409 ? `${ fullName } already exists.` : err))
.then(() => console.log(`Successfully published ${ fullName }!`));
});
});
});
});

103
src/store.ts Normal file
View 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)));
}
}

View file

@ -1,6 +1,8 @@
import { Promise, nfcall } from 'q';
import { assign } from 'lodash';
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[]) {
if (message instanceof Error) {
@ -18,4 +20,10 @@ export function fatal(message: any, ...args: any[]) {
export function read(prompt: string, options: _read.Options = {}): Promise<string> {
return nfcall<string>(_read, assign({ prompt }, options))
.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
View 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 }'`);
}
}