@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/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +111 -0
- package/README.md +87 -0
- package/dist/index.mjs +45649 -0
- package/index.ts +1 -0
- package/package.json +40 -0
- package/src/colors.ts +10 -0
- package/src/commands/add.tsx +152 -0
- package/src/commands/branch.tsx +150 -0
- package/src/commands/checkout.tsx +55 -0
- package/src/commands/commit.tsx +77 -0
- package/src/commands/init.tsx +45 -0
- package/src/commands/log.tsx +99 -0
- package/src/commands/status.tsx +77 -0
- package/src/index.tsx +80 -0
- package/src/types/add.ts +4 -0
- package/src/types/branch.ts +6 -0
- package/src/types/checkpoint.ts +7 -0
- package/src/types/commit.ts +16 -0
- package/src/types/files.ts +13 -0
- package/src/types/repo.ts +5 -0
- package/src/utils/add.ts +126 -0
- package/src/utils/branch.ts +590 -0
- package/src/utils/checkout.ts +57 -0
- package/src/utils/checkpoint.ts +52 -0
- package/src/utils/commit.ts +237 -0
- package/src/utils/forgeIgnore.ts +56 -0
- package/src/utils/objects.ts +28 -0
- package/src/utils/repo.ts +64 -0
- package/src/utils/status.ts +46 -0
- package/src/utils/switchEvents.ts +10 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Box, Text } from "ink";
|
|
2
|
+
import { ACCENT, BLUE, GREEN, PURPLE, RED, TEXT, YELLOW } from "../colors";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { listAllFilesWithStatus, type FileRef } from "../utils/status";
|
|
5
|
+
import Spinner from "ink-spinner";
|
|
6
|
+
import type { FileStatus } from "../types/files";
|
|
7
|
+
|
|
8
|
+
export function StatusCommand() {
|
|
9
|
+
const [stage, setStage] = useState<"working" | "done">("working");
|
|
10
|
+
const [error, setError] = useState<string | null>(null);
|
|
11
|
+
const [files, setFiles] = useState<FileRef[]>([]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const res = listAllFilesWithStatus(".");
|
|
15
|
+
|
|
16
|
+
if (res.error) {
|
|
17
|
+
setError(res.error);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!res.files) {
|
|
21
|
+
setError("no files found.");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
setFiles(res.files);
|
|
26
|
+
|
|
27
|
+
setStage("done");
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
const colorMap: Record<FileStatus, string> = {
|
|
31
|
+
deleted: RED,
|
|
32
|
+
modified: YELLOW,
|
|
33
|
+
staged: BLUE,
|
|
34
|
+
untracked: PURPLE,
|
|
35
|
+
unchanged: TEXT,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (error)
|
|
39
|
+
return (
|
|
40
|
+
<Box flexDirection="column">
|
|
41
|
+
<Text color={RED}>✗ {error}</Text>
|
|
42
|
+
<Text color={TEXT}>Retry</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<>
|
|
48
|
+
{stage === "working" && (
|
|
49
|
+
<Box flexDirection="column">
|
|
50
|
+
<Box gap={1}>
|
|
51
|
+
<Text color={ACCENT}>
|
|
52
|
+
<Spinner type="balloon2" />
|
|
53
|
+
</Text>
|
|
54
|
+
<Text>Fetching status</Text>
|
|
55
|
+
</Box>
|
|
56
|
+
</Box>
|
|
57
|
+
)}
|
|
58
|
+
{stage === "done" && (
|
|
59
|
+
<Box flexDirection="column">
|
|
60
|
+
<Text color={GREEN}>✓ Status</Text>
|
|
61
|
+
{files.length > 0 ? (
|
|
62
|
+
files.map((file) => (
|
|
63
|
+
<Box key={file.path} flexDirection="column">
|
|
64
|
+
<Box gap={1}>
|
|
65
|
+
<Text>{file.path}</Text>
|
|
66
|
+
<Text color={colorMap[file.status]}>({file.status})</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
</Box>
|
|
69
|
+
))
|
|
70
|
+
) : (
|
|
71
|
+
<Text color={TEXT}>No files changed</Text>
|
|
72
|
+
)}
|
|
73
|
+
</Box>
|
|
74
|
+
)}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { InitCommand } from "./commands/init";
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { AddCommand } from "./commands/add";
|
|
5
|
+
import { CommitCommand } from "./commands/commit";
|
|
6
|
+
import { CheckoutCommand } from "./commands/checkout";
|
|
7
|
+
import { LogCommand } from "./commands/log";
|
|
8
|
+
import { BranchCommand } from "./commands/branch";
|
|
9
|
+
import { StatusCommand } from "./commands/status";
|
|
10
|
+
|
|
11
|
+
const program = new Command();
|
|
12
|
+
|
|
13
|
+
program
|
|
14
|
+
.command("init")
|
|
15
|
+
.description("init a repo")
|
|
16
|
+
.action(() => {
|
|
17
|
+
render(<InitCommand />);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command("add")
|
|
22
|
+
.argument("[path]")
|
|
23
|
+
.description("add a file or everything")
|
|
24
|
+
.action((path) => {
|
|
25
|
+
render(<AddCommand fileOrFolderPath={path} />);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.command("commit")
|
|
30
|
+
.argument("[path]")
|
|
31
|
+
.requiredOption("-m, --message <msg>", "commit message")
|
|
32
|
+
.option("-a, --all", "commit all files")
|
|
33
|
+
.action((path, options) => {
|
|
34
|
+
render(
|
|
35
|
+
<CommitCommand
|
|
36
|
+
message={options.message}
|
|
37
|
+
isAll={options.all}
|
|
38
|
+
singlePath={path}
|
|
39
|
+
/>,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
program
|
|
44
|
+
.command("checkout")
|
|
45
|
+
.argument("[commit]")
|
|
46
|
+
.option("-b, --branch <branch>", "specify which branch commit")
|
|
47
|
+
.action((commit, options) => {
|
|
48
|
+
render(<CheckoutCommand commitId={commit} branch={options.branch} />);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
program
|
|
52
|
+
.command("log")
|
|
53
|
+
.option("-g, --global", "log all commits")
|
|
54
|
+
.action((options) => {
|
|
55
|
+
render(<LogCommand global={options.global} />);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
program
|
|
59
|
+
.command("branch")
|
|
60
|
+
.argument("[name]")
|
|
61
|
+
.option("-d, --delete", "delete a branch")
|
|
62
|
+
.option("-s, --switch", "switch branches")
|
|
63
|
+
.option("-m, --merge <branch>", "merge branches")
|
|
64
|
+
.action((name, options) => {
|
|
65
|
+
render(
|
|
66
|
+
<BranchCommand
|
|
67
|
+
name={name}
|
|
68
|
+
isDelete={options.delete}
|
|
69
|
+
isSwitch={options.switch}
|
|
70
|
+
isMerge={!!options.merge}
|
|
71
|
+
mergingBranchName={options.merge}
|
|
72
|
+
/>,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program.command("status").action(() => {
|
|
77
|
+
render(<StatusCommand />);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
program.parse(process.argv);
|
package/src/types/add.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FileBlob } from "./files";
|
|
2
|
+
|
|
3
|
+
export interface Commit {
|
|
4
|
+
id: string;
|
|
5
|
+
message: string;
|
|
6
|
+
fileBlobs: FileBlob[];
|
|
7
|
+
date: string;
|
|
8
|
+
parent?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommitRef {
|
|
12
|
+
id: string;
|
|
13
|
+
message: string;
|
|
14
|
+
branch: string;
|
|
15
|
+
date: string;
|
|
16
|
+
}
|
package/src/utils/add.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import type { FileBlob, FileStatus } from "../types/files";
|
|
4
|
+
import { getCommit, getLatestCommitId } from "./commit";
|
|
5
|
+
import { getCurrentBranch } from "./branch";
|
|
6
|
+
import { hashContent, readObject, writeObject } from "./objects";
|
|
7
|
+
|
|
8
|
+
export function addFile(
|
|
9
|
+
file_path: string,
|
|
10
|
+
repo_path: string,
|
|
11
|
+
): {
|
|
12
|
+
status: "ok" | "error";
|
|
13
|
+
error?: string;
|
|
14
|
+
} {
|
|
15
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
16
|
+
const tempAddedFiles = path.join(forgeFolder, "tempAddedFiles.json");
|
|
17
|
+
|
|
18
|
+
if (!fs.existsSync(tempAddedFiles))
|
|
19
|
+
return {
|
|
20
|
+
status: "error",
|
|
21
|
+
error: "tempAddedFiles.json is missing, consider reinitialize repo.",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const parsed = JSON.parse(fs.readFileSync(tempAddedFiles, "utf-8"));
|
|
25
|
+
|
|
26
|
+
const tempFiles: FileBlob[] = Array.isArray(parsed.files) ? parsed.files : [];
|
|
27
|
+
|
|
28
|
+
const alreadyExists = tempFiles.some((f) => f.path === file_path);
|
|
29
|
+
|
|
30
|
+
if (alreadyExists) {
|
|
31
|
+
return { status: "ok" };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let fileStatus = determineFileStatus(repo_path, file_path);
|
|
35
|
+
|
|
36
|
+
const content = fs.readFileSync(file_path).toString();
|
|
37
|
+
const hash = writeObject(repo_path, content);
|
|
38
|
+
|
|
39
|
+
const newTempFiles = [
|
|
40
|
+
...tempFiles,
|
|
41
|
+
{
|
|
42
|
+
name: path.basename(file_path),
|
|
43
|
+
path: file_path,
|
|
44
|
+
hash: hash,
|
|
45
|
+
status: fileStatus.file_status ?? "untracked",
|
|
46
|
+
} as FileBlob,
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
tempAddedFiles,
|
|
51
|
+
JSON.stringify({ files: newTempFiles }, null, 2),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
return { status: "ok" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function determineFileStatus(
|
|
58
|
+
repo_path: string,
|
|
59
|
+
file_path: string,
|
|
60
|
+
): { status: "ok" | "error"; file_status?: FileStatus; error?: string } {
|
|
61
|
+
const isInDisk = fs.existsSync(file_path);
|
|
62
|
+
if (!isInDisk) return { status: "ok", file_status: "deleted" };
|
|
63
|
+
|
|
64
|
+
const diskFileContent = fs.readFileSync(file_path).toString();
|
|
65
|
+
|
|
66
|
+
const forgeFolder = path.join(repo_path, ".forge");
|
|
67
|
+
const tempAddedFiles = path.join(forgeFolder, "tempAddedFiles.json");
|
|
68
|
+
const latestCommitId = getLatestCommitId(repo_path);
|
|
69
|
+
if (latestCommitId.error) {
|
|
70
|
+
return {
|
|
71
|
+
status: "error",
|
|
72
|
+
error: latestCommitId.error,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!latestCommitId.latestCommitId) {
|
|
77
|
+
return { status: "ok", file_status: "untracked" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const branchName = getCurrentBranch(repo_path);
|
|
81
|
+
if (branchName.error || !branchName.branch) {
|
|
82
|
+
return {
|
|
83
|
+
status: "error",
|
|
84
|
+
error: latestCommitId.error || "found no branch",
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const commitData = getCommit(
|
|
89
|
+
repo_path,
|
|
90
|
+
latestCommitId.latestCommitId,
|
|
91
|
+
branchName.branch.name,
|
|
92
|
+
);
|
|
93
|
+
if (commitData.error || !commitData.commit) {
|
|
94
|
+
return {
|
|
95
|
+
status: "error",
|
|
96
|
+
error: latestCommitId.error || "No commit data found.",
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parsed = JSON.parse(fs.readFileSync(tempAddedFiles, "utf-8"));
|
|
101
|
+
|
|
102
|
+
const tempFiles: FileBlob[] = Array.isArray(parsed.files) ? parsed.files : [];
|
|
103
|
+
const commitFiles: FileBlob[] = commitData.commit.fileBlobs;
|
|
104
|
+
|
|
105
|
+
const targetTempFile = tempFiles.find((t) => t.path === file_path);
|
|
106
|
+
const targetComittedFile = commitFiles.find((c) => c.path === file_path);
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
targetComittedFile &&
|
|
110
|
+
isInDisk &&
|
|
111
|
+
readObject(repo_path, targetComittedFile.hash) !== diskFileContent
|
|
112
|
+
)
|
|
113
|
+
return { status: "ok", file_status: "modified" };
|
|
114
|
+
else if (
|
|
115
|
+
targetComittedFile &&
|
|
116
|
+
isInDisk &&
|
|
117
|
+
readObject(repo_path, targetComittedFile.hash) === diskFileContent
|
|
118
|
+
)
|
|
119
|
+
return { status: "ok", file_status: "unchanged" };
|
|
120
|
+
else if (!targetComittedFile && targetTempFile)
|
|
121
|
+
return { status: "ok", file_status: "staged" };
|
|
122
|
+
else if (!targetComittedFile && !targetTempFile)
|
|
123
|
+
return { status: "ok", file_status: "untracked" };
|
|
124
|
+
|
|
125
|
+
return { status: "ok" };
|
|
126
|
+
}
|