@hallaxius/forge 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,160 @@
1
+ import fs from "node:fs";
2
+ import { Octokit } from "@octokit/rest";
3
+ import { currentBranch, getConfig } from "isomorphic-git";
4
+ import { resolveToken } from "./auth.js";
5
+
6
+ function parseRemoteUrl(remoteUrl: string): { owner: string; repo: string } {
7
+ const match = remoteUrl.match(
8
+ /github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/,
9
+ );
10
+ if (!match)
11
+ throw new Error(
12
+ `Could not determine GitHub repo from remote URL: ${remoteUrl}`,
13
+ );
14
+ return { owner: match[1], repo: match[2] };
15
+ }
16
+
17
+ async function createClient(): Promise<Octokit> {
18
+ const token = await resolveToken();
19
+ return new Octokit({ auth: token });
20
+ }
21
+
22
+ export async function getRepoInfo(): Promise<{ owner: string; repo: string }> {
23
+ const dir = process.cwd();
24
+ const remoteUrl = await getConfig({ fs, dir, path: "remote.origin.url" });
25
+ if (!remoteUrl)
26
+ throw new Error(
27
+ "No remote 'origin' configured. Set up a GitHub remote first.",
28
+ );
29
+ return parseRemoteUrl(remoteUrl);
30
+ }
31
+
32
+ export async function createPR(
33
+ title: string,
34
+ body: string,
35
+ head: string,
36
+ base: string = "main",
37
+ ): Promise<{ number: number; url: string }> {
38
+ const octokit = await createClient();
39
+ const { owner, repo } = await getRepoInfo();
40
+ const { data } = await octokit.pulls.create({
41
+ owner,
42
+ repo,
43
+ title,
44
+ body,
45
+ head,
46
+ base,
47
+ });
48
+ return { number: data.number, url: data.html_url };
49
+ }
50
+
51
+ export async function listPRs(
52
+ state: "open" | "closed" | "all" = "open",
53
+ ): Promise<
54
+ {
55
+ number: number;
56
+ title: string;
57
+ state: string;
58
+ url: string;
59
+ author: string;
60
+ }[]
61
+ > {
62
+ const octokit = await createClient();
63
+ const { owner, repo } = await getRepoInfo();
64
+ const { data } = await octokit.pulls.list({
65
+ owner,
66
+ repo,
67
+ state,
68
+ per_page: 20,
69
+ });
70
+ return data.map((pr) => ({
71
+ number: pr.number,
72
+ title: pr.title,
73
+ state: pr.state,
74
+ url: pr.html_url,
75
+ author: pr.user?.login || "",
76
+ }));
77
+ }
78
+
79
+ export async function createIssue(
80
+ title: string,
81
+ body: string,
82
+ labels: string[] = [],
83
+ ): Promise<{ number: number; url: string }> {
84
+ const octokit = await createClient();
85
+ const { owner, repo } = await getRepoInfo();
86
+ const { data } = await octokit.issues.create({
87
+ owner,
88
+ repo,
89
+ title,
90
+ body,
91
+ labels,
92
+ });
93
+ return { number: data.number, url: data.html_url };
94
+ }
95
+
96
+ export async function listIssues(
97
+ state: "open" | "closed" | "all" = "open",
98
+ ): Promise<
99
+ {
100
+ number: number;
101
+ title: string;
102
+ state: string;
103
+ url: string;
104
+ author: string;
105
+ }[]
106
+ > {
107
+ const octokit = await createClient();
108
+ const { owner, repo } = await getRepoInfo();
109
+ const { data } = await octokit.issues.list({
110
+ owner,
111
+ repo,
112
+ state,
113
+ per_page: 20,
114
+ });
115
+ return data.map((issue) => ({
116
+ number: issue.number,
117
+ title: issue.title,
118
+ state: issue.state,
119
+ url: issue.html_url,
120
+ author: issue.user?.login || "",
121
+ }));
122
+ }
123
+
124
+ export async function createRelease(
125
+ tag: string,
126
+ name: string,
127
+ body: string,
128
+ ): Promise<{ url: string }> {
129
+ const octokit = await createClient();
130
+ const { owner, repo } = await getRepoInfo();
131
+ const { data } = await octokit.repos.createRelease({
132
+ owner,
133
+ repo,
134
+ tag_name: tag,
135
+ name,
136
+ body,
137
+ });
138
+ return { url: data.html_url };
139
+ }
140
+
141
+ export async function getCIStatus(): Promise<
142
+ { branch: string; name: string; conclusion: string; url: string }[]
143
+ > {
144
+ const octokit = await createClient();
145
+ const { owner, repo } = await getRepoInfo();
146
+ const dir = process.cwd();
147
+ const branch = (await currentBranch({ fs, dir })) || "HEAD";
148
+ const { data } = await octokit.checks.listForRef({
149
+ owner,
150
+ repo,
151
+ ref: branch,
152
+ per_page: 10,
153
+ });
154
+ return data.check_runs.map((run) => ({
155
+ branch,
156
+ name: run.name,
157
+ conclusion: run.conclusion || "pending",
158
+ url: run.html_url,
159
+ }));
160
+ }
package/src/lib/ui.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  import boxen from "boxen";
2
2
  import chalk from "chalk";
3
+ import inquirer from "inquirer";
3
4
  import ora from "ora";
4
5
  import { colors } from "../constants/colors.js";
5
6
  import { formatting } from "../constants/messages.js";
6
7
 
8
+ const { prompt } = inquirer;
9
+
7
10
  function stripAnsi(str: string): string {
8
11
  return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
9
12
  }
@@ -77,16 +80,11 @@ export function showSeparator(): void {
77
80
  console.log(chalk.dim(formatting.separator));
78
81
  }
79
82
 
80
- async function getInquirer() {
81
- return await import("inquirer");
82
- }
83
-
84
83
  export async function confirm(
85
84
  msg: string,
86
85
  defaultValue: boolean = true,
87
86
  ): Promise<boolean> {
88
- const inquirer = await getInquirer();
89
- const { value } = await inquirer.prompt([
87
+ const { value } = await prompt([
90
88
  {
91
89
  type: "confirm",
92
90
  name: "value",
@@ -101,8 +99,7 @@ export async function select<T>(
101
99
  message: string,
102
100
  choices: { name: string; value: T }[],
103
101
  ): Promise<T> {
104
- const inquirer = await getInquirer();
105
- const { value } = await inquirer.prompt([
102
+ const { value } = await prompt([
106
103
  {
107
104
  type: "list",
108
105
  name: "value",
@@ -117,8 +114,7 @@ export async function input(
117
114
  message: string,
118
115
  defaultValue?: string,
119
116
  ): Promise<string> {
120
- const inquirer = await getInquirer();
121
- const { value } = await inquirer.prompt([
117
+ const { value } = await prompt([
122
118
  {
123
119
  type: "input",
124
120
  name: "value",
@@ -133,8 +129,7 @@ export async function password(
133
129
  message: string,
134
130
  mask: string = "*",
135
131
  ): Promise<string> {
136
- const inquirer = await getInquirer();
137
- const { value } = await inquirer.prompt([
132
+ const { value } = await prompt([
138
133
  {
139
134
  type: "password",
140
135
  name: "value",
@@ -149,8 +144,7 @@ export async function checkbox(
149
144
  message: string,
150
145
  choices: { name: string; value: string; checked?: boolean }[],
151
146
  ): Promise<string[]> {
152
- const inquirer = await getInquirer();
153
- const { value } = await inquirer.prompt([
147
+ const { value } = await prompt([
154
148
  {
155
149
  type: "checkbox",
156
150
  name: "value",
@@ -1,5 +1,3 @@
1
- import { execSync } from "node:child_process";
2
-
3
1
  export function validateEmail(email: string): boolean {
4
2
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5
3
  return emailRegex.test(email);
@@ -16,12 +14,3 @@ export function validateNotEmpty(input: string): boolean | string {
16
14
  }
17
15
  return true;
18
16
  }
19
-
20
- export async function validateGitInstalled(): Promise<boolean> {
21
- try {
22
- execSync("git --version", { stdio: "ignore" });
23
- return true;
24
- } catch {
25
- return false;
26
- }
27
- }
@@ -1,2 +1,2 @@
1
1
  // Auto-generated by build script. DO NOT EDIT.
2
- export const VERSION = "0.1.1";
2
+ export const VERSION = "0.1.3";
@@ -1,35 +0,0 @@
1
- import type { Command } from "commander";
2
- import * as git from "../lib/git.js";
3
- import { error, success } from "../lib/logger.js";
4
-
5
- export default function register(program: Command): void {
6
- program
7
- .command("archive")
8
- .description("Create an archive of the repository")
9
- .argument("<format>", "Archive format (tar, tar.gz, zip)")
10
- .option("--prefix <dir>", "Prepend prefix to archive paths")
11
- .option("--output <file>", "Output file path")
12
- .option("--tree-ish <ref>", "Tree or commit to archive")
13
- .action(
14
- async (
15
- format: string,
16
- options: { prefix?: string; output?: string; treeIsh?: string },
17
- ) => {
18
- try {
19
- const opts: { prefix?: string; output?: string; treeIsh?: string } =
20
- {};
21
- if (options.prefix) opts.prefix = options.prefix;
22
- if (options.output) opts.output = options.output;
23
- if (options.treeIsh) opts.treeIsh = options.treeIsh;
24
-
25
- const _result = await git.archive(format, opts);
26
- const outputPath = options.output || `archive.${format}`;
27
- success(`Archive created: ${outputPath}`);
28
- } catch (err) {
29
- error(
30
- `Archive failed: ${err instanceof Error ? err.message : String(err)}`,
31
- );
32
- }
33
- },
34
- );
35
- }
@@ -1,102 +0,0 @@
1
- import type { Command } from "commander";
2
- import * as git from "../lib/git.js";
3
- import { error, info, success, text } from "../lib/logger.js";
4
-
5
- export default function register(program: Command): void {
6
- const cmd = program
7
- .command("bisect")
8
- .description("Bisect to find the commit that introduced a bug");
9
-
10
- cmd
11
- .command("start")
12
- .description("Start a bisect session")
13
- .argument("[bad]", "Bad commit (default: HEAD)")
14
- .argument("[good...]", "Good commits")
15
- .action(async (bad: string | undefined, good: string[]) => {
16
- try {
17
- await git.bisectStart(bad, good);
18
- success("Bisect started.");
19
- } catch (err) {
20
- error(
21
- `Bisect start failed: ${err instanceof Error ? err.message : String(err)}`,
22
- );
23
- }
24
- });
25
-
26
- cmd
27
- .command("bad")
28
- .description("Mark a commit as bad")
29
- .argument("[commit]", "Commit hash (default: HEAD)")
30
- .action(async (commit: string | undefined) => {
31
- try {
32
- await git.bisectBad(commit);
33
- success(`Commit ${commit || "HEAD"} marked as bad.`);
34
- } catch (err) {
35
- error(
36
- `Bisect bad failed: ${err instanceof Error ? err.message : String(err)}`,
37
- );
38
- }
39
- });
40
-
41
- cmd
42
- .command("good")
43
- .description("Mark commits as good")
44
- .argument("[commits...]", "Commits to mark as good")
45
- .action(async (commits: string[]) => {
46
- try {
47
- if (commits.length === 0) {
48
- info("Provide at least one commit to mark as good.");
49
- return;
50
- }
51
- await git.bisectGood(commits);
52
- success(`Marked ${commits.join(", ")} as good.`);
53
- } catch (err) {
54
- error(
55
- `Bisect good failed: ${err instanceof Error ? err.message : String(err)}`,
56
- );
57
- }
58
- });
59
-
60
- cmd
61
- .command("reset")
62
- .description("Reset bisect state")
63
- .action(async () => {
64
- try {
65
- await git.bisectReset();
66
- success("Bisect reset.");
67
- } catch (err) {
68
- error(
69
- `Bisect reset failed: ${err instanceof Error ? err.message : String(err)}`,
70
- );
71
- }
72
- });
73
-
74
- cmd
75
- .command("log")
76
- .description("Show bisect log")
77
- .action(async () => {
78
- try {
79
- const log = await git.bisectLog();
80
- text(log);
81
- } catch (err) {
82
- error(
83
- `Bisect log failed: ${err instanceof Error ? err.message : String(err)}`,
84
- );
85
- }
86
- });
87
-
88
- cmd
89
- .command("run")
90
- .description("Run a script for bisect")
91
- .argument("<cmd>", "Command to run")
92
- .action(async (cmd: string) => {
93
- try {
94
- await git.bisectRun(cmd);
95
- success("Bisect run completed.");
96
- } catch (err) {
97
- error(
98
- `Bisect run failed: ${err instanceof Error ? err.message : String(err)}`,
99
- );
100
- }
101
- });
102
- }
@@ -1,57 +0,0 @@
1
- import type { Command } from "commander";
2
- import * as git from "../lib/git.js";
3
- import { error, info, success } from "../lib/logger.js";
4
-
5
- export default function register(program: Command): void {
6
- program
7
- .command("cherry-pick")
8
- .description("Cherry-pick commits")
9
- .argument("[commits...]", "Commits to cherry-pick")
10
- .option("--no-commit", "Apply changes without committing")
11
- .option(
12
- "--mainline <number>",
13
- "Mainline parent for cherry-pick merge commits",
14
- )
15
- .option("--continue", "Continue after resolving conflicts")
16
- .option("--abort", "Abort cherry-pick in progress")
17
- .action(
18
- async (
19
- commits: string[],
20
- options: {
21
- noCommit?: boolean;
22
- mainline?: number;
23
- continue?: boolean;
24
- abort?: boolean;
25
- },
26
- ) => {
27
- try {
28
- if (options.continue) {
29
- await git.cherryPickContinue();
30
- success("Cherry-pick continued.");
31
- return;
32
- }
33
-
34
- if (options.abort) {
35
- await git.cherryPickAbort();
36
- success("Cherry-pick aborted.");
37
- return;
38
- }
39
-
40
- if (commits.length === 0) {
41
- info("Provide at least one commit to cherry-pick.");
42
- return;
43
- }
44
-
45
- await git.cherryPick(commits, {
46
- noCommit: options.noCommit ?? undefined,
47
- mainline: options.mainline ?? undefined,
48
- });
49
- success(`Cherry-pick applied: ${commits.join(", ")}`);
50
- } catch (err) {
51
- error(
52
- `Cherry-pick failed: ${err instanceof Error ? err.message : String(err)}`,
53
- );
54
- }
55
- },
56
- );
57
- }
@@ -1,76 +0,0 @@
1
- import type { Command } from "commander";
2
- import * as git from "../lib/git.js";
3
- import { error, info, success, warning } from "../lib/logger.js";
4
- import { confirm } from "../lib/ui.js";
5
-
6
- export default function register(program: Command): void {
7
- program
8
- .command("clean")
9
- .description("Clean untracked files")
10
- .argument("[paths...]", "Paths to clean")
11
- .option("--dry-run", "Show what would be removed")
12
- .option("--force", "Skip confirmation prompt")
13
- .option("--exclude <pattern>", "Exclude files matching pattern")
14
- .action(
15
- async (
16
- _paths: string[],
17
- options: { dryRun?: boolean; force?: boolean; exclude?: string },
18
- ) => {
19
- try {
20
- const opts: { dryRun?: boolean; force?: boolean; exclude?: string } =
21
- {};
22
- if (options.dryRun) opts.dryRun = true;
23
- if (options.force) opts.force = true;
24
- if (options.exclude) opts.exclude = options.exclude;
25
-
26
- const wouldRemove = await git.clean(opts);
27
-
28
- if (options.dryRun) {
29
- if (wouldRemove.length === 0) {
30
- info("Nothing would be removed.");
31
- return;
32
- }
33
- info("Would remove:");
34
- for (const line of wouldRemove) {
35
- warning(line);
36
- }
37
- return;
38
- }
39
-
40
- if (wouldRemove.length === 0) {
41
- info("Nothing to clean.");
42
- return;
43
- }
44
-
45
- if (!options.force) {
46
- const ok = await confirm(
47
- `Remove ${wouldRemove.length} untracked file(s)?`,
48
- );
49
- if (!ok) {
50
- info("Clean cancelled.");
51
- return;
52
- }
53
- }
54
-
55
- const result = await git.clean({
56
- force: true,
57
- ...(options.exclude ? { exclude: options.exclude } : {}),
58
- });
59
-
60
- if (result.length === 0) {
61
- info("Nothing cleaned.");
62
- return;
63
- }
64
-
65
- for (const line of result) {
66
- warning(line);
67
- }
68
- success(`Cleaned ${result.length} file(s).`);
69
- } catch (err) {
70
- error(
71
- `Clean failed: ${err instanceof Error ? err.message : String(err)}`,
72
- );
73
- }
74
- },
75
- );
76
- }
@@ -1,92 +0,0 @@
1
- import type { Command } from "commander";
2
- import * as git from "../lib/git.js";
3
- import { error, info, success } from "../lib/logger.js";
4
- import { createTable } from "../lib/ui.js";
5
-
6
- export default function register(program: Command): void {
7
- const cmd = program.command("worktree").description("Manage worktrees");
8
-
9
- cmd
10
- .command("add")
11
- .description("Add a new worktree")
12
- .argument("<path>", "Path for the new worktree")
13
- .argument("[branch]", "Branch to checkout")
14
- .option("--new", "Create and checkout a new branch")
15
- .option("--detach", "Checkout detached HEAD")
16
- .action(
17
- async (
18
- path: string,
19
- branch: string | undefined,
20
- options: { new?: boolean; detach?: boolean },
21
- ) => {
22
- try {
23
- await git.worktreeAdd(path, branch, {
24
- new: options.new,
25
- detach: options.detach,
26
- });
27
- success(`Worktree added at '${path}'.`);
28
- } catch (err) {
29
- error(
30
- `Worktree add failed: ${err instanceof Error ? err.message : String(err)}`,
31
- );
32
- }
33
- },
34
- );
35
-
36
- cmd
37
- .command("list")
38
- .description("List all worktrees")
39
- .action(async () => {
40
- try {
41
- const entries = await git.worktreeList();
42
- if (entries.length === 0) {
43
- info("No worktrees found.");
44
- return;
45
- }
46
- const rows = entries.map((e) => [e.path, e.branch, e.hash]);
47
- console.log(createTable(["Path", "Branch", "Hash"], rows));
48
- } catch (err) {
49
- error(
50
- `Worktree list failed: ${err instanceof Error ? err.message : String(err)}`,
51
- );
52
- }
53
- });
54
-
55
- cmd
56
- .command("remove")
57
- .description("Remove a worktree")
58
- .argument("<path>", "Path of the worktree to remove")
59
- .option("--force", "Force removal")
60
- .action(async (path: string, options: { force?: boolean }) => {
61
- try {
62
- await git.worktreeRemove(path, options.force);
63
- success(`Worktree '${path}' removed.`);
64
- } catch (err) {
65
- error(
66
- `Worktree remove failed: ${err instanceof Error ? err.message : String(err)}`,
67
- );
68
- }
69
- });
70
-
71
- cmd
72
- .command("prune")
73
- .description("Prune stale worktree references")
74
- .option("--dry-run", "Only show what would be pruned")
75
- .action(async (options: { dryRun?: boolean }) => {
76
- try {
77
- const result = await git.worktreePrune(options.dryRun);
78
- if (result.length === 0) {
79
- info("Nothing to prune.");
80
- return;
81
- }
82
- for (const line of result) {
83
- info(line);
84
- }
85
- success(`Pruned ${result.length} reference(s).`);
86
- } catch (err) {
87
- error(
88
- `Worktree prune failed: ${err instanceof Error ? err.message : String(err)}`,
89
- );
90
- }
91
- });
92
- }