Skip to content

Commit

Permalink
feat: add cd command
Browse files Browse the repository at this point in the history
  • Loading branch information
mdonnalley committed Aug 23, 2021
1 parent 7db7695 commit 4e974ca
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 44 deletions.
17 changes: 9 additions & 8 deletions src/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -14,29 +14,30 @@ _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 ))
}
`;

const COMPLETE = 'complete -F _repo_completions mpm';

export class AutoComplete extends AsyncCreatable<string> {
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<void> {
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();
}
}
44 changes: 44 additions & 0 deletions src/bashRc.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
this.contents = await readFile(BashRc.LOCATION, 'utf-8');
return this.contents;
}

public async write(): Promise<void> {
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<void> {
if (process.platform === 'win32') return;
await this.read();
}
}
23 changes: 23 additions & 0 deletions src/commands/cd.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
/**
* 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.
*/
}
}
2 changes: 1 addition & 1 deletion src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`) });
Expand Down
2 changes: 2 additions & 0 deletions src/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
}
}
32 changes: 32 additions & 0 deletions src/commands/where.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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);
}
}
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
export interface Configuration {
directory: string;
}

Expand Down
46 changes: 27 additions & 19 deletions src/configFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@ import { Stats } from 'fs';
import { AsyncOptionalCreatable } from '@salesforce/kit';
import { Directory } from './directory';

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

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

Expand All @@ -24,36 +20,42 @@ export abstract class ConfigFile<T extends JsonMap> extends AsyncOptionalCreatab
}

public async read(): Promise<T> {
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<void> {
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<keyof T>;
return keys.includes(key);
}

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

public async exists(filepath: string): Promise<boolean> {
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<boolean> {
try {
await access(filepath);
await access(this.filepath);
return true;
} catch {
return false;
Expand All @@ -62,7 +64,13 @@ export abstract class ConfigFile<T extends JsonMap> extends AsyncOptionalCreatab

protected async init(): Promise<void> {
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);
}

Expand Down
39 changes: 39 additions & 0 deletions src/mpmCd.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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();
}
}
23 changes: 9 additions & 14 deletions src/repos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<RepoIndex> {
public static REFRESH_TIME = Duration.hours(2);
public static REFRESH_TIME = Duration.hours(8);
public directory!: Directory;
private octokit!: Octokit;

Expand Down Expand Up @@ -96,9 +96,7 @@ export class Repos extends ConfigFile<RepoIndex> {
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 {
Expand All @@ -110,11 +108,9 @@ export class Repos extends ConfigFile<RepoIndex> {
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();
}
Expand All @@ -140,9 +136,8 @@ export class Repos extends ConfigFile<RepoIndex> {
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<string, string>;
Expand Down

0 comments on commit 4e974ca

Please sign in to comment.