Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 0 additions & 42 deletions .eslintrc.cjs

This file was deleted.

3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"trailingComma": "es5"
}
8 changes: 5 additions & 3 deletions cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "gitrover",
"version": "0.0.6",
"version": "0.1.0",
"description": "The better GitHub CLI we all needed",
"main": "./dist/index.js",
"bin": {
Expand All @@ -22,16 +22,18 @@
"author": "ToastedToast <hey@toasted.dev>",
"license": "MIT",
"dependencies": {
"@inquirer/prompts": "^3.3.0",
"@effect/cli": "^0.73.0",
"@effect/platform": "^0.94.0",
"@effect/platform-node": "^0.104.0",
"@octokit/auth-oauth-device": "^6.0.1",
"@octokit/core": "^5.0.2",
"@octokit/request-error": "^5.0.1",
"@octokit/rest": "^20.0.2",
"chalk": "^5.3.0",
"commander": "^11.1.0",
"effect": "^3.19.13",
"env-paths": "^3.0.0",
"fs-extra": "^11.2.0",
"isomorphic-unfetch": "^4.0.2",
"keypress": "^0.2.1",
"open": "^10.0.3"
},
Expand Down
139 changes: 97 additions & 42 deletions cli/src/commands/browse.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,107 @@
import { RequestError } from "@octokit/request-error";
import { Octokit } from "@octokit/rest";
import chalk from "chalk";
import { Command } from "commander";
import { error } from "~/utils/logger.js";
import { Command, Args } from "@effect/cli";
import { Console, Data, Effect, Option } from "effect";
import open from "open";
import { convertOriginUrlToGitHubUrl } from "~/utils/git.js";
import { handler } from "~/utils/command.js";

export const browseCommand = new Command()
.name("browse")
.argument("[repository]", "The repository you want to browse.")
.description("Open a repository in your browser.")
.action(
handler(async (repository?: string) => {
const url = repository
? repository?.startsWith("https://github.com")
? repository
: `https://github.com/${repository}`
: convertOriginUrlToGitHubUrl();
import { GitClient } from "~/services/git.js";
import { GithubClient } from "~/services/github.js";

const repoArgument = Args.text({
name: "repository",
}).pipe(
Args.optional,
Args.withDescription("The repository you want to browse.")
);

class OpenError extends Data.TaggedError("OpenError")<{
cause: unknown;
}> {}

export const BrowseCommand = Command.make(
"browse",
{ repoArgument },
({ repoArgument }) =>
Effect.gen(function* () {
const gitClient = yield* GitClient;
const githubClient = yield* GithubClient;

const url = yield* Option.match(repoArgument, {
onNone: () => gitClient.getOriginGithubUrl(),
onSome: (repository) =>
Effect.sync(() =>
repository.startsWith("https://github.com")
? repository
: `https://github.com/${repository}`
),
});
if (!url) {
if (!repository)
error(
"Failed to parse origin URL.\nThis probably means you don't have an",
chalk.bold("origin"),
"branch.",
);
process.exit(1);
}

const splitSource = url.replace("https://github.com/", "").split("/");
const owner = splitSource[0]!;
const name = splitSource[1] ?? splitSource[0]!;

const octokit = new Octokit();
try {
await octokit.rest.repos.get({
owner,
repo: name,
});
} catch (err) {
if (err instanceof RequestError && err.status === 404) {
error(`Repository ${chalk.bold(`${owner}/${name}`)} does not exist.`);
process.exit(1);
}
console.error(err);
process.exit(1);
}
yield* githubClient.getRepository(owner, name);

return yield* Effect.tryPromise({
try: () => open(url),
catch: (e) => new OpenError({ cause: e }),
});
}).pipe(
Effect.catchTag("RepositoryNotFoundError", () =>
Console.error("Repository not found.")
)
)
);

await open(url);
}),
);
// import { RequestError } from "@octokit/request-error";
// import { Octokit } from "@octokit/rest";
// import chalk from "chalk";
// import { Command } from "commander";
// import { error } from "~/utils/logger.js";
// import open from "open";
// import { convertOriginUrlToGitHubUrl } from "~/utils/git.js";
// import { handler } from "~/utils/command.js";
//
// export const browseCommand = new Command()
// .name("browse")
// .argument("[repository]", "The repository you want to browse.")
// .description("Open a repository in your browser.")
// .action(
// handler(async (repository?: string) => {
// const url = repository
// ? repository?.startsWith("https://github.com")
// ? repository
// : `https://github.com/${repository}`
// : convertOriginUrlToGitHubUrl();
// if (!url) {
// if (!repository)
// error(
// "Failed to parse origin URL.\nThis probably means you don't have an",
// chalk.bold("origin"),
// "branch.",
// );
// process.exit(1);
// }
//
// const splitSource = url.replace("https://github.com/", "").split("/");
// const owner = splitSource[0]!;
// const name = splitSource[1] ?? splitSource[0]!;
//
// const octokit = new Octokit();
// try {
// await octokit.rest.repos.get({
// owner,
// repo: name,
// });
// } catch (err) {
// if (err instanceof RequestError && err.status === 404) {
// error(`Repository ${chalk.bold(`${owner}/${name}`)} does not exist.`);
// process.exit(1);
// }
// console.error(err);
// process.exit(1);
// }
//
// await open(url);
// }),
// );
47 changes: 35 additions & 12 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,40 @@
#!/usr/bin/env node

import "isomorphic-unfetch";
import { commandHandler } from "./commands/index.js";
import {
getNpmVersion,
renderVersionWarning,
} from "./utils/version-warning.js";
import { Effect, Layer, Logger } from "effect";
import { Command } from "@effect/cli";
import { NodeContext, NodeRuntime } from "@effect/platform-node";
import { BrowseCommand } from "./commands/browse.js";
import { GitClient } from "./services/git.js";
import { GithubClient } from "./services/github.js";
import { cliLogger } from "./utils/logger.js";
import { getVersion } from "./utils/version.js";

const main = async () => {
const npmVersion = await getNpmVersion();
if (npmVersion) renderVersionWarning(npmVersion);
const cli = (args: readonly string[]) =>
Effect.gen(function* () {
const MainCommand = Command.make("gitrover").pipe(
Command.withSubcommands([BrowseCommand])
);

commandHandler();
};
const cli = Command.run(MainCommand, {
name: "gitrover",
version: yield* getVersion(),
executable: "gitrover",
});

main();
return yield* cli(args);
});

const MainLayer = Layer.mergeAll(
GitClient.Default,
GithubClient.Default,
NodeContext.layer
).pipe(Layer.provideMerge(Logger.replace(Logger.defaultLogger, cliLogger)));

cli(process.argv).pipe(
Effect.tapErrorCause((cause) => Effect.logError(cause)),
Effect.provide(MainLayer),
NodeRuntime.runMain({
disablePrettyLogger: true,
disableErrorReporting: true,
})
);
52 changes: 52 additions & 0 deletions cli/src/services/git.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NodeContext } from "@effect/platform-node";
import { Data, Effect } from "effect";
import { Command, CommandExecutor } from "@effect/platform";

export class NoOriginError extends Data.TaggedError("NoOriginError") {
override message = "No remote named `origin` was found.";
}

export class GitClient extends Effect.Service<GitClient>()(
"@gitrover/GitClient",
{
dependencies: [NodeContext.layer],
effect: Effect.gen(function* () {
const executor = yield* CommandExecutor.CommandExecutor;

const gitRepoHasOrigin = Effect.fn("gitRepoHasOrigin")(function* () {
const remoteShowCommand = Command.make("git", ...["remote", "show"]);
const output = yield* executor
.string(remoteShowCommand)
.pipe(Effect.orElse(() => Effect.sync(() => "")));
return output.trim().length !== 0;
});

const getOriginUrl = Effect.fn("getOriginUrl")(function* () {
if (!(yield* gitRepoHasOrigin())) return yield* new NoOriginError();
const remoteGetUrlCommand = Command.make(
"git",
...["remote", "get-url", "origin"]
);
const output = yield* executor.string(remoteGetUrlCommand);
return output;
});

const getOriginGithubUrl = Effect.fn("getOriginGithubUrl")(function* () {
const originUrl = yield* getOriginUrl();
const urlMatch = originUrl
.trim()
.replace(".git", "")
.match(
/((?<=git@github.com:)(.*)\/(.*)|(?<=https?:\/\/github.com\/)(.*)\/(.*))/
)?.[0];
return urlMatch ? `https://github.com/${urlMatch}` : undefined;
});

return {
gitRepoHasOrigin,
getOriginUrl,
getOriginGithubUrl,
} as const;
}),
}
) {}
39 changes: 39 additions & 0 deletions cli/src/services/github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { FetchHttpClient, HttpClient } from "@effect/platform";
import { NodeContext } from "@effect/platform-node";
import { Data, Effect } from "effect";

export class RepositoryNotFoundError extends Data.TaggedError(
"RepositoryNotFoundError"
)<{
message: string;
}> {}

export class GithubClient extends Effect.Service<GithubClient>()(
"@gitrover/GithubClient",
{
dependencies: [NodeContext.layer, FetchHttpClient.layer],
effect: Effect.gen(function* () {
const http = yield* HttpClient.HttpClient;

const getRepository = Effect.fn("getRepostory")(function* (
owner: string,
name: string
) {
const res = yield* http.get(
`https://api.github.com/repos/${owner}/${name}`,
{
headers: {
Accept: "application/vnd.github+json",
},
}
);
const data = yield* res.json;
if (res.status === 404)
return yield* new RepositoryNotFoundError(data.message);
return data;
});

return { getRepository } as const;
}),
}
) {}
6 changes: 3 additions & 3 deletions cli/src/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const execGitCommandSync = (args: string[], options?: ExecSyncOptions) =>

export const execGitCommand = (
args: string[],
callback?: Parameters<typeof exec>[1],
callback?: Parameters<typeof exec>[1]
) => exec("git " + args.join(" "), callback);

export const isGitRepository = (cwd?: string) =>
Expand Down Expand Up @@ -54,7 +54,7 @@ export const getRepoFromOrigin = () => {
.trim()
.replace(".git", "")
.match(
/((?<=git@github.com:)(.*)\/(.*)|(?<=https?:\/\/github.com\/)(.*)\/(.*))/,
/((?<=git@github.com:)(.*)\/(.*)|(?<=https?:\/\/github.com\/)(.*)\/(.*))/
)?.[0];
return urlMatch ? (urlMatch.split("/") as [string, string]) : undefined;
};
Expand All @@ -67,7 +67,7 @@ export const convertOriginUrlToGitHubUrl = () => {
.trim()
.replace(".git", "")
.match(
/((?<=git@github.com:)(.*)\/(.*)|(?<=https?:\/\/github.com\/)(.*)\/(.*))/,
/((?<=git@github.com:)(.*)\/(.*)|(?<=https?:\/\/github.com\/)(.*)\/(.*))/
)?.[0];

return urlMatch ? `https://github.com/${urlMatch}` : undefined;
Expand Down
Loading
Loading