From 4e974ca2ab3b0e7e30086482746763949a68a38d Mon Sep 17 00:00:00 2001 From: Mike Donnalley Date: Mon, 23 Aug 2021 09:20:31 -0600 Subject: [PATCH] feat: add cd command --- src/autocomplete.ts | 17 ++++++++-------- src/bashRc.ts | 44 +++++++++++++++++++++++++++++++++++++++++ src/commands/cd.ts | 23 ++++++++++++++++++++++ src/commands/list.ts | 2 +- src/commands/setup.ts | 2 ++ src/commands/where.ts | 32 ++++++++++++++++++++++++++++++ src/config.ts | 4 ++-- src/configFile.ts | 46 +++++++++++++++++++++++++------------------ src/mpmCd.ts | 39 ++++++++++++++++++++++++++++++++++++ src/repos.ts | 23 +++++++++------------- 10 files changed, 188 insertions(+), 44 deletions(-) create mode 100644 src/bashRc.ts create mode 100644 src/commands/cd.ts create mode 100644 src/commands/where.ts create mode 100644 src/mpmCd.ts diff --git a/src/autocomplete.ts b/src/autocomplete.ts index cfe53549..d6901309 100644 --- a/src/autocomplete.ts +++ b/src/autocomplete.ts @@ -1,9 +1,9 @@ import * as path from 'path'; import * as os from 'os'; -import { writeFile, appendFile } from 'fs/promises'; -import { exec } from 'shelljs'; +import { writeFile } from 'fs/promises'; import { AsyncCreatable } from '@salesforce/kit'; import { ConfigFile } from './configFile'; +import { BashRc } from './bashRc'; const AUTO_COMPLETE_TEMPLATE = ` #/usr/bin/env bash @@ -14,7 +14,7 @@ _repo_completions() COMPREPLY=() cur=\${COMP_WORDS[COMP_CWORD]} code_dir=@CODE_DIRECTORY@ - COMPREPLY=($( compgen -W "$(ls -d $code_dir/*/ | cut -d "/" -f 5)" -- $cur ) ) + COMPREPLY=($( compgen -W "$(ls -d $code_dir/**/* | xargs basename)" -- $cur )) } `; @@ -22,21 +22,22 @@ const COMPLETE = 'complete -F _repo_completions mpm'; export class AutoComplete extends AsyncCreatable { public static LOCATION = path.join(ConfigFile.MPM_DIR, 'autocomplete.bash'); - public static COMMANDS = ['view', 'open']; + public static COMMANDS = ['view', 'open', 'cd']; public constructor(private directory: string) { super(directory); } protected async init(): Promise { if (process.platform === 'win32') return; - + const bashRc = await BashRc.create(); let contents = AUTO_COMPLETE_TEMPLATE.replace('@CODE_DIRECTORY@', this.directory); for (const cmd of AutoComplete.COMMANDS) { contents += `${COMPLETE} ${cmd}${os.EOL}`; } await writeFile(AutoComplete.LOCATION, contents); - const bashrcPath = path.join(os.homedir(), '.bashrc'); - await appendFile(bashrcPath, `source ${AutoComplete.LOCATION}`); - exec(`source ${bashrcPath}`); + + bashRc.append(`source ${AutoComplete.LOCATION}`); + await bashRc.write(); + bashRc.source(); } } diff --git a/src/bashRc.ts b/src/bashRc.ts new file mode 100644 index 00000000..bfa57d82 --- /dev/null +++ b/src/bashRc.ts @@ -0,0 +1,44 @@ +import * as path from 'path'; +import * as os from 'os'; +import { writeFile, readFile } from 'fs/promises'; +import { exec } from 'shelljs'; +import { AsyncOptionalCreatable } from '@salesforce/kit'; + +export class BashRc extends AsyncOptionalCreatable { + public static LOCATION = path.join(os.homedir(), '.bashrc'); + public static COMMANDS = ['view', 'open', 'cd']; + + private contents!: string; + + public constructor() { + super(); + } + + public async read(): Promise { + this.contents = await readFile(BashRc.LOCATION, 'utf-8'); + return this.contents; + } + + public async write(): Promise { + await writeFile(BashRc.LOCATION, this.contents); + } + + public has(str: string): boolean { + return this.contents.includes(str); + } + + public append(str: string): void { + if (!this.has(str)) { + this.contents += `${os.EOL}${str}`; + } + } + + public source(): void { + exec(`source ${BashRc.LOCATION}`); + } + + protected async init(): Promise { + if (process.platform === 'win32') return; + await this.read(); + } +} diff --git a/src/commands/cd.ts b/src/commands/cd.ts new file mode 100644 index 00000000..dd6352bc --- /dev/null +++ b/src/commands/cd.ts @@ -0,0 +1,23 @@ +import { Command } from '@oclif/core'; + +export class Cd extends Command { + public static readonly description = 'cd into a github repository.'; + public static readonly flags = {}; + public static readonly args = [ + { + name: 'repo', + description: 'Name of repository.', + required: true, + }, + ]; + + public async run(): Promise { + /** + * Do nothing. The cd command is written to .bashrc on setup since we cannot change the directory + * of the executing shell - instead we must use bash. This command is here just so that it shows up in + * the help output. + * + * See the MpmCd class for more context. + */ + } +} diff --git a/src/commands/list.ts b/src/commands/list.ts index cf82fcf1..4f478333 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -20,7 +20,7 @@ export class List extends Command { const columns = { name: { header: 'Name' }, url: { header: 'URL', get: (r: Repository): string => r.urls.html }, - version: { header: 'Version', get: (r: Repository): string => r.npm.version }, + location: { header: 'Location', get: (r: Repository): string => r.location }, }; const sorted = sortBy(Object.values(repos), 'name'); cli.table(sorted, columns, { title: chalk.cyan.bold(`${org} Respositories`) }); diff --git a/src/commands/setup.ts b/src/commands/setup.ts index befb7410..d5a2e0b4 100644 --- a/src/commands/setup.ts +++ b/src/commands/setup.ts @@ -4,6 +4,7 @@ import { Command, Flags } from '@oclif/core'; import { prompt } from 'inquirer'; import { Config } from '../config'; import { AutoComplete } from '../autocomplete'; +import { MpmCd } from '../mpmCd'; export class Setup extends Command { public static readonly description = 'Setup mpm'; @@ -32,5 +33,6 @@ export class Setup extends Command { this.log(`All repositories will be cloned into ${config.get('directory')}`); await AutoComplete.create(config.get('directory')); + await MpmCd.create(); } } diff --git a/src/commands/where.ts b/src/commands/where.ts new file mode 100644 index 00000000..7fcacb5d --- /dev/null +++ b/src/commands/where.ts @@ -0,0 +1,32 @@ +import { Command, Flags } from '@oclif/core'; +import { Repos } from '../repos'; + +export class View extends Command { + public static readonly description = 'print location of a github repository.'; + public static readonly flags = { + remote: Flags.boolean({ + description: 'Return url of repository', + default: false, + }), + }; + public static readonly args = [ + { + name: 'repo', + description: 'Name of repository.', + required: true, + }, + ]; + public static readonly aliases = ['v']; + + public async run(): Promise { + const { args, flags } = await this.parse(View); + const repos = await Repos.create(); + const repo = repos.get(args.repo); + if (!repo) { + process.exitCode = 1; + throw new Error(`${args.repo as string} has not been added yet.`); + } + + this.log(flags.remote ? repo.urls.html : repo.location); + } +} diff --git a/src/config.ts b/src/config.ts index e6ce165c..7037e406 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,8 +1,8 @@ import * as os from 'os'; import * as path from 'path'; -import { ConfigFile, JsonMap } from './configFile'; +import { ConfigFile } from './configFile'; -export interface Configuration extends JsonMap { +export interface Configuration { directory: string; } diff --git a/src/configFile.ts b/src/configFile.ts index 42edff90..ad4aa994 100644 --- a/src/configFile.ts +++ b/src/configFile.ts @@ -5,11 +5,7 @@ import { Stats } from 'fs'; import { AsyncOptionalCreatable } from '@salesforce/kit'; import { Directory } from './directory'; -export interface JsonMap { - [key: string]: T; -} - -export abstract class ConfigFile extends AsyncOptionalCreatable { +export abstract class ConfigFile extends AsyncOptionalCreatable { public static MPM_DIR_NAME = '.mpm'; public static MPM_DIR = path.join(os.homedir(), ConfigFile.MPM_DIR_NAME); @@ -24,36 +20,42 @@ export abstract class ConfigFile extends AsyncOptionalCreatab } public async read(): Promise { - if (await this.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; - } + this.contents = JSON.parse(await readFile(this.filepath, 'utf-8')) as T; + return this.contents; } public async write(newContents: T = this.contents): Promise { await writeFile(this.filepath, JSON.stringify(newContents, null, 2)); } + public getContents(): T { + return this.contents; + } + public get(key: keyof T): T[keyof T] { return this.contents[key]; } - public getContents(): T { - return this.contents; + public has(key: keyof T): boolean { + const keys = Object.keys(this.getContents()) as Array; + return keys.includes(key); } public set(key: keyof T, value: T[keyof T]): void { this.contents[key] = value; } - public async exists(filepath: string): Promise { + public update(key: keyof T, value: T[keyof T]): void { + this.contents[key] = Object.assign({}, this.contents[key], value); + } + + public unset(key: keyof T): void { + delete this.contents[key]; + } + + public async exists(): Promise { try { - await access(filepath); + await access(this.filepath); return true; } catch { return false; @@ -62,7 +64,13 @@ export abstract class ConfigFile extends AsyncOptionalCreatab protected async init(): Promise { await Directory.create({ name: ConfigFile.MPM_DIR }); - this.contents = await this.read(); + if (await this.exists()) { + this.contents = await this.read(); + } else { + this.contents = this.make(); + await this.write(); + } + this.stats = await stat(this.filepath); } diff --git a/src/mpmCd.ts b/src/mpmCd.ts new file mode 100644 index 00000000..ff0d5d76 --- /dev/null +++ b/src/mpmCd.ts @@ -0,0 +1,39 @@ +import * as path from 'path'; +import { writeFile } from 'fs/promises'; +import { AsyncOptionalCreatable } from '@salesforce/kit'; +import { ConfigFile } from './configFile'; +import { BashRc } from './bashRc'; + +const TEMPLATE = ` +#/usr/bin/env bash + +function mpm { + if [[ "$1" == "cd" ]]; then + cd $(mpm where $2) + else + mpm "$@" + fi +} +`; + +/** + * It's not possible to use node to change the directory of the executing + * shell so instead we write a mpm function to the .bashrc so that we can + * capture the `mpm cd` execution and use bash instead. + */ +export class MpmCd extends AsyncOptionalCreatable { + public static LOCATION = path.join(ConfigFile.MPM_DIR, 'mpmcd.bash'); + public constructor() { + super(); + } + + protected async init(): Promise { + if (process.platform === 'win32') return; + + await writeFile(MpmCd.LOCATION, TEMPLATE); + const bashRc = await BashRc.create(); + bashRc.append(`source ${MpmCd.LOCATION}`); + await bashRc.write(); + bashRc.source(); + } +} diff --git a/src/repos.ts b/src/repos.ts index 4717e1cc..3604baab 100644 --- a/src/repos.ts +++ b/src/repos.ts @@ -5,7 +5,7 @@ import { mkdir, readFile } from 'fs/promises'; import { Octokit } from '@octokit/core'; import { Duration } from '@salesforce/kit'; import { exec } from 'shelljs'; -import { ConfigFile, JsonMap } from './configFile'; +import { ConfigFile } from './configFile'; import { getToken } from './util'; import { Config } from './config'; import { Directory } from './directory'; @@ -46,12 +46,12 @@ export type RepositoryResponse = { default_branch: string; }; -export interface RepoIndex extends JsonMap { +export interface RepoIndex { [key: string]: Repository; } export class Repos extends ConfigFile { - public static REFRESH_TIME = Duration.hours(2); + public static REFRESH_TIME = Duration.hours(8); public directory!: Directory; private octokit!: Octokit; @@ -96,9 +96,7 @@ export class Repos extends ConfigFile { const config = await Config.create(); this.directory = await Directory.create({ name: config.get('directory') }); - if (this.needsRefresh()) { - await this.refresh(); - } + if (this.needsRefresh()) await this.refresh(); } private needsRefresh(): boolean { @@ -110,11 +108,9 @@ export class Repos extends ConfigFile { const orgs = Array.from(new Set(Object.values(this.getContents()).map((r) => r.org))); for (const org of orgs) { const orgRepos = await this.fetch(org); - for (const repo of orgRepos) { - if (originalRepos.includes(repo.name)) { - this.set(repo.name, await this.addAdditionalInfo(repo)); - } - } + orgRepos.forEach((repo) => { + if (originalRepos.includes(repo.name)) this.update(repo.name, repo); + }); } await this.write(); } @@ -140,9 +136,8 @@ export class Repos extends ConfigFile { const location = repo.location || path.join(this.directory.name, repo.org, repo.name); const pkgJsonPath = path.join(location, 'package.json'); const pkgJson = JSON.parse(await readFile(pkgJsonPath, 'utf-8')) as { name: string }; - repo.npm = { - name: pkgJson.name, - }; + repo.npm = { name: pkgJson.name }; + const npmInfoRaw = exec(`npm view ${pkgJson.name} --json`, { silent: true }).stdout; const npmInfo = JSON.parse(npmInfoRaw) as { 'dist-tags': Record;