@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.
@@ -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);
@@ -0,0 +1,4 @@
1
+ export interface File {
2
+ name: string;
3
+ content: string;
4
+ }
@@ -0,0 +1,6 @@
1
+ import type { Commit } from "./commit";
2
+
3
+ export interface Branch {
4
+ name: string;
5
+ latestCommitId: string;
6
+ }
@@ -0,0 +1,7 @@
1
+ import type { FileBlob } from "./files";
2
+
3
+ export interface Checkpoint {
4
+ files: FileBlob[];
5
+ date: string;
6
+ commitId: string;
7
+ }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ export type FileStatus =
2
+ | "deleted"
3
+ | "untracked"
4
+ | "modified"
5
+ | "staged"
6
+ | "unchanged";
7
+
8
+ export interface FileBlob {
9
+ name: string;
10
+ path: string;
11
+ hash: string;
12
+ status: FileStatus;
13
+ }
@@ -0,0 +1,5 @@
1
+ export interface Repo {
2
+ name: string;
3
+ isFork: boolean;
4
+ branch: string;
5
+ }
@@ -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
+ }