@hallaxius/forge 0.1.3 → 0.1.4

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 (45) hide show
  1. package/README.md +160 -158
  2. package/bin/forge.js +2 -2
  3. package/dist/cli.js +12764 -13800
  4. package/package.json +75 -75
  5. package/src/cli.ts +80 -78
  6. package/src/commands/account.ts +80 -0
  7. package/src/commands/alias.ts +66 -66
  8. package/src/commands/branch.ts +46 -46
  9. package/src/commands/ci.ts +28 -28
  10. package/src/commands/clone.ts +100 -100
  11. package/src/commands/commit.ts +88 -88
  12. package/src/commands/config.ts +47 -48
  13. package/src/commands/diff.ts +26 -26
  14. package/src/commands/fetch.ts +20 -20
  15. package/src/commands/help.ts +58 -58
  16. package/src/commands/init.ts +32 -33
  17. package/src/commands/issue.ts +63 -63
  18. package/src/commands/log.ts +29 -29
  19. package/src/commands/merge.ts +37 -37
  20. package/src/commands/pr.ts +65 -65
  21. package/src/commands/push.ts +35 -35
  22. package/src/commands/release.ts +26 -26
  23. package/src/commands/remote.ts +107 -107
  24. package/src/commands/reset.ts +30 -30
  25. package/src/commands/setup.ts +93 -94
  26. package/src/commands/stash.ts +44 -44
  27. package/src/commands/status.ts +74 -74
  28. package/src/commands/sync.ts +20 -20
  29. package/src/commands/tag.ts +41 -41
  30. package/src/commands/undo.ts +27 -27
  31. package/src/commands/version.ts +12 -12
  32. package/src/constants/colors.ts +7 -7
  33. package/src/constants/commit-types.ts +24 -24
  34. package/src/constants/messages.ts +13 -23
  35. package/src/lib/auth.ts +172 -172
  36. package/src/lib/config.ts +108 -108
  37. package/src/lib/git.ts +543 -543
  38. package/src/lib/github.ts +202 -160
  39. package/src/lib/logger.ts +18 -31
  40. package/src/lib/ui.ts +122 -156
  41. package/src/lib/validators.ts +16 -16
  42. package/src/templates/commit-types.json +9 -9
  43. package/src/utils/files.ts +21 -21
  44. package/src/utils/strings.ts +19 -19
  45. package/src/version.const.ts +1 -1
package/src/lib/auth.ts CHANGED
@@ -1,172 +1,172 @@
1
- import { createInterface } from "node:readline";
2
- import { ConfigManager } from "./config.js";
3
-
4
- const ITERATIONS = 100000;
5
- const KEY_LENGTH = 256;
6
- const SALT_LENGTH = 32;
7
- const IV_LENGTH = 12;
8
-
9
- let cachedToken: string | null = null;
10
-
11
- function base64Encode(buf: Uint8Array): string {
12
- return btoa(String.fromCharCode(...buf));
13
- }
14
-
15
- function base64Decode(str: string): Uint8Array {
16
- return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
17
- }
18
-
19
- export function generateSalt(length: number = SALT_LENGTH): Uint8Array {
20
- return crypto.getRandomValues(new Uint8Array(length));
21
- }
22
-
23
- export function generateIV(): Uint8Array {
24
- return crypto.getRandomValues(new Uint8Array(IV_LENGTH));
25
- }
26
-
27
- export function generateMachineKey(): string {
28
- return base64Encode(crypto.getRandomValues(new Uint8Array(32)));
29
- }
30
-
31
- async function deriveKey(
32
- password: string,
33
- salt: Uint8Array,
34
- ): Promise<CryptoKey> {
35
- const encoder = new TextEncoder();
36
- const keyMaterial = await crypto.subtle.importKey(
37
- "raw",
38
- encoder.encode(password),
39
- "PBKDF2",
40
- false,
41
- ["deriveKey"],
42
- );
43
- return crypto.subtle.deriveKey(
44
- { name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
45
- keyMaterial,
46
- { name: "AES-GCM", length: KEY_LENGTH },
47
- false,
48
- ["encrypt", "decrypt"],
49
- );
50
- }
51
-
52
- export async function encryptToken(
53
- token: string,
54
- key: string,
55
- ): Promise<string> {
56
- const salt = generateSalt();
57
- const iv = generateIV();
58
- const derivedKey = await deriveKey(key, salt);
59
- const encoder = new TextEncoder();
60
- const encrypted = await crypto.subtle.encrypt(
61
- { name: "AES-GCM", iv },
62
- derivedKey,
63
- encoder.encode(token),
64
- );
65
- const parts = [
66
- base64Encode(salt),
67
- base64Encode(iv),
68
- base64Encode(new Uint8Array(encrypted)),
69
- ];
70
- return parts.join(":");
71
- }
72
-
73
- export async function decryptToken(
74
- encryptedData: string,
75
- key: string,
76
- ): Promise<string> {
77
- const parts = encryptedData.split(":");
78
- if (parts.length !== 3) {
79
- throw new Error("Invalid encrypted data format");
80
- }
81
- const [saltB64, ivB64, ciphertextB64] = parts;
82
- const salt = base64Decode(saltB64);
83
- const iv = base64Decode(ivB64);
84
- const ciphertext = base64Decode(ciphertextB64);
85
- const derivedKey = await deriveKey(key, salt);
86
- const decrypted = await crypto.subtle.decrypt(
87
- { name: "AES-GCM", iv },
88
- derivedKey,
89
- ciphertext,
90
- );
91
- const decoder = new TextDecoder();
92
- return decoder.decode(decrypted);
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
- }
1
+ import { createInterface } from "node:readline";
2
+ import { ConfigManager } from "./config.js";
3
+
4
+ const ITERATIONS = 100000;
5
+ const KEY_LENGTH = 256;
6
+ const SALT_LENGTH = 32;
7
+ const IV_LENGTH = 12;
8
+
9
+ let cachedToken: string | null = null;
10
+
11
+ function base64Encode(buf: Uint8Array): string {
12
+ return btoa(String.fromCharCode(...buf));
13
+ }
14
+
15
+ function base64Decode(str: string): Uint8Array {
16
+ return Uint8Array.from(atob(str), (c) => c.charCodeAt(0));
17
+ }
18
+
19
+ export function generateSalt(length: number = SALT_LENGTH): Uint8Array {
20
+ return crypto.getRandomValues(new Uint8Array(length));
21
+ }
22
+
23
+ export function generateIV(): Uint8Array {
24
+ return crypto.getRandomValues(new Uint8Array(IV_LENGTH));
25
+ }
26
+
27
+ export function generateMachineKey(): string {
28
+ return base64Encode(crypto.getRandomValues(new Uint8Array(32)));
29
+ }
30
+
31
+ async function deriveKey(
32
+ password: string,
33
+ salt: Uint8Array,
34
+ ): Promise<CryptoKey> {
35
+ const encoder = new TextEncoder();
36
+ const keyMaterial = await crypto.subtle.importKey(
37
+ "raw",
38
+ encoder.encode(password),
39
+ "PBKDF2",
40
+ false,
41
+ ["deriveKey"],
42
+ );
43
+ return crypto.subtle.deriveKey(
44
+ { name: "PBKDF2", salt, iterations: ITERATIONS, hash: "SHA-256" },
45
+ keyMaterial,
46
+ { name: "AES-GCM", length: KEY_LENGTH },
47
+ false,
48
+ ["encrypt", "decrypt"],
49
+ );
50
+ }
51
+
52
+ export async function encryptToken(
53
+ token: string,
54
+ key: string,
55
+ ): Promise<string> {
56
+ const salt = generateSalt();
57
+ const iv = generateIV();
58
+ const derivedKey = await deriveKey(key, salt);
59
+ const encoder = new TextEncoder();
60
+ const encrypted = await crypto.subtle.encrypt(
61
+ { name: "AES-GCM", iv },
62
+ derivedKey,
63
+ encoder.encode(token),
64
+ );
65
+ const parts = [
66
+ base64Encode(salt),
67
+ base64Encode(iv),
68
+ base64Encode(new Uint8Array(encrypted)),
69
+ ];
70
+ return parts.join(":");
71
+ }
72
+
73
+ export async function decryptToken(
74
+ encryptedData: string,
75
+ key: string,
76
+ ): Promise<string> {
77
+ const parts = encryptedData.split(":");
78
+ if (parts.length !== 3) {
79
+ throw new Error("Invalid encrypted data format");
80
+ }
81
+ const [saltB64, ivB64, ciphertextB64] = parts;
82
+ const salt = base64Decode(saltB64);
83
+ const iv = base64Decode(ivB64);
84
+ const ciphertext = base64Decode(ciphertextB64);
85
+ const derivedKey = await deriveKey(key, salt);
86
+ const decrypted = await crypto.subtle.decrypt(
87
+ { name: "AES-GCM", iv },
88
+ derivedKey,
89
+ ciphertext,
90
+ );
91
+ const decoder = new TextDecoder();
92
+ return decoder.decode(decrypted);
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
@@ -1,108 +1,108 @@
1
- import { homedir } from "node:os";
2
- import { join } from "node:path";
3
- import Conf from "conf";
4
-
5
- export interface ForgeConfig {
6
- user: {
7
- name: string;
8
- email: string;
9
- };
10
- github: {
11
- encryptedToken: string;
12
- };
13
- auth: {
14
- machineKey: string;
15
- hasMasterPassword: boolean;
16
- };
17
- preferences: {
18
- autoPush: boolean;
19
- commitTemplate: string;
20
- editor: string;
21
- };
22
- clones: string[];
23
- }
24
-
25
- const DEFAULTS: ForgeConfig = {
26
- user: { name: "", email: "" },
27
- github: { encryptedToken: "" },
28
- auth: { machineKey: "", hasMasterPassword: false },
29
- preferences: {
30
- autoPush: false,
31
- commitTemplate: "",
32
- editor: process.env.EDITOR || "vim",
33
- },
34
- clones: [],
35
- };
36
-
37
- const configPath = join(homedir(), ".forge");
38
-
39
- export class ConfigManager {
40
- private conf: Conf<ForgeConfig>;
41
-
42
- constructor(options?: { configName?: string; cwd?: string }) {
43
- this.conf = new Conf<ForgeConfig>({
44
- configName: options?.configName || "forge",
45
- cwd: options?.cwd || configPath,
46
- defaults: DEFAULTS,
47
- });
48
- }
49
-
50
- get(key: string): any {
51
- return this.conf.get(key);
52
- }
53
-
54
- set(key: string, value: any): void {
55
- this.conf.set(key, value);
56
- }
57
-
58
- delete(key: string): void {
59
- this.conf.delete(key);
60
- }
61
-
62
- clear(): void {
63
- this.conf.clear();
64
- }
65
-
66
- getPath(): string {
67
- return this.conf.path;
68
- }
69
-
70
- getAll(): ForgeConfig & { clones: string[] } {
71
- return {
72
- user: {
73
- name: this.conf.get("user.name") as string,
74
- email: this.conf.get("user.email") as string,
75
- },
76
- github: {
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,
82
- },
83
- preferences: {
84
- autoPush: this.conf.get("preferences.autoPush") as boolean,
85
- commitTemplate: this.conf.get("preferences.commitTemplate") as string,
86
- editor: this.conf.get("preferences.editor") as string,
87
- },
88
- clones: this.getClones(),
89
- };
90
- }
91
-
92
- addClone(entry: string): void {
93
- const clones = (this.conf.get("clones") as string[]) || [];
94
- clones.unshift(entry);
95
- if (clones.length > 10) clones.length = 10;
96
- this.conf.set("clones", clones);
97
- }
98
-
99
- getClones(): string[] {
100
- return (this.conf.get("clones") as string[]) || [];
101
- }
102
-
103
- isConfigured(): boolean {
104
- const name = this.conf.get("user.name") as string;
105
- const email = this.conf.get("user.email") as string;
106
- return !!(name && email);
107
- }
108
- }
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import Conf from "conf";
4
+
5
+ export interface ForgeConfig {
6
+ user: {
7
+ name: string;
8
+ email: string;
9
+ };
10
+ github: {
11
+ encryptedToken: string;
12
+ };
13
+ auth: {
14
+ machineKey: string;
15
+ hasMasterPassword: boolean;
16
+ };
17
+ preferences: {
18
+ autoPush: boolean;
19
+ commitTemplate: string;
20
+ editor: string;
21
+ };
22
+ clones: string[];
23
+ }
24
+
25
+ const DEFAULTS: ForgeConfig = {
26
+ user: { name: "", email: "" },
27
+ github: { encryptedToken: "" },
28
+ auth: { machineKey: "", hasMasterPassword: false },
29
+ preferences: {
30
+ autoPush: false,
31
+ commitTemplate: "",
32
+ editor: process.env.EDITOR || "vim",
33
+ },
34
+ clones: [],
35
+ };
36
+
37
+ const configPath = join(homedir(), ".forge");
38
+
39
+ export class ConfigManager {
40
+ private conf: Conf<ForgeConfig>;
41
+
42
+ constructor(options?: { configName?: string; cwd?: string }) {
43
+ this.conf = new Conf<ForgeConfig>({
44
+ configName: options?.configName || "forge",
45
+ cwd: options?.cwd || configPath,
46
+ defaults: DEFAULTS,
47
+ });
48
+ }
49
+
50
+ get(key: string): any {
51
+ return this.conf.get(key);
52
+ }
53
+
54
+ set(key: string, value: any): void {
55
+ this.conf.set(key, value);
56
+ }
57
+
58
+ delete(key: string): void {
59
+ this.conf.delete(key);
60
+ }
61
+
62
+ clear(): void {
63
+ this.conf.clear();
64
+ }
65
+
66
+ getPath(): string {
67
+ return this.conf.path;
68
+ }
69
+
70
+ getAll(): ForgeConfig & { clones: string[] } {
71
+ return {
72
+ user: {
73
+ name: this.conf.get("user.name") as string,
74
+ email: this.conf.get("user.email") as string,
75
+ },
76
+ github: {
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,
82
+ },
83
+ preferences: {
84
+ autoPush: this.conf.get("preferences.autoPush") as boolean,
85
+ commitTemplate: this.conf.get("preferences.commitTemplate") as string,
86
+ editor: this.conf.get("preferences.editor") as string,
87
+ },
88
+ clones: this.getClones(),
89
+ };
90
+ }
91
+
92
+ addClone(entry: string): void {
93
+ const clones = (this.conf.get("clones") as string[]) || [];
94
+ clones.unshift(entry);
95
+ if (clones.length > 10) clones.length = 10;
96
+ this.conf.set("clones", clones);
97
+ }
98
+
99
+ getClones(): string[] {
100
+ return (this.conf.get("clones") as string[]) || [];
101
+ }
102
+
103
+ isConfigured(): boolean {
104
+ const name = this.conf.get("user.name") as string;
105
+ const email = this.conf.get("user.email") as string;
106
+ return !!(name && email);
107
+ }
108
+ }