diff --git a/src/login.ts b/src/login.ts deleted file mode 100644 index 77450eb..0000000 --- a/src/login.ts +++ /dev/null @@ -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 { - return nfcall(fs.readFile, credentialsPath, 'utf8') - .catch(err => err.code !== 'ENOENT' ? reject(err) : resolve('null')) - .then(credentialsStr => { - try { - return resolve(JSON.parse(credentialsStr)); - } catch (e) { - return reject(`Error parsing credentials: ${ credentialsPath }`); - } - }); -} - -function writeCredentials(credentials: ICredentials): Promise { - return nfcall(fs.writeFile, credentialsPath, JSON.stringify(credentials)) - .then(() => credentials); -} - -function clearCredentials(): Promise { - return nfcall(fs.unlink, credentialsPath) - .catch(err => err.code !== 'ENOENT' ? reject(err) : resolve('null')); -} - -function promptForCredentials(): Promise { - 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 { - 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(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 { - 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 { - return clearCredentials(); -} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index b0300a0..b099e39 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 { diff --git a/src/package.ts b/src/package.ts index d4c623e..5d88f89 100644 --- a/src/package.ts +++ b/src/package.ts @@ -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'); diff --git a/src/publish.ts b/src/publish.ts index 9c6a673..5429081 100644 --- a/src/publish.ts +++ b/src/publish.ts @@ -1,27 +1,21 @@ import { readFile } from 'fs'; -import { WebApi, getBasicHandler } from 'vso-node-api/WebApi'; -import { IQGalleryApi } from 'vso-node-api/GalleryApi'; 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, ICredentials } from './login'; +import { get } from './store'; +import { getGalleryAPI } from './util'; const galleryUrl = 'https://app.market.visualstudio.com'; -function getGalleryAPI({ account, pat }: ICredentials): IQGalleryApi { - const authHandler = getBasicHandler('oauth', pat); - const vsoapi = new WebApi(account, authHandler); - return vsoapi.getQGalleryApi(galleryUrl); -} - export function publish(cwd = process.cwd()): Promise { - return getCredentials({ promptIfMissing: true }) - .then(getGalleryAPI) - .then(api => nfcall(tmpName) - .then(packagePath => pack(packagePath, cwd)) - .then(({ manifest, packagePath }) => nfcall(readFile, packagePath, 'base64') - .then(extensionManifest => { + return nfcall(tmpName) + .then(packagePath => pack(packagePath, cwd)) + .then(result => { + const { manifest, packagePath } = result; + + return get(manifest.publisher).then(getGalleryAPI).then(api => { + return nfcall(readFile, packagePath, 'base64').then(extensionManifest => { const fullName = `${ manifest.name }@${ manifest.version }`; console.log(`Publishing ${ fullName }...`); @@ -40,7 +34,7 @@ export function publish(cwd = process.cwd()): Promise { .catch(err => reject(err.statusCode === 409 ? `${ fullName } already exists.` : err)) .then(() => console.log(`Successfully published ${ fullName }!`)); }); - }) - ) - ); + }); + }); + }); }; \ No newline at end of file diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 0000000..fd30612 --- /dev/null +++ b/src/store.ts @@ -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 { + return nfcall(fs.readFile, storePath, 'utf8') + .catch(err => err.code !== 'ENOENT' ? reject(err) : resolve('{}')) + .then(rawStore => { + try { + return resolve(JSON.parse(rawStore)); + } catch (e) { + return reject(`Error parsing store: ${ storePath }`); + } + }); +} + +function save(store: IStore): Promise { + return nfcall(fs.writeFile, storePath, JSON.stringify(store)) + .then(() => store); +} + +function requestPAT(store: IStore, publisher: string): Promise { + 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 { + validatePublisher(publisher); + + return load().then(store => { + if (store[publisher]) { + return resolve(store[publisher]); + } + + return requestPAT(store, publisher); + }); +} + +function add(publisher: string): Promise { + validatePublisher(publisher); + + return load() + .then(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 { + validatePublisher(publisher); + + return load().then(store => { + if (!store[publisher]) { + return reject(`Unknown publisher '${ publisher }'`); + } + + delete store[publisher]; + return save(store); + }); +} + +function list(): Promise { + return load().then(store => Object.keys(store)); +} + +export function publisher(action: string, publisher: string): Promise { + 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))); + } +} \ No newline at end of file diff --git a/src/util.ts b/src/util.ts index 96b58e5..c51e26a 100644 --- a/src/util.ts +++ b/src/util.ts @@ -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 { return nfcall(_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'); } \ No newline at end of file diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 0000000..9dbdcb9 --- /dev/null +++ b/src/validation.ts @@ -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 }'`); + } +} \ No newline at end of file