@hallaxius/forge 0.1.2 → 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.
- package/dist/cli.js +31666 -8923
- package/package.json +7 -4
- package/src/cli.ts +10 -10
- package/src/commands/ci.ts +28 -0
- package/src/commands/commit.ts +2 -7
- package/src/commands/init.ts +3 -7
- package/src/commands/issue.ts +63 -0
- package/src/commands/pr.ts +65 -0
- package/src/commands/release.ts +26 -0
- package/src/commands/setup.ts +32 -33
- package/src/lib/auth.ts +95 -18
- package/src/lib/config.ts +12 -3
- package/src/lib/git.ts +397 -236
- package/src/lib/github.ts +160 -0
- package/src/lib/validators.ts +0 -11
- package/src/version.const.ts +1 -1
- package/src/commands/archive.ts +0 -35
- package/src/commands/bisect.ts +0 -102
- package/src/commands/cherry-pick.ts +0 -57
- package/src/commands/clean.ts +0 -76
- package/src/commands/worktree.ts +0 -92
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hallaxius/forge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "A modern Git CLI with professional UX",
|
|
5
5
|
"author": "hallaxius",
|
|
6
6
|
"license": "MIT",
|
|
@@ -26,14 +26,16 @@
|
|
|
26
26
|
"major": "npm version major && npm publish"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
+
"@octokit/rest": "^21.1.0",
|
|
29
30
|
"boxen": "^7.1.0",
|
|
30
31
|
"chalk": "^5.3.0",
|
|
31
32
|
"commander": "^12.0.0",
|
|
32
33
|
"conf": "^12.0.0",
|
|
34
|
+
"diff": "^7.0.0",
|
|
33
35
|
"figures": "^5.0.0",
|
|
34
36
|
"inquirer": "^14.0.2",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
+
"isomorphic-git": "^1.38.0",
|
|
38
|
+
"ora": "^8.0.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@biomejs/biome": "2.5.1",
|
|
@@ -67,6 +69,7 @@
|
|
|
67
69
|
"developer-tools",
|
|
68
70
|
"productivity",
|
|
69
71
|
"bun",
|
|
70
|
-
"command-line"
|
|
72
|
+
"command-line",
|
|
73
|
+
"github-api"
|
|
71
74
|
]
|
|
72
75
|
}
|
package/src/cli.ts
CHANGED
|
@@ -18,11 +18,9 @@ program
|
|
|
18
18
|
.version(pkg.version);
|
|
19
19
|
|
|
20
20
|
import registerAlias from "./commands/alias.js";
|
|
21
|
-
|
|
22
|
-
import registerBisect from "./commands/bisect.js";
|
|
21
|
+
|
|
23
22
|
import registerBranch from "./commands/branch.js";
|
|
24
|
-
import
|
|
25
|
-
import registerClean from "./commands/clean.js";
|
|
23
|
+
import registerCi from "./commands/ci.js";
|
|
26
24
|
import registerClone from "./commands/clone.js";
|
|
27
25
|
import registerCommit from "./commands/commit.js";
|
|
28
26
|
import registerConfig from "./commands/config.js";
|
|
@@ -30,9 +28,12 @@ import registerDiff from "./commands/diff.js";
|
|
|
30
28
|
import registerFetch from "./commands/fetch.js";
|
|
31
29
|
import registerHelp from "./commands/help.js";
|
|
32
30
|
import registerInit from "./commands/init.js";
|
|
31
|
+
import registerIssue from "./commands/issue.js";
|
|
33
32
|
import registerLog from "./commands/log.js";
|
|
34
33
|
import registerMerge from "./commands/merge.js";
|
|
34
|
+
import registerPr from "./commands/pr.js";
|
|
35
35
|
import registerPush from "./commands/push.js";
|
|
36
|
+
import registerRelease from "./commands/release.js";
|
|
36
37
|
import registerRemote from "./commands/remote.js";
|
|
37
38
|
import registerReset from "./commands/reset.js";
|
|
38
39
|
import registerSetup from "./commands/setup.js";
|
|
@@ -42,7 +43,6 @@ import registerSync from "./commands/sync.js";
|
|
|
42
43
|
import registerTag from "./commands/tag.js";
|
|
43
44
|
import registerUndo from "./commands/undo.js";
|
|
44
45
|
import registerVersion from "./commands/version.js";
|
|
45
|
-
import registerWorktree from "./commands/worktree.js";
|
|
46
46
|
|
|
47
47
|
registerSetup(program);
|
|
48
48
|
registerCommit(program);
|
|
@@ -64,12 +64,12 @@ registerVersion(program);
|
|
|
64
64
|
registerClone(program);
|
|
65
65
|
registerInit(program);
|
|
66
66
|
registerRemote(program);
|
|
67
|
-
|
|
67
|
+
|
|
68
|
+
registerCi(program);
|
|
69
|
+
registerIssue(program);
|
|
68
70
|
registerMerge(program);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
registerArchive(program);
|
|
72
|
-
registerBisect(program);
|
|
71
|
+
registerPr(program);
|
|
72
|
+
registerRelease(program);
|
|
73
73
|
|
|
74
74
|
if (process.argv.length <= 2) {
|
|
75
75
|
program.outputHelp();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { getCIStatus } from "../lib/github.js";
|
|
3
|
+
import { error, info } from "../lib/logger.js";
|
|
4
|
+
import { createTable, withSpinner } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("ci")
|
|
9
|
+
.description("Check CI status")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
try {
|
|
12
|
+
const checks = await withSpinner("Fetching CI status...", () =>
|
|
13
|
+
getCIStatus(),
|
|
14
|
+
);
|
|
15
|
+
if (checks.length === 0) {
|
|
16
|
+
info("No CI checks found.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const rows = checks.map((c) => [c.name, c.conclusion, c.branch]);
|
|
20
|
+
info("CI Checks:");
|
|
21
|
+
console.log(createTable(["Name", "Status", "Branch"], rows));
|
|
22
|
+
} catch (err) {
|
|
23
|
+
error(
|
|
24
|
+
`CI check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
package/src/commands/commit.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import simpleGit from "simple-git";
|
|
3
2
|
import { commitTypes } from "../constants/messages.js";
|
|
4
3
|
import * as git from "../lib/git.js";
|
|
5
4
|
import { error, info, newline, success, text, warning } from "../lib/logger.js";
|
|
@@ -14,8 +13,7 @@ export default function register(program: Command): void {
|
|
|
14
13
|
.action(async (options) => {
|
|
15
14
|
try {
|
|
16
15
|
if (options.amend) {
|
|
17
|
-
|
|
18
|
-
await sg.commit("", { "--amend": null, "--no-edit": null });
|
|
16
|
+
await git.amendCommit();
|
|
19
17
|
success("Last commit amended.");
|
|
20
18
|
return;
|
|
21
19
|
}
|
|
@@ -51,10 +49,7 @@ export default function register(program: Command): void {
|
|
|
51
49
|
return;
|
|
52
50
|
}
|
|
53
51
|
|
|
54
|
-
|
|
55
|
-
await withSpinner("Staging files...", async () => {
|
|
56
|
-
await sg.add(selectedFiles);
|
|
57
|
-
});
|
|
52
|
+
await withSpinner("Staging files...", () => git.add(selectedFiles));
|
|
58
53
|
|
|
59
54
|
newline();
|
|
60
55
|
const typeChoices = commitTypes.map((t) => ({
|
package/src/commands/init.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { resolve } from "node:path";
|
|
2
1
|
import type { Command } from "commander";
|
|
3
|
-
import simpleGit from "simple-git";
|
|
4
2
|
import * as git from "../lib/git.js";
|
|
5
3
|
import { error, success } from "../lib/logger.js";
|
|
6
4
|
import { showBox } from "../lib/ui.js";
|
|
@@ -19,15 +17,13 @@ export default function register(program: Command): void {
|
|
|
19
17
|
await git.init(dir, { initialBranch: options.branch });
|
|
20
18
|
|
|
21
19
|
if (options.initialCommit) {
|
|
22
|
-
|
|
23
|
-
await sg.raw(["commit", "--allow-empty", "-m", "Initial commit"]);
|
|
20
|
+
await git.commit("Initial commit");
|
|
24
21
|
}
|
|
25
22
|
|
|
26
|
-
const
|
|
27
|
-
const content = [`Path: ${absPath}`].join("\n");
|
|
23
|
+
const content = [`Path: ${targetDir}`].join("\n");
|
|
28
24
|
showBox("Repository Initialized", content);
|
|
29
25
|
|
|
30
|
-
success(`Git repository initialized at ${
|
|
26
|
+
success(`Git repository initialized at ${targetDir}.`);
|
|
31
27
|
} catch (err) {
|
|
32
28
|
error(
|
|
33
29
|
`Init failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createIssue, listIssues } from "../lib/github.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
|
+
const issue = program.command("issue").description("Manage issues");
|
|
8
|
+
|
|
9
|
+
issue
|
|
10
|
+
.command("create")
|
|
11
|
+
.description("Create an issue")
|
|
12
|
+
.option("-t, --title <title>", "Issue title")
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
try {
|
|
15
|
+
const title = options.title || (await input("Issue title"));
|
|
16
|
+
if (!title) {
|
|
17
|
+
error("Title is required.");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const body = await input("Description (optional)");
|
|
21
|
+
const result = await withSpinner("Creating issue...", () =>
|
|
22
|
+
createIssue(title, body),
|
|
23
|
+
);
|
|
24
|
+
success(`Issue #${result.number} created: ${result.url}`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
error(
|
|
27
|
+
`Issue creation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
issue
|
|
33
|
+
.command("list")
|
|
34
|
+
.description("List issues")
|
|
35
|
+
.option(
|
|
36
|
+
"-s, --state <state>",
|
|
37
|
+
"Filter by state (open, closed, all)",
|
|
38
|
+
"open",
|
|
39
|
+
)
|
|
40
|
+
.action(async (options) => {
|
|
41
|
+
try {
|
|
42
|
+
const issues = await withSpinner("Fetching issues...", () =>
|
|
43
|
+
listIssues(options.state),
|
|
44
|
+
);
|
|
45
|
+
if (issues.length === 0) {
|
|
46
|
+
info("No issues found.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const rows = issues.map((i) => [
|
|
50
|
+
`#${i.number}`,
|
|
51
|
+
i.title.substring(0, 60),
|
|
52
|
+
i.state,
|
|
53
|
+
i.author,
|
|
54
|
+
]);
|
|
55
|
+
info("Issues:");
|
|
56
|
+
console.log(createTable(["#", "Title", "State", "Author"], rows));
|
|
57
|
+
} catch (err) {
|
|
58
|
+
error(
|
|
59
|
+
`Failed to list issues: ${err instanceof Error ? err.message : String(err)}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createPR, listPRs } from "../lib/github.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
|
+
const pr = program.command("pr").description("Manage pull requests");
|
|
8
|
+
|
|
9
|
+
pr.command("create")
|
|
10
|
+
.description("Create a pull request")
|
|
11
|
+
.option("-t, --title <title>", "PR title")
|
|
12
|
+
.option("-H, --head <branch>", "Source branch (default: current branch)")
|
|
13
|
+
.option("-B, --base <branch>", "Target branch (default: main)")
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
try {
|
|
16
|
+
const title = options.title || (await input("PR title"));
|
|
17
|
+
if (!title) {
|
|
18
|
+
error("Title is required.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const head = options.head || (await input("Source branch"));
|
|
22
|
+
const base = options.base || (await input("Target branch", "main"));
|
|
23
|
+
const body = await input("Description (optional)");
|
|
24
|
+
const result = await withSpinner("Creating PR...", () =>
|
|
25
|
+
createPR(title, body, head, base),
|
|
26
|
+
);
|
|
27
|
+
success(`PR #${result.number} created: ${result.url}`);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
error(
|
|
30
|
+
`PR creation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
pr.command("list")
|
|
36
|
+
.description("List pull requests")
|
|
37
|
+
.option(
|
|
38
|
+
"-s, --state <state>",
|
|
39
|
+
"Filter by state (open, closed, all)",
|
|
40
|
+
"open",
|
|
41
|
+
)
|
|
42
|
+
.action(async (options) => {
|
|
43
|
+
try {
|
|
44
|
+
const prs = await withSpinner("Fetching PRs...", () =>
|
|
45
|
+
listPRs(options.state),
|
|
46
|
+
);
|
|
47
|
+
if (prs.length === 0) {
|
|
48
|
+
info("No pull requests found.");
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const rows = prs.map((p) => [
|
|
52
|
+
`#${p.number}`,
|
|
53
|
+
p.title.substring(0, 60),
|
|
54
|
+
p.state,
|
|
55
|
+
p.author,
|
|
56
|
+
]);
|
|
57
|
+
info("Pull Requests:");
|
|
58
|
+
console.log(createTable(["#", "Title", "State", "Author"], rows));
|
|
59
|
+
} catch (err) {
|
|
60
|
+
error(
|
|
61
|
+
`Failed to list PRs: ${err instanceof Error ? err.message : String(err)}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import { createRelease } from "../lib/github.js";
|
|
3
|
+
import { error, success } from "../lib/logger.js";
|
|
4
|
+
import { input, withSpinner } from "../lib/ui.js";
|
|
5
|
+
|
|
6
|
+
export default function register(program: Command): void {
|
|
7
|
+
program
|
|
8
|
+
.command("release")
|
|
9
|
+
.description("Create a GitHub release")
|
|
10
|
+
.argument("<tag>", "Git tag name")
|
|
11
|
+
.option("-n, --name <name>", "Release name")
|
|
12
|
+
.action(async (tag, options) => {
|
|
13
|
+
try {
|
|
14
|
+
const name = options.name || (await input("Release name", tag));
|
|
15
|
+
const body = await input("Release description (optional)");
|
|
16
|
+
const result = await withSpinner("Creating release...", () =>
|
|
17
|
+
createRelease(tag, name, body),
|
|
18
|
+
);
|
|
19
|
+
success(`Release created: ${result.url}`);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
error(
|
|
22
|
+
`Release creation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
import type { Command } from "commander";
|
|
2
|
-
import { encryptToken } from "../lib/auth.js";
|
|
2
|
+
import { encryptToken, generateMachineKey } from "../lib/auth.js";
|
|
3
3
|
import { ConfigManager } from "../lib/config.js";
|
|
4
4
|
import { error, info, newline, success, warning } from "../lib/logger.js";
|
|
5
5
|
import { confirm, input, password, showBox } from "../lib/ui.js";
|
|
6
|
-
import {
|
|
7
|
-
validateEmail,
|
|
8
|
-
validateGitHubToken,
|
|
9
|
-
validateGitInstalled,
|
|
10
|
-
} from "../lib/validators.js";
|
|
6
|
+
import { validateEmail } from "../lib/validators.js";
|
|
11
7
|
|
|
12
8
|
export default function register(program: Command): void {
|
|
13
9
|
program
|
|
@@ -26,12 +22,6 @@ export default function register(program: Command): void {
|
|
|
26
22
|
}
|
|
27
23
|
}
|
|
28
24
|
|
|
29
|
-
const gitInstalled = await validateGitInstalled();
|
|
30
|
-
if (!gitInstalled) {
|
|
31
|
-
error("Git is not installed. Please install Git first.");
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
25
|
newline();
|
|
36
26
|
info("Starting setup...");
|
|
37
27
|
newline();
|
|
@@ -44,25 +34,39 @@ export default function register(program: Command): void {
|
|
|
44
34
|
email = await input("Your email");
|
|
45
35
|
}
|
|
46
36
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
const rawToken = await input(
|
|
38
|
+
"GitHub token (required for remote operations)",
|
|
39
|
+
);
|
|
40
|
+
if (!rawToken) {
|
|
41
|
+
error(
|
|
42
|
+
"GitHub token is required. Get one at https://github.com/settings/tokens",
|
|
43
|
+
);
|
|
44
|
+
return;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
47
|
+
const useMasterPassword = await confirm(
|
|
48
|
+
"Protect token with a master password? (recommended)",
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const machineKey = generateMachineKey();
|
|
53
|
+
let hasMasterPassword = false;
|
|
54
|
+
|
|
55
|
+
if (useMasterPassword) {
|
|
56
|
+
const mp = await password("Master password:");
|
|
57
|
+
const encryptedKey = await encryptToken(machineKey, mp);
|
|
58
|
+
config.set("auth.machineKey", encryptedKey);
|
|
59
|
+
config.set("auth.hasMasterPassword", true);
|
|
60
|
+
hasMasterPassword = true;
|
|
61
|
+
} else {
|
|
62
|
+
config.set("auth.machineKey", machineKey);
|
|
63
|
+
config.set("auth.hasMasterPassword", false);
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
const encryptedToken = await encryptToken(rawToken, machineKey);
|
|
67
|
+
config.set("github.encryptedToken", encryptedToken);
|
|
68
|
+
|
|
64
69
|
config.set("user", { name, email });
|
|
65
|
-
config.set("github", { token });
|
|
66
70
|
config.set("preferences", {
|
|
67
71
|
autoPush: false,
|
|
68
72
|
commitTemplate: "",
|
|
@@ -70,17 +74,12 @@ export default function register(program: Command): void {
|
|
|
70
74
|
});
|
|
71
75
|
|
|
72
76
|
newline();
|
|
73
|
-
const tokenStatus = token
|
|
74
|
-
? token.includes(":")
|
|
75
|
-
? "Set (encrypted)"
|
|
76
|
-
: "Set"
|
|
77
|
-
: "Not set";
|
|
78
77
|
showBox(
|
|
79
78
|
"Configuration Complete",
|
|
80
79
|
[
|
|
81
80
|
`User: ${name} <${email}>`,
|
|
82
|
-
`Token:
|
|
83
|
-
`
|
|
81
|
+
`Token: Set (encrypted)`,
|
|
82
|
+
`Security: ${hasMasterPassword ? "Master password protected" : "Machine key"}`,
|
|
84
83
|
`Config: ${config.getPath()}`,
|
|
85
84
|
].join("\n"),
|
|
86
85
|
);
|
package/src/lib/auth.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { ConfigManager } from "./config.js";
|
|
3
|
+
|
|
1
4
|
const ITERATIONS = 100000;
|
|
2
5
|
const KEY_LENGTH = 256;
|
|
3
6
|
const SALT_LENGTH = 32;
|
|
4
7
|
const IV_LENGTH = 12;
|
|
5
8
|
|
|
9
|
+
let cachedToken: string | null = null;
|
|
10
|
+
|
|
6
11
|
function base64Encode(buf: Uint8Array): string {
|
|
7
12
|
return btoa(String.fromCharCode(...buf));
|
|
8
13
|
}
|
|
@@ -19,6 +24,10 @@ export function generateIV(): Uint8Array {
|
|
|
19
24
|
return crypto.getRandomValues(new Uint8Array(IV_LENGTH));
|
|
20
25
|
}
|
|
21
26
|
|
|
27
|
+
export function generateMachineKey(): string {
|
|
28
|
+
return base64Encode(crypto.getRandomValues(new Uint8Array(32)));
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
async function deriveKey(
|
|
23
32
|
password: string,
|
|
24
33
|
salt: Uint8Array,
|
|
@@ -31,14 +40,8 @@ async function deriveKey(
|
|
|
31
40
|
false,
|
|
32
41
|
["deriveKey"],
|
|
33
42
|
);
|
|
34
|
-
|
|
35
43
|
return crypto.subtle.deriveKey(
|
|
36
|
-
{
|
|
37
|
-
name: "PBKDF2",
|
|
38
|
-
salt,
|
|
39
|
-
iterations: ITERATIONS,
|
|
40
|
-
hash: "SHA-256",
|
|
41
|
-
},
|
|
44
|
+
{ name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
|
|
42
45
|
keyMaterial,
|
|
43
46
|
{ name: "AES-GCM", length: KEY_LENGTH },
|
|
44
47
|
false,
|
|
@@ -48,48 +51,122 @@ async function deriveKey(
|
|
|
48
51
|
|
|
49
52
|
export async function encryptToken(
|
|
50
53
|
token: string,
|
|
51
|
-
|
|
54
|
+
key: string,
|
|
52
55
|
): Promise<string> {
|
|
53
56
|
const salt = generateSalt();
|
|
54
57
|
const iv = generateIV();
|
|
55
|
-
const
|
|
58
|
+
const derivedKey = await deriveKey(key, salt);
|
|
56
59
|
const encoder = new TextEncoder();
|
|
57
60
|
const encrypted = await crypto.subtle.encrypt(
|
|
58
61
|
{ name: "AES-GCM", iv },
|
|
59
|
-
|
|
62
|
+
derivedKey,
|
|
60
63
|
encoder.encode(token),
|
|
61
64
|
);
|
|
62
|
-
|
|
63
65
|
const parts = [
|
|
64
66
|
base64Encode(salt),
|
|
65
67
|
base64Encode(iv),
|
|
66
68
|
base64Encode(new Uint8Array(encrypted)),
|
|
67
69
|
];
|
|
68
|
-
|
|
69
70
|
return parts.join(":");
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
export async function decryptToken(
|
|
73
74
|
encryptedData: string,
|
|
74
|
-
|
|
75
|
+
key: string,
|
|
75
76
|
): Promise<string> {
|
|
76
77
|
const parts = encryptedData.split(":");
|
|
77
78
|
if (parts.length !== 3) {
|
|
78
79
|
throw new Error("Invalid encrypted data format");
|
|
79
80
|
}
|
|
80
|
-
|
|
81
81
|
const [saltB64, ivB64, ciphertextB64] = parts;
|
|
82
82
|
const salt = base64Decode(saltB64);
|
|
83
83
|
const iv = base64Decode(ivB64);
|
|
84
84
|
const ciphertext = base64Decode(ciphertextB64);
|
|
85
|
-
const
|
|
86
|
-
|
|
85
|
+
const derivedKey = await deriveKey(key, salt);
|
|
87
86
|
const decrypted = await crypto.subtle.decrypt(
|
|
88
87
|
{ name: "AES-GCM", iv },
|
|
89
|
-
|
|
88
|
+
derivedKey,
|
|
90
89
|
ciphertext,
|
|
91
90
|
);
|
|
92
|
-
|
|
93
91
|
const decoder = new TextDecoder();
|
|
94
92
|
return decoder.decode(decrypted);
|
|
95
93
|
}
|
|
94
|
+
|
|
95
|
+
export function cacheToken(token: string): void {
|
|
96
|
+
cachedToken = token;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getCachedToken(): string | null {
|
|
100
|
+
return cachedToken;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function clearTokenCache(): void {
|
|
104
|
+
cachedToken = null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function promptPassword(): Promise<string> {
|
|
108
|
+
const rl = createInterface({
|
|
109
|
+
input: process.stdin,
|
|
110
|
+
output: process.stdout,
|
|
111
|
+
});
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
rl.question("Master password: ", (answer: string) => {
|
|
114
|
+
rl.close();
|
|
115
|
+
resolve(answer);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function resolveToken(): Promise<string> {
|
|
121
|
+
if (cachedToken) return cachedToken;
|
|
122
|
+
|
|
123
|
+
const config = new ConfigManager();
|
|
124
|
+
const encryptedToken = config.get("github.encryptedToken") as string;
|
|
125
|
+
if (!encryptedToken) {
|
|
126
|
+
throw new Error("No GitHub token configured. Run 'fg setup' first.");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hasMasterPassword = config.get("auth.hasMasterPassword") as boolean;
|
|
130
|
+
|
|
131
|
+
let machineKey: string;
|
|
132
|
+
if (hasMasterPassword) {
|
|
133
|
+
const encryptedKey = config.get("auth.machineKey") as string;
|
|
134
|
+
const password = await promptPassword();
|
|
135
|
+
machineKey = await decryptToken(encryptedKey, password);
|
|
136
|
+
} else {
|
|
137
|
+
machineKey = config.get("auth.machineKey") as string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const token = await decryptToken(encryptedToken, machineKey);
|
|
141
|
+
cachedToken = token;
|
|
142
|
+
return token;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function resolveTokenWithPassword(
|
|
146
|
+
password?: string,
|
|
147
|
+
): Promise<string> {
|
|
148
|
+
if (cachedToken) return cachedToken;
|
|
149
|
+
|
|
150
|
+
const config = new ConfigManager();
|
|
151
|
+
const encryptedToken = config.get("github.encryptedToken") as string;
|
|
152
|
+
if (!encryptedToken) {
|
|
153
|
+
throw new Error("No GitHub token configured. Run 'fg setup' first.");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const hasMasterPassword = config.get("auth.hasMasterPassword") as boolean;
|
|
157
|
+
|
|
158
|
+
let machineKey: string;
|
|
159
|
+
if (hasMasterPassword) {
|
|
160
|
+
const encryptedKey = config.get("auth.machineKey") as string;
|
|
161
|
+
if (!password) {
|
|
162
|
+
throw new Error("Master password required to decrypt token.");
|
|
163
|
+
}
|
|
164
|
+
machineKey = await decryptToken(encryptedKey, password);
|
|
165
|
+
} else {
|
|
166
|
+
machineKey = config.get("auth.machineKey") as string;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const token = await decryptToken(encryptedToken, machineKey);
|
|
170
|
+
cachedToken = token;
|
|
171
|
+
return token;
|
|
172
|
+
}
|
package/src/lib/config.ts
CHANGED
|
@@ -8,7 +8,11 @@ export interface ForgeConfig {
|
|
|
8
8
|
email: string;
|
|
9
9
|
};
|
|
10
10
|
github: {
|
|
11
|
-
|
|
11
|
+
encryptedToken: string;
|
|
12
|
+
};
|
|
13
|
+
auth: {
|
|
14
|
+
machineKey: string;
|
|
15
|
+
hasMasterPassword: boolean;
|
|
12
16
|
};
|
|
13
17
|
preferences: {
|
|
14
18
|
autoPush: boolean;
|
|
@@ -20,7 +24,8 @@ export interface ForgeConfig {
|
|
|
20
24
|
|
|
21
25
|
const DEFAULTS: ForgeConfig = {
|
|
22
26
|
user: { name: "", email: "" },
|
|
23
|
-
github: {
|
|
27
|
+
github: { encryptedToken: "" },
|
|
28
|
+
auth: { machineKey: "", hasMasterPassword: false },
|
|
24
29
|
preferences: {
|
|
25
30
|
autoPush: false,
|
|
26
31
|
commitTemplate: "",
|
|
@@ -69,7 +74,11 @@ export class ConfigManager {
|
|
|
69
74
|
email: this.conf.get("user.email") as string,
|
|
70
75
|
},
|
|
71
76
|
github: {
|
|
72
|
-
|
|
77
|
+
encryptedToken: this.conf.get("github.encryptedToken") as string,
|
|
78
|
+
},
|
|
79
|
+
auth: {
|
|
80
|
+
machineKey: this.conf.get("auth.machineKey") as string,
|
|
81
|
+
hasMasterPassword: this.conf.get("auth.hasMasterPassword") as boolean,
|
|
73
82
|
},
|
|
74
83
|
preferences: {
|
|
75
84
|
autoPush: this.conf.get("preferences.autoPush") as boolean,
|