@hallaxius/forge 0.1.0

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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/bin/forge.js +2 -0
  4. package/dist/cli.js +35641 -0
  5. package/package.json +65 -0
  6. package/src/cli.ts +78 -0
  7. package/src/commands/alias.ts +66 -0
  8. package/src/commands/archive.ts +35 -0
  9. package/src/commands/bisect.ts +102 -0
  10. package/src/commands/branch.ts +46 -0
  11. package/src/commands/cherry-pick.ts +57 -0
  12. package/src/commands/clean.ts +76 -0
  13. package/src/commands/clone.ts +100 -0
  14. package/src/commands/commit.ts +93 -0
  15. package/src/commands/config.ts +48 -0
  16. package/src/commands/diff.ts +26 -0
  17. package/src/commands/fetch.ts +20 -0
  18. package/src/commands/help.ts +58 -0
  19. package/src/commands/init.ts +37 -0
  20. package/src/commands/log.ts +29 -0
  21. package/src/commands/merge.ts +37 -0
  22. package/src/commands/push.ts +35 -0
  23. package/src/commands/remote.ts +107 -0
  24. package/src/commands/reset.ts +30 -0
  25. package/src/commands/setup.ts +95 -0
  26. package/src/commands/stash.ts +44 -0
  27. package/src/commands/status.ts +74 -0
  28. package/src/commands/sync.ts +20 -0
  29. package/src/commands/tag.ts +41 -0
  30. package/src/commands/undo.ts +27 -0
  31. package/src/commands/version.ts +19 -0
  32. package/src/commands/worktree.ts +92 -0
  33. package/src/constants/colors.ts +7 -0
  34. package/src/constants/commit-types.ts +24 -0
  35. package/src/constants/messages.ts +23 -0
  36. package/src/lib/auth.ts +95 -0
  37. package/src/lib/config.ts +99 -0
  38. package/src/lib/git.ts +382 -0
  39. package/src/lib/logger.ts +31 -0
  40. package/src/lib/ui.ts +162 -0
  41. package/src/lib/validators.ts +27 -0
  42. package/src/templates/commit-types.json +9 -0
  43. package/src/utils/files.ts +21 -0
  44. package/src/utils/strings.ts +19 -0
package/src/lib/git.ts ADDED
@@ -0,0 +1,382 @@
1
+ import simpleGit from "simple-git";
2
+
3
+ const git = simpleGit();
4
+
5
+ interface StatusResult {
6
+ current: string;
7
+ tracking: string;
8
+ ahead: number;
9
+ behind: number;
10
+ files: { path: string; working_dir: string; index: string }[];
11
+ recentCommits: { hash: string; date: string; message: string }[];
12
+ }
13
+
14
+ interface LogEntry {
15
+ hash: string;
16
+ date: string;
17
+ message: string;
18
+ author: string;
19
+ }
20
+
21
+ interface BranchesResult {
22
+ current: string;
23
+ branches: string[];
24
+ all: string[];
25
+ }
26
+
27
+ interface StashEntry {
28
+ index: number;
29
+ description: string;
30
+ }
31
+
32
+ export async function getStatus(): Promise<StatusResult> {
33
+ const status = await git.status();
34
+ const log = await git.log({ maxCount: 5 });
35
+
36
+ return {
37
+ current: status.current || "",
38
+ tracking: status.tracking || "",
39
+ ahead: status.ahead,
40
+ behind: status.behind,
41
+ files: status.files.map((f) => ({
42
+ path: f.path,
43
+ working_dir: f.working_dir,
44
+ index: f.index,
45
+ })),
46
+ recentCommits: log.all.map((c) => ({
47
+ hash: c.hash,
48
+ date: c.date,
49
+ message: c.message,
50
+ })),
51
+ };
52
+ }
53
+
54
+ export async function commit(message: string): Promise<string> {
55
+ const result = await git.commit(message);
56
+ return result.commit || "";
57
+ }
58
+
59
+ export async function push(force: boolean = false): Promise<string> {
60
+ const args = force ? (["-f"] as any) : undefined;
61
+ const result = await git.push("origin", undefined as any, args);
62
+ return result;
63
+ }
64
+
65
+ export async function pullRebase(): Promise<string> {
66
+ const result = await git.pull(["--rebase"]);
67
+ return result;
68
+ }
69
+
70
+ export async function log(maxCount: number = 10): Promise<LogEntry[]> {
71
+ const result = await git.log({ maxCount });
72
+ return result.all.map((c) => ({
73
+ hash: c.hash,
74
+ date: c.date,
75
+ message: c.message,
76
+ author: c.author_name,
77
+ }));
78
+ }
79
+
80
+ export async function diff(): Promise<string> {
81
+ const result = await git.diff();
82
+ return result;
83
+ }
84
+
85
+ export async function diffStaged(): Promise<string> {
86
+ const result = await git.diff(["--cached"]);
87
+ return result;
88
+ }
89
+
90
+ export async function getBranches(): Promise<BranchesResult> {
91
+ const result = await git.branch();
92
+ return {
93
+ current: result.current,
94
+ branches: result.branches
95
+ ? Object.keys(result.branches).filter((b) => b !== result.current)
96
+ : [],
97
+ all: result.all || [],
98
+ };
99
+ }
100
+
101
+ export async function createBranch(name: string): Promise<void> {
102
+ await git.branch([name]);
103
+ }
104
+
105
+ export async function deleteBranch(
106
+ name: string,
107
+ force: boolean = false,
108
+ ): Promise<void> {
109
+ const args = force ? ["-D", name] : ["-d", name];
110
+ await git.branch(args);
111
+ }
112
+
113
+ export async function switchBranch(name: string): Promise<void> {
114
+ await git.checkout(name);
115
+ }
116
+
117
+ export async function stash(): Promise<string> {
118
+ const result = await git.stash();
119
+ return result;
120
+ }
121
+
122
+ export async function stashPop(): Promise<string> {
123
+ const result = await git.stash(["pop"]);
124
+ return result;
125
+ }
126
+
127
+ export async function stashList(): Promise<StashEntry[]> {
128
+ const result = await git.stashList();
129
+ return result.all.map((s, i) => ({
130
+ index: i + 1,
131
+ description: s.message,
132
+ }));
133
+ }
134
+
135
+ export async function tag(name: string, message?: string): Promise<string> {
136
+ if (message) {
137
+ await git.tag(["-a", name, "-m", message]);
138
+ } else {
139
+ await git.tag([name]);
140
+ }
141
+ return name;
142
+ }
143
+
144
+ export async function tagList(): Promise<string[]> {
145
+ const result = await git.tag();
146
+ return result.split("\n").filter(Boolean);
147
+ }
148
+
149
+ export async function fetch(): Promise<string> {
150
+ const result = await git.fetch();
151
+ return result;
152
+ }
153
+
154
+ export async function undo(): Promise<string> {
155
+ const result = await git.reset(["--soft", "HEAD~1"]);
156
+ return result;
157
+ }
158
+
159
+ export async function getCurrentBranch(): Promise<string> {
160
+ const result = await git.branch();
161
+ return result.current;
162
+ }
163
+
164
+ export async function clone(
165
+ url: string,
166
+ dir?: string,
167
+ options?: {
168
+ ssh?: boolean;
169
+ depth?: number;
170
+ branch?: string;
171
+ recurseSubmodules?: boolean;
172
+ },
173
+ ): Promise<string> {
174
+ const args: string[] = [];
175
+ if (options?.depth) args.push("--depth", String(options.depth));
176
+ if (options?.branch) args.push("--branch", options.branch);
177
+ if (options?.recurseSubmodules) args.push("--recurse-submodules");
178
+ const targetDir = dir || url.split("/").pop()?.replace(".git", "") || "repo";
179
+ await git.clone(url, targetDir, args);
180
+ return targetDir;
181
+ }
182
+
183
+ export async function init(
184
+ dir?: string,
185
+ options?: { initialBranch?: string },
186
+ ): Promise<void> {
187
+ const opts: string[] = [];
188
+ if (options?.initialBranch)
189
+ opts.push("--initial-branch", options.initialBranch);
190
+ if (dir) opts.push(dir);
191
+ await git.init(opts.length > 0 ? opts : (true as any));
192
+ }
193
+
194
+ interface RemoteEntry {
195
+ name: string;
196
+ url: string;
197
+ }
198
+
199
+ export async function remoteAdd(name: string, url: string): Promise<void> {
200
+ await git.addRemote(name, url);
201
+ }
202
+
203
+ export async function remoteRemove(name: string): Promise<void> {
204
+ await git.removeRemote(name);
205
+ }
206
+
207
+ export async function remoteSetUrl(
208
+ name: string,
209
+ newUrl: string,
210
+ ): Promise<void> {
211
+ await git.raw(["remote", "set-url", name, newUrl]);
212
+ }
213
+
214
+ export async function remoteRename(
215
+ oldName: string,
216
+ newName: string,
217
+ ): Promise<void> {
218
+ await git.raw(["remote", "rename", oldName, newName]);
219
+ }
220
+
221
+ export async function remoteGetUrl(name: string): Promise<string> {
222
+ const result = await git.raw(["remote", "get-url", name]);
223
+ return result.trim();
224
+ }
225
+
226
+ export async function remoteList(): Promise<RemoteEntry[]> {
227
+ const result = await git.getRemotes(true);
228
+ return result.map((r) => ({ name: r.name, url: r.refs.fetch }));
229
+ }
230
+
231
+ interface WorktreeEntry {
232
+ path: string;
233
+ branch: string;
234
+ hash: string;
235
+ }
236
+
237
+ export async function worktreeAdd(
238
+ path: string,
239
+ branch?: string,
240
+ options?: { new?: boolean; detach?: boolean },
241
+ ): Promise<void> {
242
+ const args = ["worktree", "add"];
243
+ if (options?.detach) args.push("--detach");
244
+ args.push(path);
245
+ if (branch) {
246
+ if (options?.new) args.push(branch);
247
+ else args.push(branch);
248
+ }
249
+ await git.raw(args);
250
+ }
251
+
252
+ export async function worktreeList(): Promise<WorktreeEntry[]> {
253
+ const result = await git.raw(["worktree", "list", "--porcelain"]);
254
+ const entries: WorktreeEntry[] = [];
255
+ const lines = result.split("\n");
256
+ let current: Partial<WorktreeEntry> = {};
257
+ for (const line of lines) {
258
+ if (line.startsWith("worktree ")) {
259
+ current.path = line.slice(9).trim();
260
+ } else if (line.startsWith("HEAD ")) {
261
+ current.hash = line.slice(5).trim();
262
+ } else if (line.startsWith("branch ")) {
263
+ current.branch = line.slice(7).trim().replace("refs/heads/", "");
264
+ } else if (line === "") {
265
+ if (current.path) entries.push(current as WorktreeEntry);
266
+ current = {};
267
+ }
268
+ }
269
+ if (current.path) entries.push(current as WorktreeEntry);
270
+ return entries;
271
+ }
272
+
273
+ export async function worktreeRemove(
274
+ path: string,
275
+ force: boolean = false,
276
+ ): Promise<void> {
277
+ const args = ["worktree", "remove"];
278
+ if (force) args.push("--force");
279
+ args.push(path);
280
+ await git.raw(args);
281
+ }
282
+
283
+ export async function worktreePrune(
284
+ dryRun: boolean = false,
285
+ ): Promise<string[]> {
286
+ const args = ["worktree", "prune"];
287
+ if (dryRun) args.push("--dry-run");
288
+ const result = await git.raw(args);
289
+ return result.split("\n").filter(Boolean);
290
+ }
291
+
292
+ export async function merge(
293
+ branch: string,
294
+ options?: { noFF?: boolean; squash?: boolean; noCommit?: boolean },
295
+ ): Promise<string> {
296
+ const args = ["merge"];
297
+ if (options?.noFF) args.push("--no-ff");
298
+ if (options?.squash) args.push("--squash");
299
+ if (options?.noCommit) args.push("--no-commit");
300
+ args.push(branch);
301
+ const result = await git.raw(args);
302
+ return result;
303
+ }
304
+
305
+ export async function cherryPick(
306
+ commits: string[],
307
+ options?: { noCommit?: boolean; mainline?: number },
308
+ ): Promise<void> {
309
+ const args = ["cherry-pick"];
310
+ if (options?.noCommit) args.push("--no-commit");
311
+ if (options?.mainline) args.push("-m", String(options.mainline));
312
+ args.push(...commits);
313
+ await git.raw(args);
314
+ }
315
+
316
+ export async function cherryPickContinue(): Promise<void> {
317
+ await git.raw(["cherry-pick", "--continue"]);
318
+ }
319
+
320
+ export async function cherryPickAbort(): Promise<void> {
321
+ await git.raw(["cherry-pick", "--abort"]);
322
+ }
323
+
324
+ export async function clean(options?: {
325
+ dryRun?: boolean;
326
+ force?: boolean;
327
+ exclude?: string;
328
+ }): Promise<string[]> {
329
+ const args = ["clean", "-d"];
330
+ if (options?.dryRun) args.push("-n");
331
+ if (options?.force) args.push("-f");
332
+ else args.push("-f");
333
+ if (options?.exclude) args.push("-x", options.exclude);
334
+ const result = await git.raw(args);
335
+ return result.split("\n").filter(Boolean);
336
+ }
337
+
338
+ export async function archive(
339
+ format: string,
340
+ options?: { prefix?: string; output?: string; treeIsh?: string },
341
+ ): Promise<string> {
342
+ const args = ["archive", `--format=${format}`];
343
+ if (options?.prefix) args.push(`--prefix=${options.prefix}`);
344
+ if (options?.output) args.push(`--output=${options.output}`);
345
+ if (options?.treeIsh) args.push(options.treeIsh);
346
+ else args.push("HEAD");
347
+ const result = await git.raw(args);
348
+ return result;
349
+ }
350
+
351
+ export async function bisectStart(
352
+ bad?: string,
353
+ good?: string[],
354
+ ): Promise<void> {
355
+ await git.raw(["bisect", "start"]);
356
+ if (bad) await git.raw(["bisect", "bad", bad]);
357
+ if (good && good.length > 0) {
358
+ for (const g of good) await git.raw(["bisect", "good", g]);
359
+ }
360
+ }
361
+
362
+ export async function bisectBad(commit?: string): Promise<void> {
363
+ const args = ["bisect", "bad"];
364
+ if (commit) args.push(commit);
365
+ await git.raw(args);
366
+ }
367
+
368
+ export async function bisectGood(commits: string[]): Promise<void> {
369
+ for (const c of commits) await git.raw(["bisect", "good", c]);
370
+ }
371
+
372
+ export async function bisectReset(): Promise<void> {
373
+ await git.raw(["bisect", "reset"]);
374
+ }
375
+
376
+ export async function bisectLog(): Promise<string> {
377
+ return await git.raw(["bisect", "log"]);
378
+ }
379
+
380
+ export async function bisectRun(cmd: string): Promise<void> {
381
+ await git.raw(["bisect", "run", cmd]);
382
+ }
@@ -0,0 +1,31 @@
1
+ import chalk from "chalk";
2
+ import { colors } from "../constants/colors.js";
3
+ import { icons } from "../constants/messages.js";
4
+
5
+ export function success(msg: string): void {
6
+ console.log(chalk.hex(colors.success)(`${icons.success} ${msg}`));
7
+ }
8
+
9
+ export function error(msg: string): void {
10
+ console.error(chalk.hex(colors.error)(`${icons.error} ${msg}`));
11
+ }
12
+
13
+ export function warning(msg: string): void {
14
+ console.warn(chalk.hex(colors.warning)(`${icons.warning} ${msg}`));
15
+ }
16
+
17
+ export function info(msg: string): void {
18
+ console.log(chalk.hex(colors.info)(`${icons.info} ${msg}`));
19
+ }
20
+
21
+ export function highlight(msg: string): void {
22
+ console.log(chalk.hex(colors.highlight)(`${icons.highlight} ${msg}`));
23
+ }
24
+
25
+ export function text(msg: string): void {
26
+ console.log(msg);
27
+ }
28
+
29
+ export function newline(): void {
30
+ console.log();
31
+ }
package/src/lib/ui.ts ADDED
@@ -0,0 +1,162 @@
1
+ import boxen from "boxen";
2
+ import chalk from "chalk";
3
+ import ora from "ora";
4
+ import { colors } from "../constants/colors.js";
5
+ import { formatting } from "../constants/messages.js";
6
+
7
+ function stripAnsi(str: string): string {
8
+ return str.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "");
9
+ }
10
+
11
+ function padEnd(str: string, len: number): string {
12
+ const visibleLen = stripAnsi(str).length;
13
+ const diff = len - visibleLen;
14
+ return diff > 0 ? str + " ".repeat(diff) : str;
15
+ }
16
+
17
+ export function showBox(
18
+ title: string,
19
+ content: string,
20
+ options?: Record<string, unknown>,
21
+ ): void {
22
+ const boxContent = `${chalk.bold(title)}\n\n${content}`;
23
+ console.log(
24
+ boxen(boxContent, {
25
+ padding: 1,
26
+ borderColor: "cyan",
27
+ borderStyle: "round",
28
+ ...options,
29
+ }),
30
+ );
31
+ }
32
+
33
+ export function createTable(headers: string[], rows: string[][]): string {
34
+ const _colCount = headers.length;
35
+ const colWidths: number[] = headers.map((h, i) => {
36
+ const maxRow = Math.max(...rows.map((r) => stripAnsi(r[i] || "").length));
37
+ return Math.max(stripAnsi(h).length, maxRow);
38
+ });
39
+
40
+ const separator = `+${colWidths.map((w) => "-".repeat(w + 2)).join("+")}+`;
41
+ const headerRow = `| ${headers.map((h, i) => padEnd(h, colWidths[i])).join(" | ")} |`;
42
+ const dataRows = rows.map(
43
+ (row) =>
44
+ "| " +
45
+ row.map((cell, i) => padEnd(cell, colWidths[i])).join(" | ") +
46
+ " |",
47
+ );
48
+
49
+ return [separator, headerRow, separator, ...dataRows, separator].join("\n");
50
+ }
51
+
52
+ export async function withSpinner<T>(
53
+ text: string,
54
+ fn: () => Promise<T>,
55
+ ): Promise<T> {
56
+ const spinner = ora({
57
+ text,
58
+ color: "cyan",
59
+ }).start();
60
+
61
+ try {
62
+ const result = await fn();
63
+ spinner.succeed();
64
+ return result;
65
+ } catch (err) {
66
+ spinner.fail();
67
+ throw err;
68
+ }
69
+ }
70
+
71
+ export function showHeader(text: string): void {
72
+ console.log(chalk.hex(colors.highlight)(text));
73
+ console.log(chalk.hex(colors.info)(formatting.header));
74
+ }
75
+
76
+ export function showSeparator(): void {
77
+ console.log(chalk.dim(formatting.separator));
78
+ }
79
+
80
+ async function getInquirer() {
81
+ return await import("inquirer");
82
+ }
83
+
84
+ export async function confirm(
85
+ msg: string,
86
+ defaultValue: boolean = true,
87
+ ): Promise<boolean> {
88
+ const inquirer = await getInquirer();
89
+ const { value } = await inquirer.prompt([
90
+ {
91
+ type: "confirm",
92
+ name: "value",
93
+ message: msg,
94
+ default: defaultValue,
95
+ },
96
+ ]);
97
+ return value;
98
+ }
99
+
100
+ export async function select<T>(
101
+ message: string,
102
+ choices: { name: string; value: T }[],
103
+ ): Promise<T> {
104
+ const inquirer = await getInquirer();
105
+ const { value } = await inquirer.prompt([
106
+ {
107
+ type: "list",
108
+ name: "value",
109
+ message,
110
+ choices,
111
+ },
112
+ ]);
113
+ return value;
114
+ }
115
+
116
+ export async function input(
117
+ message: string,
118
+ defaultValue?: string,
119
+ ): Promise<string> {
120
+ const inquirer = await getInquirer();
121
+ const { value } = await inquirer.prompt([
122
+ {
123
+ type: "input",
124
+ name: "value",
125
+ message,
126
+ default: defaultValue,
127
+ },
128
+ ]);
129
+ return value;
130
+ }
131
+
132
+ export async function password(
133
+ message: string,
134
+ mask: string = "*",
135
+ ): Promise<string> {
136
+ const inquirer = await getInquirer();
137
+ const { value } = await inquirer.prompt([
138
+ {
139
+ type: "password",
140
+ name: "value",
141
+ message,
142
+ mask,
143
+ },
144
+ ]);
145
+ return value;
146
+ }
147
+
148
+ export async function checkbox(
149
+ message: string,
150
+ choices: { name: string; value: string; checked?: boolean }[],
151
+ ): Promise<string[]> {
152
+ const inquirer = await getInquirer();
153
+ const { value } = await inquirer.prompt([
154
+ {
155
+ type: "checkbox",
156
+ name: "value",
157
+ message,
158
+ choices,
159
+ },
160
+ ]);
161
+ return value;
162
+ }
@@ -0,0 +1,27 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export function validateEmail(email: string): boolean {
4
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
5
+ return emailRegex.test(email);
6
+ }
7
+
8
+ export function validateGitHubToken(token: string): boolean {
9
+ const tokenRegex = /^(ghp_|gho_|ghu_|ghs_|ghr_|github_pat_)[a-zA-Z0-9]{4,}$/;
10
+ return tokenRegex.test(token);
11
+ }
12
+
13
+ export function validateNotEmpty(input: string): boolean | string {
14
+ if (input.length === 0) {
15
+ return "Value cannot be empty";
16
+ }
17
+ return true;
18
+ }
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
+ }
@@ -0,0 +1,9 @@
1
+ [
2
+ { "value": "feat", "description": "A new feature" },
3
+ { "value": "fix", "description": "A bug fix" },
4
+ { "value": "docs", "description": "Documentation only changes" },
5
+ { "value": "style", "description": "Code style changes (formatting, etc)" },
6
+ { "value": "refactor", "description": "Code refactoring" },
7
+ { "value": "test", "description": "Adding or fixing tests" },
8
+ { "value": "chore", "description": "Build process or tool changes" }
9
+ ]
@@ -0,0 +1,21 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+
5
+ export async function ensureDir(dirPath: string): Promise<void> {
6
+ if (!existsSync(dirPath)) {
7
+ await mkdir(dirPath, { recursive: true });
8
+ }
9
+ }
10
+
11
+ export async function readJSON(path: string): Promise<any> {
12
+ const content = await readFile(path, "utf-8");
13
+ return JSON.parse(content);
14
+ }
15
+
16
+ export async function writeJSON(path: string, data: any): Promise<void> {
17
+ const directory = dirname(path);
18
+ await ensureDir(directory);
19
+ const content = JSON.stringify(data, null, 2);
20
+ await writeFile(path, content, "utf-8");
21
+ }
@@ -0,0 +1,19 @@
1
+ export function capitalize(str: string): string {
2
+ if (str.length === 0) return str;
3
+ return str.charAt(0).toUpperCase() + str.slice(1);
4
+ }
5
+
6
+ export function truncate(str: string, maxLength: number): string {
7
+ if (str.length <= maxLength) return str;
8
+ return `${str.slice(0, maxLength - 3)}...`;
9
+ }
10
+
11
+ export function formatTimestamp(date: Date): string {
12
+ const year = date.getFullYear();
13
+ const month = String(date.getMonth() + 1).padStart(2, "0");
14
+ const day = String(date.getDate()).padStart(2, "0");
15
+ const hours = String(date.getHours()).padStart(2, "0");
16
+ const minutes = String(date.getMinutes()).padStart(2, "0");
17
+ const seconds = String(date.getSeconds()).padStart(2, "0");
18
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
19
+ }