@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.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/bin/forge.js +2 -0
- package/dist/cli.js +35641 -0
- package/package.json +65 -0
- package/src/cli.ts +78 -0
- package/src/commands/alias.ts +66 -0
- package/src/commands/archive.ts +35 -0
- package/src/commands/bisect.ts +102 -0
- package/src/commands/branch.ts +46 -0
- package/src/commands/cherry-pick.ts +57 -0
- package/src/commands/clean.ts +76 -0
- package/src/commands/clone.ts +100 -0
- package/src/commands/commit.ts +93 -0
- package/src/commands/config.ts +48 -0
- package/src/commands/diff.ts +26 -0
- package/src/commands/fetch.ts +20 -0
- package/src/commands/help.ts +58 -0
- package/src/commands/init.ts +37 -0
- package/src/commands/log.ts +29 -0
- package/src/commands/merge.ts +37 -0
- package/src/commands/push.ts +35 -0
- package/src/commands/remote.ts +107 -0
- package/src/commands/reset.ts +30 -0
- package/src/commands/setup.ts +95 -0
- package/src/commands/stash.ts +44 -0
- package/src/commands/status.ts +74 -0
- package/src/commands/sync.ts +20 -0
- package/src/commands/tag.ts +41 -0
- package/src/commands/undo.ts +27 -0
- package/src/commands/version.ts +19 -0
- package/src/commands/worktree.ts +92 -0
- package/src/constants/colors.ts +7 -0
- package/src/constants/commit-types.ts +24 -0
- package/src/constants/messages.ts +23 -0
- package/src/lib/auth.ts +95 -0
- package/src/lib/config.ts +99 -0
- package/src/lib/git.ts +382 -0
- package/src/lib/logger.ts +31 -0
- package/src/lib/ui.ts +162 -0
- package/src/lib/validators.ts +27 -0
- package/src/templates/commit-types.json +9 -0
- package/src/utils/files.ts +21 -0
- package/src/utils/strings.ts +19 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import simpleGit from "simple-git";
|
|
3
|
+
import { commitTypes } from "../constants/messages.js";
|
|
4
|
+
import * as git from "../lib/git.js";
|
|
5
|
+
import { error, info, newline, success, text, warning } from "../lib/logger.js";
|
|
6
|
+
import { checkbox, confirm, input, select, withSpinner } from "../lib/ui.js";
|
|
7
|
+
|
|
8
|
+
export default function register(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command("commit")
|
|
11
|
+
.description("Create a commit (interactive or quick)")
|
|
12
|
+
.option("-m, --message <message>", "Quick commit message")
|
|
13
|
+
.option("--amend", "Amend last commit")
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
try {
|
|
16
|
+
if (options.amend) {
|
|
17
|
+
const sg = simpleGit();
|
|
18
|
+
await sg.commit("", { "--amend": null, "--no-edit": null });
|
|
19
|
+
success("Last commit amended.");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (options.message) {
|
|
24
|
+
const hash = await git.commit(options.message);
|
|
25
|
+
success(`Committed: ${hash}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const status = await git.getStatus();
|
|
30
|
+
|
|
31
|
+
if (status.files.length === 0) {
|
|
32
|
+
warning(
|
|
33
|
+
"No files to commit. Stage files first or use -m for quick commit.",
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fileChoices = status.files.map((f) => ({
|
|
39
|
+
name: `${f.path} (${f.working_dir || f.index})`,
|
|
40
|
+
value: f.path,
|
|
41
|
+
checked: f.index !== " " || f.working_dir !== " ",
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const selectedFiles = await checkbox(
|
|
45
|
+
"Select files to stage",
|
|
46
|
+
fileChoices,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (selectedFiles.length === 0) {
|
|
50
|
+
warning("No files selected. Commit cancelled.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sg = simpleGit();
|
|
55
|
+
await withSpinner("Staging files...", async () => {
|
|
56
|
+
await sg.add(selectedFiles);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
newline();
|
|
60
|
+
const typeChoices = commitTypes.map((t) => ({
|
|
61
|
+
name: `${t.value.padEnd(10)} ${t.description}`,
|
|
62
|
+
value: t.value,
|
|
63
|
+
}));
|
|
64
|
+
const type = await select("Commit type", typeChoices);
|
|
65
|
+
|
|
66
|
+
const scope = await input("Scope (optional)");
|
|
67
|
+
|
|
68
|
+
const description = await input("Description");
|
|
69
|
+
|
|
70
|
+
const fullMessage = scope
|
|
71
|
+
? `${type}(${scope}): ${description}`
|
|
72
|
+
: `${type}: ${description}`;
|
|
73
|
+
|
|
74
|
+
newline();
|
|
75
|
+
info("Commit preview:");
|
|
76
|
+
text(` ${fullMessage}`);
|
|
77
|
+
newline();
|
|
78
|
+
|
|
79
|
+
const proceed = await confirm("Proceed with commit?");
|
|
80
|
+
if (!proceed) {
|
|
81
|
+
info("Commit cancelled.");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const hash = await git.commit(fullMessage);
|
|
86
|
+
success(`Committed: ${hash}`);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
error(
|
|
89
|
+
`Commit failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import { ConfigManager } from "../lib/config.js";
|
|
4
|
+
import { info } from "../lib/logger.js";
|
|
5
|
+
import { showBox } from "../lib/ui.js";
|
|
6
|
+
|
|
7
|
+
export default function register(program: Command): void {
|
|
8
|
+
program
|
|
9
|
+
.command("config")
|
|
10
|
+
.description("Manage configuration")
|
|
11
|
+
.option("--show", "Show config")
|
|
12
|
+
.option("--edit", "Edit config in editor")
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
const config = new ConfigManager();
|
|
16
|
+
|
|
17
|
+
if (options.edit) {
|
|
18
|
+
const editor = process.env.EDITOR || "vim";
|
|
19
|
+
const configPath = config.getPath();
|
|
20
|
+
spawnSync(editor, [configPath], { stdio: "inherit" });
|
|
21
|
+
info("Config edited.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const user = config.get("user");
|
|
26
|
+
const github = config.get("github");
|
|
27
|
+
const prefs = config.get("preferences");
|
|
28
|
+
|
|
29
|
+
const content = [
|
|
30
|
+
`User:`,
|
|
31
|
+
` Name: ${user.name || "(not set)"}`,
|
|
32
|
+
` Email: ${user.email || "(not set)"}`,
|
|
33
|
+
``,
|
|
34
|
+
`GitHub:`,
|
|
35
|
+
` Token: ${github.token ? (github.token.includes(":") ? "*** (encrypted)" : "***") : "(not set)"}`,
|
|
36
|
+
``,
|
|
37
|
+
`Preferences:`,
|
|
38
|
+
` Auto Push: ${prefs.autoPush}`,
|
|
39
|
+
` Commit Template: ${prefs.commitTemplate || "(default)"}`,
|
|
40
|
+
` Editor: ${prefs.editor}`,
|
|
41
|
+
].join("\n");
|
|
42
|
+
|
|
43
|
+
showBox("Configuration", content);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
info(`Config: ${err instanceof Error ? err.message : String(err)}`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import * as git from "../lib/git.js";
|
|
3
|
+
import { info, text } from "../lib/logger.js";
|
|
4
|
+
|
|
5
|
+
export default function register(program: Command): void {
|
|
6
|
+
program
|
|
7
|
+
.command("diff")
|
|
8
|
+
.description("Show changes")
|
|
9
|
+
.option("--staged", "Show staged changes")
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
try {
|
|
12
|
+
const output = options.staged
|
|
13
|
+
? await git.diffStaged()
|
|
14
|
+
: await git.diff();
|
|
15
|
+
|
|
16
|
+
if (!output) {
|
|
17
|
+
info("No changes.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
text(output);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
info(`Diff: ${err instanceof Error ? err.message : String(err)}`);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import * as git from "../lib/git.js";
|
|
3
|
+
import { error, success } from "../lib/logger.js";
|
|
4
|
+
import { withSpinner } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("fetch")
|
|
9
|
+
.description("Fetch from remote")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const result = await withSpinner("Fetching...", () => git.fetch());
|
|
13
|
+
success(`Fetch complete: ${result}`);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
error(
|
|
16
|
+
`Fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { newline, text } from "../lib/logger.js";
|
|
3
|
+
import { createTable, showHeader } from "../lib/ui.js";
|
|
4
|
+
|
|
5
|
+
const commands: { name: string; description: string }[] = [
|
|
6
|
+
{ name: "setup", description: "Interactive setup wizard" },
|
|
7
|
+
{ name: "commit", description: "Create a commit (interactive or quick)" },
|
|
8
|
+
{ name: "push", description: "Push to remote" },
|
|
9
|
+
{ name: "sync", description: "Pull with rebase" },
|
|
10
|
+
{ name: "fetch", description: "Fetch from remote" },
|
|
11
|
+
{ name: "status", description: "Show detailed repository status" },
|
|
12
|
+
{ name: "log", description: "Show commit history" },
|
|
13
|
+
{ name: "diff", description: "Show changes" },
|
|
14
|
+
{ name: "branch", description: "Manage branches" },
|
|
15
|
+
{ name: "stash", description: "Manage stashes" },
|
|
16
|
+
{ name: "tag", description: "Manage tags" },
|
|
17
|
+
{ name: "alias", description: "Manage aliases" },
|
|
18
|
+
{ name: "config", description: "Manage configuration" },
|
|
19
|
+
{ name: "undo", description: "Undo last commit (soft reset)" },
|
|
20
|
+
{ name: "reset", description: "Reset all configuration" },
|
|
21
|
+
{ name: "help", description: "Show this help" },
|
|
22
|
+
{ name: "version", description: "Show version" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
export default function register(program: Command): void {
|
|
26
|
+
program
|
|
27
|
+
.command("help")
|
|
28
|
+
.description("Show comprehensive help")
|
|
29
|
+
.action(async () => {
|
|
30
|
+
showHeader("Forge - Git CLI");
|
|
31
|
+
|
|
32
|
+
const rows = commands.map((c) => [c.name, c.description]);
|
|
33
|
+
text(createTable(["Command", "Description"], rows));
|
|
34
|
+
|
|
35
|
+
newline();
|
|
36
|
+
showHeader("Usage Examples");
|
|
37
|
+
text("");
|
|
38
|
+
text(" fg setup Start interactive setup");
|
|
39
|
+
text(' fg commit -m "fix: resolve issue" Quick commit with message');
|
|
40
|
+
text(" fg commit Interactive commit");
|
|
41
|
+
text(" fg push Push current branch");
|
|
42
|
+
text(
|
|
43
|
+
" fg push --force Force push (with confirmation)",
|
|
44
|
+
);
|
|
45
|
+
text(" fg status Show repository status");
|
|
46
|
+
text(" fg log -n 20 Show last 20 commits");
|
|
47
|
+
text(" fg branch -n feature-x Create a new branch");
|
|
48
|
+
text(" fg branch -s feature-x Switch to a branch");
|
|
49
|
+
text(" fg sync Pull with rebase");
|
|
50
|
+
text(" fg diff --staged Show staged changes");
|
|
51
|
+
text(" fg stash Stash changes");
|
|
52
|
+
text(" fg stash --pop Apply latest stash");
|
|
53
|
+
text(" fg alias add co checkout Create an alias");
|
|
54
|
+
text(" fg config --show View configuration");
|
|
55
|
+
text(" fg config --edit Edit configuration");
|
|
56
|
+
newline();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import type { Command } from "commander";
|
|
3
|
+
import simpleGit from "simple-git";
|
|
4
|
+
import * as git from "../lib/git.js";
|
|
5
|
+
import { error, success } from "../lib/logger.js";
|
|
6
|
+
import { showBox } from "../lib/ui.js";
|
|
7
|
+
|
|
8
|
+
export default function register(program: Command): void {
|
|
9
|
+
program
|
|
10
|
+
.command("init")
|
|
11
|
+
.description("Initialize a new Git repository")
|
|
12
|
+
.argument("[dir]", "Target directory")
|
|
13
|
+
.option("--initial-commit", "Create an initial commit")
|
|
14
|
+
.option("--branch <name>", "Set initial branch name")
|
|
15
|
+
.action(async (dir, options) => {
|
|
16
|
+
try {
|
|
17
|
+
const targetDir = dir || ".";
|
|
18
|
+
|
|
19
|
+
await git.init(dir, { initialBranch: options.branch });
|
|
20
|
+
|
|
21
|
+
if (options.initialCommit) {
|
|
22
|
+
const sg = simpleGit(resolve(targetDir));
|
|
23
|
+
await sg.raw(["commit", "--allow-empty", "-m", "Initial commit"]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const absPath = resolve(targetDir);
|
|
27
|
+
const content = [`Path: ${absPath}`].join("\n");
|
|
28
|
+
showBox("Repository Initialized", content);
|
|
29
|
+
|
|
30
|
+
success(`Git repository initialized at ${absPath}.`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
error(
|
|
33
|
+
`Init failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import * as git from "../lib/git.js";
|
|
3
|
+
import { info } from "../lib/logger.js";
|
|
4
|
+
import { createTable } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("log")
|
|
9
|
+
.description("Show commit history")
|
|
10
|
+
.option("-n, --number <count>", "Number of commits", "10")
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
try {
|
|
13
|
+
const count = parseInt(options.number, 10) || 10;
|
|
14
|
+
const commits = await git.log(count);
|
|
15
|
+
|
|
16
|
+
const rows = commits.map((c) => [
|
|
17
|
+
c.hash.substring(0, 7),
|
|
18
|
+
c.date.substring(0, 10),
|
|
19
|
+
c.author,
|
|
20
|
+
c.message.substring(0, 60),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
info(`Last ${commits.length} commits:`);
|
|
24
|
+
console.log(createTable(["Hash", "Date", "Author", "Message"], rows));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
info(`Log: ${err instanceof Error ? err.message : String(err)}`);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
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("merge")
|
|
8
|
+
.description("Merge branches")
|
|
9
|
+
.argument("<branch>", "Branch to merge into current")
|
|
10
|
+
.option(
|
|
11
|
+
"--no-ff",
|
|
12
|
+
"Create a merge commit even when fast-forward is possible",
|
|
13
|
+
)
|
|
14
|
+
.option("--squash", "Squash commits into one")
|
|
15
|
+
.option("--no-commit", "Perform merge but do not commit")
|
|
16
|
+
.action(
|
|
17
|
+
async (
|
|
18
|
+
branch: string,
|
|
19
|
+
options: { noFF?: boolean; squash?: boolean; noCommit?: boolean },
|
|
20
|
+
) => {
|
|
21
|
+
try {
|
|
22
|
+
const currentBranch = await git.getCurrentBranch();
|
|
23
|
+
info(`Merging '${branch}' into '${currentBranch}'...`);
|
|
24
|
+
const result = await git.merge(branch, {
|
|
25
|
+
noFF: options.noFF ?? undefined,
|
|
26
|
+
squash: options.squash ?? undefined,
|
|
27
|
+
noCommit: options.noCommit ?? undefined,
|
|
28
|
+
});
|
|
29
|
+
success(`Merge completed: ${result}`);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error(
|
|
32
|
+
`Merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
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, withSpinner } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("push")
|
|
9
|
+
.description("Push to remote")
|
|
10
|
+
.option("--force", "Force push (with warning)")
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
try {
|
|
13
|
+
const branch = await git.getCurrentBranch();
|
|
14
|
+
info(`Pushing to origin/${branch}`);
|
|
15
|
+
|
|
16
|
+
if (options.force) {
|
|
17
|
+
warning("Force push will overwrite remote history.");
|
|
18
|
+
const proceed = await confirm("Are you sure you want to force push?");
|
|
19
|
+
if (!proceed) {
|
|
20
|
+
info("Push cancelled.");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const result = await withSpinner("Pushing...", () =>
|
|
26
|
+
git.push(!!options.force),
|
|
27
|
+
);
|
|
28
|
+
success(`Push complete: ${result}`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
error(
|
|
31
|
+
`Push failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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 { confirm, createTable } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
const remote = program.command("remote").description("Manage remotes");
|
|
8
|
+
|
|
9
|
+
remote
|
|
10
|
+
.command("add <name> <url>")
|
|
11
|
+
.description("Add a new remote")
|
|
12
|
+
.action(async (name, url) => {
|
|
13
|
+
try {
|
|
14
|
+
await git.remoteAdd(name, url);
|
|
15
|
+
success(`Remote '${name}' added (${url}).`);
|
|
16
|
+
} catch (err) {
|
|
17
|
+
error(
|
|
18
|
+
`Failed to add remote: ${err instanceof Error ? err.message : String(err)}`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
remote
|
|
24
|
+
.command("remove <name>")
|
|
25
|
+
.description("Remove a remote")
|
|
26
|
+
.action(async (name) => {
|
|
27
|
+
try {
|
|
28
|
+
const confirmed = await confirm(
|
|
29
|
+
`Are you sure you want to remove remote '${name}'?`,
|
|
30
|
+
false,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (!confirmed) {
|
|
34
|
+
info("Canceled.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
await git.remoteRemove(name);
|
|
39
|
+
success(`Remote '${name}' removed.`);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
error(
|
|
42
|
+
`Failed to remove remote: ${err instanceof Error ? err.message : String(err)}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
remote
|
|
48
|
+
.command("set-url <name> <new-url>")
|
|
49
|
+
.description("Change a remote URL")
|
|
50
|
+
.action(async (name, newUrl) => {
|
|
51
|
+
try {
|
|
52
|
+
await git.remoteSetUrl(name, newUrl);
|
|
53
|
+
success(`Remote '${name}' URL updated to ${newUrl}.`);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
error(
|
|
56
|
+
`Failed to update remote URL: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
remote
|
|
62
|
+
.command("rename <old-name> <new-name>")
|
|
63
|
+
.description("Rename a remote")
|
|
64
|
+
.action(async (oldName, newName) => {
|
|
65
|
+
try {
|
|
66
|
+
await git.remoteRename(oldName, newName);
|
|
67
|
+
success(`Remote '${oldName}' renamed to '${newName}'.`);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
error(
|
|
70
|
+
`Failed to rename remote: ${err instanceof Error ? err.message : String(err)}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
remote
|
|
76
|
+
.command("get-url <name>")
|
|
77
|
+
.description("Get the URL of a remote")
|
|
78
|
+
.action(async (name) => {
|
|
79
|
+
try {
|
|
80
|
+
const url = await git.remoteGetUrl(name);
|
|
81
|
+
info(`Remote '${name}': ${url}`);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
error(
|
|
84
|
+
`Failed to get remote URL: ${err instanceof Error ? err.message : String(err)}`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
remote.action(async () => {
|
|
90
|
+
try {
|
|
91
|
+
const remotes = await git.remoteList();
|
|
92
|
+
|
|
93
|
+
if (remotes.length === 0) {
|
|
94
|
+
info("No remotes configured.");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const rows = remotes.map((r) => [r.name, r.url]);
|
|
99
|
+
info("Remotes:");
|
|
100
|
+
console.log(createTable(["Name", "URL"], rows));
|
|
101
|
+
} catch (err) {
|
|
102
|
+
error(
|
|
103
|
+
`Failed to list remotes: ${err instanceof Error ? err.message : String(err)}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { ConfigManager } from "../lib/config.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("reset")
|
|
9
|
+
.description("Reset all configuration")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
warning("This will delete all Forge configuration.");
|
|
13
|
+
const proceed = await confirm(
|
|
14
|
+
"Are you sure? This will delete all configuration",
|
|
15
|
+
);
|
|
16
|
+
if (!proceed) {
|
|
17
|
+
info("Reset cancelled.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const config = new ConfigManager();
|
|
22
|
+
config.clear();
|
|
23
|
+
success("Configuration reset successfully.");
|
|
24
|
+
} catch (err) {
|
|
25
|
+
error(
|
|
26
|
+
`Reset failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { encryptToken } from "../lib/auth.js";
|
|
3
|
+
import { ConfigManager } from "../lib/config.js";
|
|
4
|
+
import { error, info, newline, success, warning } from "../lib/logger.js";
|
|
5
|
+
import { confirm, input, password, showBox } from "../lib/ui.js";
|
|
6
|
+
import {
|
|
7
|
+
validateEmail,
|
|
8
|
+
validateGitHubToken,
|
|
9
|
+
validateGitInstalled,
|
|
10
|
+
} from "../lib/validators.js";
|
|
11
|
+
|
|
12
|
+
export default function register(program: Command): void {
|
|
13
|
+
program
|
|
14
|
+
.command("setup")
|
|
15
|
+
.description("Interactive setup wizard")
|
|
16
|
+
.action(async () => {
|
|
17
|
+
try {
|
|
18
|
+
const config = new ConfigManager();
|
|
19
|
+
|
|
20
|
+
if (config.isConfigured()) {
|
|
21
|
+
warning("Forge is already configured. You are about to reconfigure.");
|
|
22
|
+
const proceed = await confirm("Continue with reconfiguration?");
|
|
23
|
+
if (!proceed) {
|
|
24
|
+
info("Setup cancelled.");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const gitInstalled = await validateGitInstalled();
|
|
30
|
+
if (!gitInstalled) {
|
|
31
|
+
error("Git is not installed. Please install Git first.");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
newline();
|
|
36
|
+
info("Starting setup...");
|
|
37
|
+
newline();
|
|
38
|
+
|
|
39
|
+
const name = await input("Your name");
|
|
40
|
+
|
|
41
|
+
let email = await input("Your email");
|
|
42
|
+
while (!validateEmail(email)) {
|
|
43
|
+
error("Invalid email format.");
|
|
44
|
+
email = await input("Your email");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let token = await input("GitHub token (optional)");
|
|
48
|
+
if (token && !validateGitHubToken(token)) {
|
|
49
|
+
warning("Token format looks unusual. It will be saved as-is.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (token) {
|
|
53
|
+
const encrypt = await confirm(
|
|
54
|
+
"Encrypt token with a master password?",
|
|
55
|
+
false,
|
|
56
|
+
);
|
|
57
|
+
if (encrypt) {
|
|
58
|
+
const masterPassword = await password("Master password:");
|
|
59
|
+
token = await encryptToken(token, masterPassword);
|
|
60
|
+
success("Token encrypted successfully.");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
config.set("user", { name, email });
|
|
65
|
+
config.set("github", { token });
|
|
66
|
+
config.set("preferences", {
|
|
67
|
+
autoPush: false,
|
|
68
|
+
commitTemplate: "",
|
|
69
|
+
editor: process.env.EDITOR || "vim",
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
newline();
|
|
73
|
+
const tokenStatus = token
|
|
74
|
+
? token.includes(":")
|
|
75
|
+
? "Set (encrypted)"
|
|
76
|
+
: "Set"
|
|
77
|
+
: "Not set";
|
|
78
|
+
showBox(
|
|
79
|
+
"Configuration Complete",
|
|
80
|
+
[
|
|
81
|
+
`User: ${name} <${email}>`,
|
|
82
|
+
`Token: ${tokenStatus}`,
|
|
83
|
+
`Git: Installed`,
|
|
84
|
+
`Config: ${config.getPath()}`,
|
|
85
|
+
].join("\n"),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
success("Forge is ready to use!");
|
|
89
|
+
} catch (err) {
|
|
90
|
+
error(
|
|
91
|
+
`Setup failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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, input, withSpinner } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("stash")
|
|
9
|
+
.description("Manage stashes")
|
|
10
|
+
.option("--pop", "Apply and remove latest stash")
|
|
11
|
+
.option("--list", "List all stashes")
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
try {
|
|
14
|
+
if (options.pop) {
|
|
15
|
+
const result = await withSpinner("Applying stash...", () =>
|
|
16
|
+
git.stashPop(),
|
|
17
|
+
);
|
|
18
|
+
success(`Stash popped: ${result}`);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (options.list) {
|
|
23
|
+
const stashes = await git.stashList();
|
|
24
|
+
if (stashes.length === 0) {
|
|
25
|
+
info("No stashes found.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rows = stashes.map((s) => [String(s.index), s.description]);
|
|
30
|
+
info("Stashes:");
|
|
31
|
+
console.log(createTable(["#", "Description"], rows));
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const _message = await input("Stash message (optional)");
|
|
36
|
+
const result = await withSpinner("Stashing...", () => git.stash());
|
|
37
|
+
success(`Stash saved: ${result}`);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
error(
|
|
40
|
+
`Stash operation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|