@ridit/forge 0.2.6

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/index.ts ADDED
@@ -0,0 +1 @@
1
+ console.log("Hello via Bun!");
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@ridit/forge",
3
+ "version": "0.2.6",
4
+ "description": "Git, but yours.",
5
+ "author": "Ridit Jangra <riditjangra09@gmail.com> (https://ridit.space)",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/ridit-jangra/Forge"
10
+ },
11
+ "main": "dist/index.mjs",
12
+ "bin": {
13
+ "forge": "./dist/index.mjs"
14
+ },
15
+ "scripts": {
16
+ "build": "bun build src/index.tsx --target node --outfile dist/index.mjs",
17
+ "postbuild": "node -e \"const fs=require('fs');const f='dist/index.mjs';fs.writeFileSync(f,'#!/usr/bin/env node\\n'+fs.readFileSync(f,'utf8'))\"",
18
+ "tag": "node -e \"const v=require('./package.json').version;require('child_process').execSync('git tag v'+v+' && git push origin v'+v,{stdio:'inherit'})\"",
19
+ "prepublishOnly": "npm run build && npm run tag"
20
+ },
21
+ "dependencies": {
22
+ "commander": "^14.0.3",
23
+ "figures": "^6.1.0",
24
+ "ignore": "^7.0.5",
25
+ "ink": "^6.8.0",
26
+ "ink-spinner": "^5.0.0",
27
+ "react": "^19.2.4",
28
+ "react-devtools-core": "^7.0.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/bun": "latest",
32
+ "@types/react": "^19.2.14"
33
+ },
34
+ "peerDependencies": {
35
+ "typescript": "^5"
36
+ },
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }
package/src/colors.ts ADDED
@@ -0,0 +1,10 @@
1
+ export const ACCENT = "#DA7758";
2
+ export const HEADING = "#FFFFFF";
3
+ export const TEXT = "#E8E8E8";
4
+ export const RED = "#E06C75";
5
+ export const GREEN = "#85C98A";
6
+ export const CYAN = "#79C5D4";
7
+ export const YELLOW = "#E5C07B"; // modified
8
+ export const BLUE = "#61AFEF"; // staged
9
+ export const GRAY = "#636D83"; // deleted
10
+ export const PURPLE = "#C678DD"; // untracked
@@ -0,0 +1,152 @@
1
+ import { Box, Text } from "ink";
2
+ import { useEffect, useState } from "react";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import { matchPaths } from "../utils/forgeIgnore";
6
+ import { ACCENT, GREEN, RED, TEXT } from "../colors";
7
+ import Spinner from "ink-spinner";
8
+ import { addFile } from "../utils/add";
9
+
10
+ export function AddCommand({ fileOrFolderPath }: { fileOrFolderPath: string }) {
11
+ const [stage, setStage] = useState<"working" | "done">("working");
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [indexAddedCount, setIndexAddedCount] = useState<number>(0);
14
+ const [fileCount, setFileCount] = useState<number>(0);
15
+ const [filesToCheck, setFilesToCheck] = useState<string[]>();
16
+ const [filesChecked, setFilesChecked] = useState<string[]>([]);
17
+
18
+ function processFiles(basePath: string, files: (string | Buffer)[]) {
19
+ const normalized = files
20
+ .map((f) => path.join(basePath, f.toString()).replace(/\\/g, "/"))
21
+ .filter((p) => {
22
+ try {
23
+ return fs.statSync(p).isFile();
24
+ } catch {
25
+ return false;
26
+ }
27
+ });
28
+
29
+ const checkedFiles = matchPaths(normalized);
30
+
31
+ if (checkedFiles.files) {
32
+ setFileCount(checkedFiles.files.length);
33
+ setFilesToCheck(checkedFiles.files);
34
+ } else if (checkedFiles.error) {
35
+ setError(checkedFiles.error);
36
+ }
37
+ }
38
+
39
+ useEffect(() => {
40
+ if (fileOrFolderPath === ".") {
41
+ fs.readdir(".", { recursive: true }, (err, files) => {
42
+ if (err) {
43
+ setError(err.message);
44
+ return;
45
+ }
46
+ processFiles(".", files);
47
+ });
48
+ } else {
49
+ fs.stat(fileOrFolderPath, (err, stat) => {
50
+ if (err) {
51
+ setError(err.message);
52
+ return;
53
+ }
54
+
55
+ if (stat.isFile()) {
56
+ setFileCount(1);
57
+ setFilesToCheck([fileOrFolderPath]);
58
+ } else {
59
+ fs.readdir(fileOrFolderPath, { recursive: true }, (err, files) => {
60
+ if (err) {
61
+ setError(err.message);
62
+ return;
63
+ }
64
+
65
+ processFiles(fileOrFolderPath, files);
66
+ });
67
+ }
68
+ });
69
+ }
70
+ }, []);
71
+
72
+ useEffect(() => {
73
+ if (!filesToCheck) return;
74
+
75
+ let cancelled = false;
76
+
77
+ async function run() {
78
+ let checked: string[] = [];
79
+
80
+ if (!filesToCheck) return;
81
+
82
+ for (const file of filesToCheck) {
83
+ if (cancelled) return;
84
+
85
+ const res = addFile(file, ".");
86
+ if (res.error) {
87
+ setError(res.error);
88
+ return;
89
+ }
90
+
91
+ checked.push(file);
92
+
93
+ setIndexAddedCount(checked.length);
94
+ setFilesChecked([...checked]);
95
+
96
+ await new Promise((r) => setTimeout(r, 10));
97
+ }
98
+
99
+ setStage("done");
100
+ }
101
+
102
+ run();
103
+
104
+ return () => {
105
+ cancelled = true;
106
+ };
107
+ }, [filesToCheck]);
108
+
109
+ if (error)
110
+ return (
111
+ <Box flexDirection="column">
112
+ <Text color={RED}>✗ {error}</Text>
113
+ <Text color={TEXT}>Retry</Text>
114
+ </Box>
115
+ );
116
+
117
+ return (
118
+ <>
119
+ {stage === "working" && (
120
+ <Box gap={1} flexDirection="column">
121
+ <Box gap={1}>
122
+ <Text color={ACCENT}>
123
+ <Spinner type="dots10" />
124
+ </Text>
125
+ <Text>
126
+ Adding [{indexAddedCount}/{fileCount}]
127
+ </Text>
128
+ </Box>
129
+
130
+ <Box flexDirection="column">
131
+ {filesChecked.map((file) => (
132
+ <Box gap={1} key={file}>
133
+ <Text color={GREEN}>✓</Text>
134
+ <Text color={"gray"}>{file}</Text>
135
+ </Box>
136
+ ))}
137
+ </Box>
138
+ </Box>
139
+ )}
140
+ {stage === "done" && (
141
+ <Box flexDirection="column">
142
+ <Text color={GREEN}>✓ Added {indexAddedCount} files</Text>
143
+ {filesChecked.map((file) => (
144
+ <Text key={file} color={"gray"}>
145
+ {file}
146
+ </Text>
147
+ ))}
148
+ </Box>
149
+ )}
150
+ </>
151
+ );
152
+ }
@@ -0,0 +1,150 @@
1
+ import { Box, Text } from "ink";
2
+ import { useEffect, useState } from "react";
3
+ import { ACCENT, GREEN, RED } from "../colors";
4
+ import Spinner from "ink-spinner";
5
+ import {
6
+ createBranch,
7
+ deleteBranch,
8
+ mergeBranch,
9
+ switchBranch,
10
+ } from "../utils/branch";
11
+ import { switchEmitter } from "../utils/switchEvents";
12
+ import type { SwitchEvent } from "../utils/switchEvents";
13
+
14
+ export function BranchCommand({
15
+ name,
16
+ isDelete,
17
+ isSwitch,
18
+ isMerge,
19
+ mergingBranchName,
20
+ }: {
21
+ name: string;
22
+ isDelete?: boolean;
23
+ isSwitch?: boolean;
24
+ isMerge?: boolean;
25
+ mergingBranchName?: string;
26
+ }) {
27
+ const [stage, setStage] = useState<"working" | "done">("working");
28
+ const [error, setError] = useState<string | null>(null);
29
+ const [logs, setLogs] = useState<string[]>([]);
30
+
31
+ useEffect(() => {
32
+ switchEmitter.on("event", (e: SwitchEvent) => {
33
+ if (e.type === "checkpoint_created")
34
+ setLogs((l) => [...l, `📦 Checkpoint saved for ${e.branch}`]);
35
+ if (e.type === "files_deleted")
36
+ setLogs((l) => [...l, `🗑 Deleted ${e.files.length} files`]);
37
+ if (e.type === "files_restored")
38
+ setLogs((l) => [
39
+ ...l,
40
+ `✅ Restored ${e.files.length} files from ${e.source}`,
41
+ ]);
42
+ if (e.type === "switched")
43
+ setLogs((l) => [...l, `⇒ ${e.from} → ${e.to}`]);
44
+ if (e.type === "merge_conflict_detected")
45
+ setLogs((l) => [...l, `⚠️ Conflict in: ${e.files.join(", ")}`]);
46
+ if (e.type === "merge_complete")
47
+ setLogs((l) => [
48
+ ...l,
49
+ `🔀 Merged ${e.from} → ${e.to} (${e.filesChanged} files changed)`,
50
+ ]);
51
+ });
52
+ return () => {
53
+ switchEmitter.removeAllListeners("event");
54
+ };
55
+ }, []);
56
+
57
+ useEffect(() => {
58
+ if (isSwitch) {
59
+ const res = switchBranch(".", name);
60
+ if (res.error) {
61
+ setError(res.error);
62
+ return;
63
+ }
64
+ } else if (isDelete) {
65
+ const res = deleteBranch(".", name);
66
+ if (res.error) {
67
+ setError(res.error);
68
+ return;
69
+ }
70
+ } else if (isMerge) {
71
+ if (!mergingBranchName) {
72
+ setError("specify target branch name.");
73
+ return;
74
+ }
75
+ const res = mergeBranch(name, mergingBranchName, ".");
76
+ if (res.error) {
77
+ setError(res.error);
78
+ return;
79
+ }
80
+ } else {
81
+ const res = createBranch(".", name);
82
+ if (res.error) {
83
+ setError(res.error);
84
+ return;
85
+ }
86
+ }
87
+ setStage("done");
88
+ }, []);
89
+
90
+ if (error)
91
+ return (
92
+ <Box flexDirection="column">
93
+ <Text color={RED}>✗ {error}</Text>
94
+ {logs.map((log, i) => (
95
+ <Text key={i} color="grey">
96
+ {log}
97
+ </Text>
98
+ ))}
99
+ </Box>
100
+ );
101
+
102
+ return (
103
+ <>
104
+ {stage === "working" && (
105
+ <Box flexDirection="column">
106
+ <Box gap={1}>
107
+ <Text color={ACCENT}>
108
+ <Spinner type="circleQuarters" />
109
+ </Text>
110
+ <Text>
111
+ {isDelete
112
+ ? "Deleting"
113
+ : isSwitch
114
+ ? "Switching"
115
+ : isMerge
116
+ ? "Merging"
117
+ : "Creating"}{" "}
118
+ branch
119
+ </Text>
120
+ <Text color={isDelete ? RED : GREEN}>{name}</Text>
121
+ {isMerge && <Text>into</Text>}
122
+ {isMerge && mergingBranchName && (
123
+ <Text color={ACCENT}>{mergingBranchName}</Text>
124
+ )}
125
+ </Box>
126
+ </Box>
127
+ )}
128
+ {stage === "done" && (
129
+ <Box flexDirection="column">
130
+ <Text color={GREEN}>
131
+ ✓{" "}
132
+ {isDelete
133
+ ? "Deleted"
134
+ : isSwitch
135
+ ? "Switched"
136
+ : isMerge
137
+ ? "Merged"
138
+ : "Created"}{" "}
139
+ branch {name}
140
+ </Text>
141
+ {logs.map((log, i) => (
142
+ <Text key={i} color="grey">
143
+ {log}
144
+ </Text>
145
+ ))}
146
+ </Box>
147
+ )}
148
+ </>
149
+ );
150
+ }
@@ -0,0 +1,55 @@
1
+ import { Box, Text } from "ink";
2
+ import { ACCENT, GREEN, RED, TEXT } from "../colors";
3
+ import { useEffect, useState } from "react";
4
+ import Spinner from "ink-spinner";
5
+ import { checkoutCommit } from "../utils/checkout";
6
+
7
+ export function CheckoutCommand({
8
+ commitId,
9
+ branch,
10
+ }: {
11
+ commitId: string;
12
+ branch?: string;
13
+ }) {
14
+ const [stage, setStage] = useState<"working" | "done">("working");
15
+ const [error, setError] = useState<string | null>(null);
16
+
17
+ useEffect(() => {
18
+ const res = checkoutCommit(commitId, ".", branch);
19
+ if (res.error) {
20
+ setError(res.error);
21
+ return;
22
+ }
23
+
24
+ setStage("done");
25
+ }, []);
26
+
27
+ if (error)
28
+ return (
29
+ <Box flexDirection="column">
30
+ <Text color={RED}>✗ {error}</Text>
31
+ <Text color={TEXT}>Retry</Text>
32
+ </Box>
33
+ );
34
+
35
+ return (
36
+ <>
37
+ {stage === "working" && (
38
+ <Box flexDirection="column">
39
+ <Box gap={1}>
40
+ <Text color={ACCENT}>
41
+ <Spinner type="balloon" />
42
+ </Text>
43
+ <Text>Checking out</Text>
44
+ <Text color={"gray"}>'{commitId}'</Text>
45
+ </Box>
46
+ </Box>
47
+ )}
48
+ {stage === "done" && (
49
+ <Box flexDirection="column">
50
+ <Text color={GREEN}>✓ Checkout {commitId}</Text>
51
+ </Box>
52
+ )}
53
+ </>
54
+ );
55
+ }
@@ -0,0 +1,77 @@
1
+ import figures from "figures";
2
+ import { Box, Text } from "ink";
3
+ import { ACCENT, GREEN, RED, TEXT } from "../colors";
4
+ import { useEffect, useState } from "react";
5
+ import Spinner from "ink-spinner";
6
+ import { commitInBranch } from "../utils/commit";
7
+ import { getCurrentBranch } from "../utils/branch";
8
+
9
+ export function CommitCommand({
10
+ isAll,
11
+ singlePath,
12
+ message,
13
+ }: {
14
+ isAll?: boolean;
15
+ singlePath?: string;
16
+ message: string;
17
+ }) {
18
+ const [stage, setStage] = useState<"working" | "done">("working");
19
+ const [error, setError] = useState<string | null>(null);
20
+
21
+ useEffect(() => {
22
+ if (!isAll && !singlePath) {
23
+ setError("use -a / --all or specify path");
24
+ }
25
+ }, []);
26
+
27
+ useEffect(() => {
28
+ const branch = getCurrentBranch(".");
29
+
30
+ if (branch.error) {
31
+ setError(branch.error);
32
+ return;
33
+ }
34
+ if (!branch.branch) {
35
+ setError("no branch found.");
36
+ return;
37
+ }
38
+
39
+ const res = commitInBranch(message, ".", branch.branch.name);
40
+ if (res.error) {
41
+ setError(res.error);
42
+ return;
43
+ }
44
+ setStage("done");
45
+ }, []);
46
+
47
+ if (error)
48
+ return (
49
+ <Box flexDirection="column">
50
+ <Text color={RED}>✗ {error}</Text>
51
+ </Box>
52
+ );
53
+
54
+ return (
55
+ <>
56
+ {stage === "working" && (
57
+ <Box flexDirection="column">
58
+ <Box gap={1}>
59
+ <Text color={ACCENT}>
60
+ <Spinner type="toggle10" />
61
+ </Text>
62
+ <Text>Commiting</Text>
63
+ </Box>
64
+ <Box gap={1}>
65
+ <Text color={ACCENT}>{figures.arrowRight}</Text>
66
+ <Text color={"gray"}>{message}</Text>
67
+ </Box>
68
+ </Box>
69
+ )}
70
+ {stage === "done" && (
71
+ <Box flexDirection="column">
72
+ <Text color={GREEN}>✓ Done</Text>
73
+ </Box>
74
+ )}
75
+ </>
76
+ );
77
+ }
@@ -0,0 +1,45 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Box, Text } from "ink";
3
+ import Spinner from "ink-spinner";
4
+ import { ACCENT, GREEN, RED, TEXT } from "../colors";
5
+ import { initRepo } from "../utils/repo";
6
+
7
+ export function InitCommand() {
8
+ const [stage, setStage] = useState<"working" | "done">("working");
9
+ const [error, setError] = useState<string | null>(null);
10
+
11
+ useEffect(() => {
12
+ const res = initRepo(".");
13
+ if (res.error) setError(res.error);
14
+ else setStage("done");
15
+
16
+ setStage("done");
17
+ }, []);
18
+
19
+ if (error)
20
+ return (
21
+ <Box flexDirection="column">
22
+ <Text color={RED}>✗ {error}</Text>
23
+ <Text color={TEXT}>Retry</Text>
24
+ </Box>
25
+ );
26
+
27
+ return (
28
+ <>
29
+ {stage === "working" && (
30
+ <Box gap={1}>
31
+ <Text color={ACCENT}>
32
+ <Spinner type="point" />
33
+ </Text>
34
+ <Text>Init</Text>
35
+ </Box>
36
+ )}
37
+ {stage === "done" && (
38
+ <Box flexDirection="column">
39
+ <Text color={GREEN}>✓ Initialized an empty repo!</Text>
40
+ <Text color={TEXT}>Start building!</Text>
41
+ </Box>
42
+ )}
43
+ </>
44
+ );
45
+ }
@@ -0,0 +1,99 @@
1
+ import { Box, Text } from "ink";
2
+ import { ACCENT, GREEN, RED, TEXT } from "../colors";
3
+ import { useEffect, useState } from "react";
4
+ import { logCommits, logCommitsInBranch } from "../utils/commit";
5
+ import type { Commit, CommitRef } from "../types/commit";
6
+ import Spinner from "ink-spinner";
7
+ import { getCurrentBranch } from "../utils/branch";
8
+ import type { Branch } from "../types/branch";
9
+
10
+ export function LogCommand({ global }: { global?: boolean }) {
11
+ const [stage, setStage] = useState<"working" | "done">("working");
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [commits, setCommits] = useState<Commit[] | CommitRef[]>([]);
14
+ const [branch, setBranch] = useState<Branch | null>(null);
15
+
16
+ useEffect(() => {
17
+ const branch = getCurrentBranch(".");
18
+
19
+ if (branch.error) {
20
+ setError(branch.error);
21
+ return;
22
+ }
23
+ if (!branch.branch) {
24
+ setError("no branch found.");
25
+ return;
26
+ }
27
+
28
+ setBranch(branch.branch);
29
+ }, []);
30
+
31
+ useEffect(() => {
32
+ let res;
33
+
34
+ if (global) {
35
+ res = logCommits(".");
36
+ } else {
37
+ if (!branch) return;
38
+ res = logCommitsInBranch(".", branch.name);
39
+ }
40
+
41
+ if (!res) return;
42
+
43
+ if (res.error) {
44
+ setError(res.error);
45
+ return;
46
+ }
47
+ if (!res.commits) setCommits([]);
48
+ else setCommits(res.commits);
49
+ setStage("done");
50
+ }, [branch]);
51
+
52
+ if (error)
53
+ return (
54
+ <Box flexDirection="column">
55
+ <Text color={RED}>✗ {error}</Text>
56
+ <Text color={TEXT}>Retry</Text>
57
+ </Box>
58
+ );
59
+
60
+ return (
61
+ <>
62
+ {stage === "working" && (
63
+ <Box flexDirection="column">
64
+ <Box gap={1}>
65
+ <Text color={ACCENT}>
66
+ <Spinner type="circleHalves" />
67
+ </Text>
68
+ <Text>Logging</Text>
69
+ </Box>
70
+ </Box>
71
+ )}
72
+ {stage === "done" && (
73
+ <Box flexDirection="column">
74
+ <Text color={GREEN}>✓ Logs</Text>
75
+ {commits.length > 0 ? (
76
+ commits.map((commit) => (
77
+ <Box key={commit.id} flexDirection="column">
78
+ <Box gap={1}>
79
+ <Text color={ACCENT}>commit</Text>
80
+ <Text>{commit.id}</Text>
81
+ <Text color={GREEN}>
82
+ ({(commit as CommitRef).branch ?? branch?.name})
83
+ </Text>
84
+ </Box>
85
+ <Text>Date: {commit.date}</Text>
86
+ <Text color={"gray"}>
87
+ {" "}
88
+ {commit.message}
89
+ </Text>
90
+ </Box>
91
+ ))
92
+ ) : (
93
+ <Text color={TEXT}>No commits found</Text>
94
+ )}
95
+ </Box>
96
+ )}
97
+ </>
98
+ );
99
+ }