Skip to content

Commit

Permalink
feat: add list and setup commands
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 22, 2021
1 parent 7e61525 commit 61c4b00
Show file tree
Hide file tree
Showing 13 changed files with 401 additions and 350 deletions.
9 changes: 3 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
# [1.0.0-beta.3](https://github.com/mdonnalley/multiple-package-manager/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2021-08-20)


### Bug Fixes

* use @octokit/core ([dbc52d7](https://github.com/mdonnalley/multiple-package-manager/commit/dbc52d733881e8758884c7ad4efea58bcff9a1b4))
- use @octokit/core ([dbc52d7](https://github.com/mdonnalley/multiple-package-manager/commit/dbc52d733881e8758884c7ad4efea58bcff9a1b4))

# [1.0.0-beta.2](https://github.com/mdonnalley/multiple-package-manager/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2021-08-20)


### Features

* add initial set of commands ([69b6f54](https://github.com/mdonnalley/multiple-package-manager/commit/69b6f5496952bc71ce43def76ff2b425077d9c13))
- add initial set of commands ([69b6f54](https://github.com/mdonnalley/multiple-package-manager/commit/69b6f5496952bc71ce43def76ff2b425077d9c13))

# 1.0.0-beta.1 (2021-08-20)


### Features

* intial release ([0a31f71](https://github.com/mdonnalley/multiple-package-manager/commit/0a31f7156fd846f8cd5007591a3597fd724812ac))
- intial release ([0a31f71](https://github.com/mdonnalley/multiple-package-manager/commit/0a31f7156fd846f8cd5007591a3597fd724812ac))
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
"dependencies": {
"@oclif/core": "^0.5.31",
"@octokit/core": "^3.5.1",
"@salesforce/kit": "^1.5.17",
"chalk": "^4.1.2",
"cli-ux": "^5.6.3",
"inquirer": "^8.1.2",
"lodash": "^4.17.21",
"open": "^8.2.1",
"shelljs": "^0.8.4",
"tslib": "^2"
Expand All @@ -24,6 +27,8 @@
"@commitlint/config-conventional": "^13.1.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@types/inquirer": "^7.3.3",
"@types/lodash": "^4.14.172",
"@types/node": "^16.6.2",
"@types/shelljs": "^0.8.9",
"@typescript-eslint/eslint-plugin": "^4.28.1",
Expand Down Expand Up @@ -60,6 +65,7 @@
"test": "mocha \"test/**/*.test.ts\" --forbid-only",
"version": "oclif readme && git add README.md"
},
"bin": "mpm",
"engines": {
"node": ">=12.0.0"
},
Expand Down
103 changes: 48 additions & 55 deletions src/commands/add.ts
Original file line number Diff line number Diff line change
@@ -1,79 +1,72 @@
import * as path from 'path';
import { mkdir, access, writeFile, readFile } from 'fs/promises';
import { Octokit } from '@octokit/core';
import { mkdir } from 'fs/promises';
import { URL } from 'url';
import { exec } from 'shelljs';
import * as chalk from 'chalk';
import { Command, Flags } from '@oclif/core';
import { MPM_DIR } from '../util';
import { Config } from '../config';
import { Repos } from '../repos';
import { Directory } from '../directory';

async function initDir(directory: string): Promise<void> {
try {
await access(directory);
} catch {
await mkdir(directory, { recursive: true });
}
}

function getToken(): string {
const token = process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
if (!token) {
throw new Error('GH_TOKEN or GITHUB_TOKEN must be set in the environment');
}
return token;
}

function clone(url: string, dir: string): void {
async function clone(url: string, dir: string): Promise<void> {
await mkdir(dir, { recursive: true });
exec(`git -C ${dir} clone ${url}`, { silent: true });
}

async function addToCache(contents: Record<string, unknown>): Promise<void> {
const filepath = path.join(MPM_DIR, 'repos.json');
let existing: Record<string, unknown> = {};
try {
existing = JSON.parse(await readFile(filepath, 'utf-8')) as Record<string, unknown>;
} catch {
// do nothing
function parseOrgAndRepo(entity: string): { org: string; repo: string | null } {
if (entity.startsWith('https://')) {
const url = new URL(entity);
const pathParts = url.pathname.split('/').filter((p) => !!p);
// ex: https://github.com/my-org
if (pathParts.length === 1) return { org: pathParts[0], repo: null };
// ex: https://github.com/my-org/my-repo
else return { org: pathParts[0], repo: pathParts[1] };
} else {
const parts = entity.split('/').filter((e) => !!e);
if (parts.length === 1) {
// ex: my-org
return { org: entity, repo: null };
} else {
// ex: my-org/my-repo
return { org: parts[0], repo: parts[1] };
}
}
const merged = Object.assign(existing, contents);
await writeFile(filepath, JSON.stringify(merged, null, 2));
}

export class Add extends Command {
public static readonly description = 'Add a github org. Requires GH_TOKEN to be set in the environment';
public static readonly description = 'Add a github org. Requires GH_TOKEN to be set in the environment.';
public static readonly flags = {
directory: Flags.string({
description: 'location to clone repo',
char: 'd',
required: true,
}),
'github-org': Flags.string({
description: 'github org to clone',
char: 'g',
required: true,
}),
method: Flags.string({
description: 'method to use for cloning',
description: 'Method to use for cloning.',
default: 'ssh',
options: ['ssh', 'https'],
}),
};

public static readonly args = [
{
name: 'entity',
description: 'Github org, repo, or url to add',
required: true,
},
];

public async run(): Promise<void> {
const { flags } = await this.parse(Add);
const directory = path.resolve(flags.directory);
await initDir(directory);
await initDir(MPM_DIR);
const octokit = new Octokit({ auth: getToken() });
const repos = await octokit.request('GET /orgs/{org}/repos', { org: flags['github-org'] });
this.log(`Cloning all ${chalk.cyan.bold(flags['github-org'])} repositories into ${flags.directory}`);
const cached = {} as Record<string, unknown>;
for (const repo of repos.data) {
const { flags, args } = await this.parse(Add);
const config = await Config.create();
const directory = await Directory.create({ name: config.get('directory') });
const repos = await Repos.create();

const info = parseOrgAndRepo(args.entity);
const repositories = await repos.fetch(info.org, info.repo);
this.log(`Cloning repositories into ${path.join(directory.name, info.org)}`);
for (const repo of repositories) {
const url = flags.method === 'ssh' ? repo.ssh_url : repo.clone_url;
this.log(` * ${chalk.bold(url)}`);
clone(url, flags.directory);
cached[repo.name] = repo;
break;
this.log(` * ${chalk.bold(repo.name)}`);
const dir = path.join(directory.name, repo.owner.login);
await clone(url, dir);
repos.set(repo.name, repo);
}
await addToCache(cached);
await repos.write();
}
}
24 changes: 24 additions & 0 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Command } from '@oclif/core';
import * as chalk from 'chalk';
import { cli } from 'cli-ux';
import { groupBy, sortBy } from 'lodash';
import { Repos } from '../repos';

export class List extends Command {
public static readonly description = 'List all added repositories.';
public static readonly flags = {};

public async run(): Promise<void> {
const repositories = (await Repos.create()).getContents();
const grouped = groupBy(repositories, 'owner.login');
for (const [org, repos] of Object.entries(grouped)) {
const columns = {
name: { header: 'Name' },
html_url: { header: 'URL' },
};
const sorted = sortBy(Object.values(repos), 'name');
cli.table(sorted, columns, { title: chalk.cyan.bold(`${org} Respositories`) });
this.log();
}
}
}
8 changes: 4 additions & 4 deletions src/commands/open.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import { Command } from '@oclif/core';
import * as open from 'open';
import { readRepo } from '../util';
import { Repos } from '../repos';

export class Open extends Command {
public static readonly description = 'Open a github repository';
public static readonly description = 'Open a github repository.';
public static readonly flags = {};
public static readonly args = [
{
name: 'repo',
description: 'Name of repository',
description: 'Name of repository.',
required: true,
},
];

public async run(): Promise<void> {
const { args } = await this.parse(Open);
const repo = await readRepo(args.repo);
const repo = (await Repos.create()).get(args.repo);
await open(repo.html_url, { wait: false });
}
}
34 changes: 34 additions & 0 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import * as os from 'os';
import * as path from 'path';
import { Command, Flags } from '@oclif/core';
import { prompt } from 'inquirer';
import { Config } from '../config';

export class Setup extends Command {
public static readonly description = 'Setup mpm';
public static readonly flags = {
directory: Flags.string({
description: 'Location to setup repositories.',
char: 'd',
}),
};

public async run(): Promise<void> {
const { flags } = await this.parse(Setup);
const config = await Config.create();
if (!flags.directory) {
const answers = await prompt<{ directory: string }>({
name: 'directory',
type: 'input',
message: 'Where would you link to clone your repositories?',
default: Config.DEFAULT_DIRECTORY,
});
config.set('directory', path.resolve(answers.directory.replace('~', os.homedir())));
} else {
config.set('directory', flags.directory);
}
await config.write();

this.log(`All repositories will be cloned into ${config.get('directory')}`);
}
}
9 changes: 5 additions & 4 deletions src/commands/view.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { cli } from 'cli-ux';
import { Command } from '@oclif/core';
import { readRepo } from '../util';
import { Repos } from '../repos';

export class View extends Command {
public static readonly description = 'View a github repository';
public static readonly description = 'View a github repository.';
public static readonly flags = {};
public static readonly args = [
{
name: 'repo',
description: 'Name of repository',
description: 'Name of repository.',
required: true,
},
];

public async run(): Promise<void> {
const { args } = await this.parse(View);
const repo = await readRepo(args.repo);
const repos = await Repos.create();
const repo = repos.get(args.repo);
const columns = { key: {}, value: {} };
const data = [
{ key: 'name', value: repo.name },
Expand Down
21 changes: 21 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as os from 'os';
import * as path from 'path';
import { ConfigFile, JsonMap } from './configFile';

export interface Configuration extends JsonMap {
directory: string;
}

export class Config extends ConfigFile<Configuration> {
public static DEFAULT_DIRECTORY = path.join(os.homedir(), 'repos');

private static DEFAULT_CONFIG: Configuration = { directory: Config.DEFAULT_DIRECTORY };

public constructor() {
super('config.json');
}

protected make(): Configuration {
return Config.DEFAULT_CONFIG;
}
}
60 changes: 60 additions & 0 deletions src/configFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { readFile, writeFile } from 'fs/promises';
import * as os from 'os';
import * as path from 'path';
import { AsyncOptionalCreatable } from '@salesforce/kit';
import { exists } from './util';
import { Directory } from './directory';

export interface JsonMap<T = unknown> {
[key: string]: T;
}

export abstract class ConfigFile<T extends JsonMap> extends AsyncOptionalCreatable<string> {
public static MPM_DIR_NAME = '.mpm';
public static MPM_DIR = path.join(os.homedir(), ConfigFile.MPM_DIR_NAME);

private contents: T;
private filepath: string;

public constructor(filename: string) {
super(filename);
this.filepath = path.join(ConfigFile.MPM_DIR, filename);
}

public async read(): Promise<T> {
if (await exists(this.filepath)) {
const config = JSON.parse(await readFile(this.filepath, 'utf-8')) as T;
this.contents = config;
return this.contents;
} else {
this.contents = this.make();
await this.write();
return this.contents;
}
}

public async write(newContents: T = this.contents): Promise<void> {
await writeFile(this.filepath, JSON.stringify(newContents, null, 2));
}

public get(key: keyof T): T[keyof T] {
return this.contents[key];
}

public getContents(): T {
return this.contents;
}

public set(key: keyof T, value: T[keyof T]): void {
this.contents[key] = value;
}

protected async init(): Promise<void> {
await Directory.create({ name: ConfigFile.MPM_DIR });
this.contents = await this.read();
}

protected make(): T {
return {} as T;
}
}
Loading

0 comments on commit 61c4b00

Please sign in to comment.