Skip to content

Commit

Permalink
Merge pull request #215 from raix/feature/issue-214-add-show-command
Browse files Browse the repository at this point in the history
Feature/issue 214 add show command
  • Loading branch information
joaomoreno committed Nov 20, 2017
2 parents ab79786 + 32c7a0e commit bf162ac
Show file tree
Hide file tree
Showing 6 changed files with 225 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as program from 'commander';
import { packageCommand, ls } from './package';
import { publish, list, unpublish } from './publish';
import { show } from './show';
import { listPublishers, createPublisher, deletePublisher, loginPublisher, logoutPublisher } from './store';
import { getLatestVersion } from './npm';
import { CancellationToken, isCancelledError } from './util';
Expand Down Expand Up @@ -112,6 +113,12 @@ module.exports = function (argv: string[]): void {
.description('Remove a publisher from the known publishers list')
.action(name => main(logoutPublisher(name)));

program
.command('show <extensionid>')
.option('--json', 'Output data in json format', false)
.description('Show extension metadata')
.action((extensionid, { json }) => main(show(extensionid, json)));

program
.command('*')
.action(() => program.help());
Expand Down
63 changes: 63 additions & 0 deletions src/publicgalleryapi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { HttpClient, HttpClientResponse } from 'vso-node-api/HttpClient';
import { PublishedExtension, ExtensionQueryFlags, FilterCriteria, SortOrderType,
SortByType, ExtensionQueryFilterType, TypeInfo} from 'vso-node-api/interfaces/GalleryInterfaces';
import { IHeaders } from 'vso-node-api/interfaces/common/VsoBaseInterfaces';
import { ContractSerializer } from 'vso-node-api/Serialization';

export interface ExtensionQuery {
pageNumber?: number;
pageSize?: number;
sortBy?: SortByType;
sortOrder?: SortOrderType;
flags?: ExtensionQueryFlags[];
criteria?: FilterCriteria[];
assetTypes?: string[];
}

export class PublicGalleryAPI {
client: HttpClient;

constructor(public baseUrl: string, public apiVersion = '3.0-preview.1') {
this.client = new HttpClient('vsce');
}

post(url: string, data: string, additionalHeaders?: IHeaders): Promise<HttpClientResponse> {
return this.client.post(`${this.baseUrl}/_apis/public${url}`, data, additionalHeaders);
}

extensionQuery({
pageNumber = 1,
pageSize = 1,
sortBy = SortByType.Relevance,
sortOrder = SortOrderType.Default,
flags = [],
criteria = [],
assetTypes = [],
}: ExtensionQuery): Promise<PublishedExtension[]> {
return this.post('/gallery/extensionquery', JSON.stringify({
filters: [{pageNumber, pageSize, criteria}],
assetTypes,
flags: flags.reduce((memo, flag) => memo | flag, 0)
}), {
Accept: `application/json;api-version=${this.apiVersion}`,
'Content-Type': 'application/json',
})
.then(res => res.readBody())
.then(data => JSON.parse(data))
.then(({results: [result = {}] = []}) => result)
.then(({extensions = []}) =>
ContractSerializer.deserialize(extensions, TypeInfo.PublishedExtension, false, false)
);
}

getExtension(extensionId: string, flags: ExtensionQueryFlags[] = []): Promise<PublishedExtension> {
return this.extensionQuery({
criteria: [{ filterType: ExtensionQueryFilterType.Name, value: extensionId }],
flags,
})
.then(result => result.filter(({publisher: {publisherName}, extensionName}) =>
extensionId === `${publisherName}.${extensionName}`)
)
.then(([extension]) => extension);
}
}
99 changes: 99 additions & 0 deletions src/show.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { getPublicGalleryAPI } from './util';
import { ExtensionQueryFlags, PublishedExtension } from 'vso-node-api/interfaces/GalleryInterfaces';
import { ViewTable, formatDate, formatDateTime, ratingStars, tableView, indentRow, wordWrap } from './viewutils';

const limitVersions = 6;

export interface ExtensionStatiticsMap {
install: number;
averagerating: number;
ratingcount:number;
}

export function show(extensionId: string, json: boolean = false): Promise<any> {
const flags = [
ExtensionQueryFlags.IncludeCategoryAndTags,
ExtensionQueryFlags.IncludeMetadata,
ExtensionQueryFlags.IncludeStatistics,
ExtensionQueryFlags.IncludeVersions,
];
return getPublicGalleryAPI()
.getExtension(extensionId, flags)
.then(extension => {
if (json) {
console.log(JSON.stringify(extension, undefined, '\t'));
} else {
if (extension === undefined) {
console.log(`Error: Extension "${extensionId}" not found.`);
} else {
showOverview(extension);
}
}
});
}

function showOverview({
displayName,
extensionName,
shortDescription,
versions,
publisher: {
displayName:publisherDisplayName,
publisherName
},
categories,
tags,
statistics,
publishedDate,
lastUpdated,
}: PublishedExtension) {

const [{ version = 'unknown' } = {}] = versions;

// Create formatted table list of versions
const versionList = <ViewTable>versions
.slice(0, limitVersions)
.map(({version, lastUpdated}) => [version, formatDate(lastUpdated)]);

const {
install: installs = 0,
averagerating = 0,
ratingcount = 0,
} = statistics
.reduce((map, {statisticName, value}) => ({ ...map, [statisticName]: value }), <ExtensionStatiticsMap>{});

// Render
console.log([
`${displayName}`,
`${publisherDisplayName} | ${'\u2913'}` +
`${Number(installs).toLocaleString()} installs |` +
` ${ratingStars(averagerating)} (${ratingcount})`,
'',
`${shortDescription}`,
'',
'Recent versions:',
...(versionList.length ? tableView(versionList).map(indentRow) : ['no versions found']),
'',
'Categories:',
` ${categories.join(', ')}`,
'',
'Tags:',
` ${tags.join(', ')}`,
'',
'More info:',
...tableView([
[ 'Uniq identifier:', `${publisherName}.${extensionName}` ],
[ 'Version:', version ],
[ 'Last updated:', formatDateTime(lastUpdated) ],
[ 'Publisher:', publisherDisplayName ],
[ 'Published at:', formatDate(publishedDate) ],
])
.map(indentRow),
'',
'Statistics:',
...tableView(<ViewTable>statistics.map(({statisticName, value}) => [statisticName, Number(value).toFixed(2)]))
.map(indentRow),
]
.map(line => wordWrap(line))
.join('\n'));
}
2 changes: 1 addition & 1 deletion src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,4 @@ export function listPublishers(): Promise<void> {
return load()
.then(store => store.publishers)
.then(publishers => publishers.forEach(p => console.log(p.name)));
}
}
7 changes: 6 additions & 1 deletion src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as _read from 'read';
import { WebApi, getBasicHandler } from 'vso-node-api/WebApi';
import { IGalleryApi } from 'vso-node-api/GalleryApi';
import * as denodeify from 'denodeify';
import { PublicGalleryAPI } from './publicgalleryapi';

const __read = denodeify<_read.Options, string>(_read);
export function read(prompt: string, options: _read.Options = {}): Promise<string> {
Expand All @@ -14,6 +15,10 @@ export function getGalleryAPI(pat: string): IGalleryApi {
return vsoapi.getGalleryApi('https://marketplace.visualstudio.com');
}

export function getPublicGalleryAPI() {
return new PublicGalleryAPI('https://marketplace.visualstudio.com', '3.0-preview.1');
}

export function normalize(path: string): string {
return path.replace(/\\/g, '/');
}
Expand Down Expand Up @@ -67,4 +72,4 @@ export class CancellationToken {
this.listeners = [];
}
}
}
}
49 changes: 49 additions & 0 deletions src/viewutils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export type ViewTableRow = string[];
export type ViewTable = ViewTableRow[];

const fixedLocale = 'en-us';
const format = {
date: { month: 'long', day: 'numeric', year: 'numeric' },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
};

export function formatDate(date) { return date.toLocaleString(fixedLocale, format.date); }
export function formatTime(date) { return date.toLocaleString(fixedLocale, format.time); }
export function formatDateTime(date) { return date.toLocaleString(fixedLocale, { ...format.date, ...format.time }); }

export function repeatString(text: string, count: number): string {
let result: string = '';
for (let i = 0; i < count; i++) {
result += text;
}
return result;
}

export function ratingStars(rating: number, total = 5): string {
const c = Math.min(Math.round(rating), total);
return `${repeatString('\u{2605} ', c)}${repeatString('\u{2606} ', total - c)}`;
}

export function tableView(table: ViewTable, spacing: number = 2): string[] {
const maxLen = {};
table.forEach(row => row.forEach((cell, i) => maxLen[i] = Math.max(maxLen[i] || 0, cell.length)));
return table.map(row => row.map((cell, i) => `${cell}${repeatString(' ', maxLen[i] - cell.length + spacing)}`).join(''));
}

export function wordWrap(text: string, width: number = 80): string {
const [indent = ''] = text.match(/^\s+/) || [];
const maxWidth = width - indent.length;
return text
.replace(/^\s+/, '')
.split('')
.reduce(([out, buffer, pos], ch, i) => {
const nl = pos === maxWidth ? `\n${indent}` : '';
const newPos: number = nl ? 0 : +pos + 1;
return / |-|,|\./.test(ch) ?
[`${out}${buffer}${ch}${nl}`, '', newPos] : [`${out}${nl}`, buffer+ch, newPos];
}, [indent, '', 0])
.slice(0, 2)
.join('');
};

export function indentRow(row: string) { return ` ${row}`; };

0 comments on commit bf162ac

Please sign in to comment.