codechronicle-cli 1.0.0

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,106 @@
1
+ const fs = require("fs").promises; //file system: we can use asynchronous Promise-based operations like await fs.mkdir(...), await fs.writeFile(...)
2
+ const path = require("path"); //helps safely construct filesystem paths
3
+ const { getToken } = require("../utils/auth");
4
+ const repositoryApi = require("../services/repositoryApi");
5
+ const { requireAuth } = require("../utils/auth");
6
+
7
+ async function initRepo(repoName) {
8
+ requireAuth();
9
+ // process.cwd() returns the directory from which the CLI command was executed.
10
+ const repoPath = path.resolve(process.cwd(), ".chron");
11
+
12
+ const commitsPath = path.join(repoPath, "commits");
13
+ const stagingPath = path.join(repoPath, "staging");
14
+ const configPath = path.join(repoPath, "config.json");
15
+
16
+ try {
17
+ // fs.access() checks whether .chron already exists to prevent reinitialization.
18
+ try {
19
+ await fs.access(repoPath);
20
+ console.log("Repository is already initialized.");
21
+ return;
22
+ } catch {
23
+ // Repository doesn't exist yet, so initialization can continue.
24
+ }
25
+
26
+ const token = getToken();
27
+
28
+ const response = await repositoryApi.createRepository(token, {
29
+ name: repoName,
30
+ description: "",
31
+ visibility: "public",
32
+ });
33
+
34
+ const repository = response.data;
35
+
36
+ // recursive:true also creates the parent .vcsGit directory automatically.
37
+ await fs.mkdir(commitsPath, { recursive: true });
38
+ await fs.mkdir(stagingPath, { recursive: true });
39
+
40
+ // information of the repository
41
+ const config = {
42
+ repositoryId: repository.repositoryId,
43
+ repositoryName: repository.repositoryName,
44
+ defaultBranch: "main",
45
+ lastPushedCommit: null,
46
+ };
47
+
48
+ // JSON.stringify(..., null, 2) converts the object to readable, indented JSON.
49
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf-8");
50
+
51
+ console.log(`Repository '${repoName}' initialized successfully.`);
52
+ } catch (err) {
53
+ console.error("Error initializing repository:", err.message);
54
+ }
55
+ }
56
+
57
+ module.exports = { initRepo };
58
+
59
+ /*
60
+ ============================================================
61
+ WORKING OF INIT COMMAND
62
+ ============================================================
63
+
64
+ The initRepo() function initializes a local Code Chronicle repository.
65
+
66
+ When the user runs:
67
+ node index.js init <repoName>
68
+
69
+ 1. index.js uses Yargs to detect the "init" command and passes the
70
+ repository name to initRepo(repoName).
71
+
72
+ 2. process.cwd() identifies the directory where the user executed
73
+ the command. This directory is treated as the project root.
74
+
75
+ 3. A hidden ".vcsGit" directory is used to store all metadata related
76
+ to our custom version-control system.
77
+
78
+ 4. Before initialization, fs.access() checks whether ".vcsGit" already
79
+ exists. If it exists, initialization stops to avoid accidentally
80
+ overwriting an existing repository.
81
+
82
+ 5. Two directories are created inside ".vcsGit":
83
+
84
+ .vcsGit/
85
+ ├── commits/ -> Stores locally created commits.
86
+ └── staging/ -> Stores files added before they are committed.
87
+
88
+ 6. A config.json file is created to store repository configuration:
89
+
90
+ {
91
+ "storage": "supabase",
92
+ "bucket": "codechronicle",
93
+ "repoName": "<repository-name>"
94
+ }
95
+
96
+ 7. At this stage, everything exists only on the local system.
97
+ No files or repository data are uploaded to Supabase.
98
+
99
+ 8. Supabase will act as the remote storage and will be used later
100
+ when commands such as "push" and "pull" are executed.
101
+
102
+ This is conceptually similar to "git init", where Git creates a
103
+ .git directory to maintain repository metadata. In Code Chronicle,
104
+ .vcsGit serves a similar purpose for our custom version-control system.
105
+ ============================================================
106
+ */
@@ -0,0 +1,56 @@
1
+ const inquirer = require("inquirer").default;
2
+ const { login: loginUser } = require("../services/authService");
3
+ const { saveConfig, getConfig } = require("../utils/chronConfig");
4
+ const { getToken } = require("../utils/auth");
5
+
6
+ async function login() {
7
+ const config = getConfig();
8
+ if (config) {
9
+ console.log(`✔ You are already logged in as ${config.user.username}.`);
10
+ console.log("Run 'chron logout' to login with another account.");
11
+ return;
12
+ }
13
+
14
+ const answers = await inquirer.prompt([
15
+ {
16
+ type: "input",
17
+ name: "email",
18
+ message: "Enter your email:",
19
+ },
20
+ {
21
+ type: "password",
22
+ name: "password",
23
+ message: "Enter your password:",
24
+ mask: "*",
25
+ },
26
+ ]);
27
+
28
+ console.log("\nLogging in...\n");
29
+
30
+ try {
31
+ const response = await loginUser(answers.email, answers.password);
32
+
33
+ saveConfig({
34
+ token: response.data.token,
35
+ user: response.data.user,
36
+ });
37
+
38
+ console.log(`✔ ${response.message}`);
39
+ console.log(`Welcome back, ${response.data.user.username}!`);
40
+ } catch (error) {
41
+ // Server is not reachable
42
+ if (error.code === "ECONNREFUSED") {
43
+ console.log("Backend server is not running.");
44
+ return;
45
+ }
46
+
47
+ // // No response received (network issue / timeout)
48
+ // if (!error.response) {
49
+ // console.log("Unable to connect. Please check your internet connection.");
50
+ // return;
51
+ // }
52
+ console.log(`✖ ${error.response?.data?.message || error.message}`);
53
+ }
54
+ }
55
+
56
+ module.exports = { login };
@@ -0,0 +1,12 @@
1
+ const { getConfig, clearConfig } = require("../utils/chronConfig");
2
+ const { requireAuth } = require("../utils/auth");
3
+
4
+ async function logout() {
5
+ const config = requireAuth();
6
+ clearConfig();
7
+
8
+ console.log("✔ Logged out successfully.");
9
+ console.log(`Goodbye, ${config.user.username}!`);
10
+ }
11
+
12
+ module.exports = { logout };
@@ -0,0 +1,94 @@
1
+ const fs = require("fs").promises; //file system
2
+ const fswp = require("fs");
3
+ const path = require("path");
4
+ const supabase = require("../config/supabase");
5
+ const { requireAuth } = require("../utils/auth");
6
+ const repositoryApi = require("../services/repositoryApi");
7
+ const { getToken } = require("../utils/auth");
8
+ const { unzipDirectory } = require("../utils/unzipDirectory");
9
+
10
+ async function pullRepo() {
11
+ let tempDirectory = null;
12
+ try {
13
+ requireAuth(); //checks user logged in or not
14
+ console.log("pulling commits back...");
15
+ const repoPath = path.resolve(process.cwd(), ".chron"); //gives the current directory path
16
+ const configPath = path.join(repoPath, "config.json"); //path to json
17
+
18
+ if (!fswp.existsSync(repoPath)) {
19
+ console.log("Repository not initialized.");
20
+ return;
21
+ }
22
+
23
+ const config = JSON.parse(await fs.readFile(configPath, "utf8")); //reads the json file
24
+ const { repositoryId } = config; //storing repoId
25
+
26
+ if (!repositoryId) {
27
+ console.log("Repository id not found.");
28
+ return;
29
+ }
30
+
31
+ const token = getToken(); //get the token
32
+ const latestCommitResponse = await repositoryApi.getLatestCommit(
33
+ repositoryId,
34
+ token
35
+ );
36
+ const latestCommit = latestCommitResponse.data.response;
37
+ console.log("Remote latest commit:", latestCommit);
38
+
39
+ const response = await repositoryApi.pullRepository(repositoryId, token); //receive the zip file from backend
40
+ // console.log(response);
41
+ const tempDir = path.join(repoPath, "temp"); //create temporary folder
42
+ tempDirectory = tempDir;
43
+ fswp.mkdirSync(tempDir, { recursive: true });
44
+ const zipPath = path.join(tempDir, "pull.zip");
45
+
46
+ const writer = fswp.createWriteStream(zipPath);
47
+ response.data.pipe(writer);
48
+
49
+ await new Promise((resolve, reject) => {
50
+ writer.on("finish", resolve);
51
+ writer.on("error", reject);
52
+ });
53
+
54
+ // console.log("ZIP downloaded successfully.");
55
+ const commitsPath = path.join(repoPath, "commits");
56
+
57
+ await unzipDirectory(zipPath, commitsPath);
58
+
59
+ // Update local config only after pull succeeds
60
+ config.lastCommit = latestCommit;
61
+ config.lastPushedCommit = latestCommit;
62
+
63
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
64
+
65
+ console.log("done");
66
+ } catch (err) {
67
+ if (err.code === "ECONNREFUSED") {
68
+ console.log("Backend server is not running.");
69
+ return;
70
+ }
71
+ if (!err.response) {
72
+ console.log("Unable to connect. Please check your internet connection.");
73
+ return;
74
+ }
75
+
76
+ console.log("Pull failed");
77
+ console.log("Status:", err.response.status);
78
+ console.log("Backend response:", err.response.data);
79
+ } finally {
80
+ //deleting the temp folder
81
+ try {
82
+ if (tempDirectory && fswp.existsSync(tempDirectory)) {
83
+ fswp.rmSync(tempDirectory, {
84
+ recursive: true,
85
+ force: true,
86
+ });
87
+ }
88
+ } catch (err) {
89
+ console.log(err.code);
90
+ }
91
+ }
92
+ }
93
+
94
+ module.exports = { pullRepo };
@@ -0,0 +1,70 @@
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const { zipDirectory } = require("../utils/zipDirectory");
4
+ const repositoryApi = require("../services/repositoryApi");
5
+ const { getToken } = require("../utils/auth");
6
+ const { requireAuth } = require("../utils/auth");
7
+
8
+ async function pushRepo() {
9
+ requireAuth();
10
+ const repoPath = path.resolve(process.cwd(), ".chron");
11
+ const commitsPath = path.join(repoPath, "commits");
12
+ const configPath = path.join(repoPath, "config.json");
13
+
14
+ try {
15
+ try {
16
+ await fs.access(repoPath); // .chron exists
17
+ } catch {
18
+ console.log("Repository not initialized.");
19
+ return;
20
+ }
21
+ const config = JSON.parse(await fs.readFile(configPath, "utf8"));
22
+ const { repositoryId, lastCommit, lastPushedCommit } = config;
23
+
24
+ if (!lastCommit) {
25
+ console.log("No commits found. Commit your changes before pushing.");
26
+ return;
27
+ }
28
+
29
+ if (lastCommit === lastPushedCommit) {
30
+ console.log("Everything is up to date.");
31
+ return;
32
+ }
33
+
34
+ const zipPath = path.join(commitsPath, `${lastCommit}.zip`);
35
+ const commitPath = path.join(commitsPath, lastCommit);
36
+
37
+ await fs.access(commitPath);
38
+ await zipDirectory(commitPath, zipPath);
39
+
40
+ const token = getToken();
41
+
42
+ const response = await repositoryApi.pushRepository(
43
+ repositoryId,
44
+ zipPath,
45
+ token
46
+ );
47
+
48
+ console.log(response);
49
+
50
+ config.lastPushedCommit = lastCommit;
51
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
52
+ } catch (err) {
53
+ if (err.code === "ECONNREFUSED") {
54
+ console.log("Backend server is not running.");
55
+ return;
56
+ }
57
+
58
+ if (!err.response) {
59
+ console.log("Unable to connect. Please check your internet connection.");
60
+ return;
61
+ }
62
+
63
+ console.log(
64
+ "problem in pushing...",
65
+ err.response?.data?.message || err.message
66
+ );
67
+ }
68
+ }
69
+
70
+ module.exports = { pushRepo };
@@ -0,0 +1,121 @@
1
+ const fs = require("fs"); //file system
2
+ const fsp = require("fs").promises;
3
+ const path = require("path");
4
+ const { requireAuth } = require("../utils/auth");
5
+
6
+ async function revertRepo(commitID) {
7
+ requireAuth();
8
+ const repoPath = path.join(process.cwd(), ".chron"); //current working directory
9
+ const commitPath = path.join(repoPath, "commits", commitID); //path to the commit folder
10
+
11
+ try {
12
+ try {
13
+ await fsp.access(repoPath);
14
+ // .chron exists
15
+ } catch {
16
+ console.log("Repository not initialized.");
17
+ return;
18
+ }
19
+
20
+ // Check whether the commit exists
21
+ if (!fs.existsSync(commitPath)) {
22
+ console.log("Commit not found.");
23
+ return;
24
+ }
25
+
26
+ clearProjectRoot(process.cwd());
27
+
28
+ const commitItems = fs.readdirSync(commitPath); //read the particular commit folder
29
+ for (const item of commitItems) {
30
+ if (item === "commit.json") {
31
+ // Skip metadata file
32
+ continue;
33
+ }
34
+
35
+ const sourcePath = path.join(commitPath, item);
36
+ const destinationPath = path.join(process.cwd(), item);
37
+ const stats = fs.statSync(sourcePath);
38
+
39
+ if (stats.isFile()) {
40
+ // Restore file
41
+ fs.copyFileSync(sourcePath, destinationPath);
42
+ console.log(`Restored file: ${item}`);
43
+ } else if (stats.isDirectory()) {
44
+ if (fs.existsSync(destinationPath)) {
45
+ // Restore folder
46
+ fs.rmSync(destinationPath, {
47
+ recursive: true,
48
+ force: true,
49
+ });
50
+ }
51
+
52
+ copyDirectory(sourcePath, destinationPath);
53
+ console.log(`Restored folder: ${item}`);
54
+
55
+ const commitData = JSON.parse(
56
+ fs.readFileSync(path.join(commitPath, "commit.json"), "utf8")
57
+ );
58
+ const config = JSON.parse(
59
+ fs.readFileSync(path.join(repoPath, "config.json"), "utf8")
60
+ );
61
+ config.lastCommit = commitData.id;
62
+
63
+ fs.writeFileSync(
64
+ path.join(repoPath, "config.json"),
65
+ JSON.stringify(config, null, 2)
66
+ );
67
+ }
68
+ }
69
+ console.log("\nRepository reverted successfully.");
70
+ } catch (error) {
71
+ console.log(error);
72
+ }
73
+ }
74
+
75
+ function clearProjectRoot(projectPath) {
76
+ const entries = fs.readdirSync(projectPath, {
77
+ withFileTypes: true,
78
+ });
79
+
80
+ for (const entry of entries) {
81
+ // Never delete the .chron directory
82
+ if (entry.name === ".chron") {
83
+ continue;
84
+ }
85
+
86
+ const fullPath = path.join(projectPath, entry.name);
87
+
88
+ fs.rmSync(fullPath, {
89
+ recursive: true,
90
+ force: true,
91
+ });
92
+
93
+ console.log(`Deleted: ${entry.name}`);
94
+ }
95
+ }
96
+
97
+ function copyDirectory(source, destination) {
98
+ if (!fs.existsSync(destination)) {
99
+ fs.mkdirSync(destination, { recursive: true });
100
+ }
101
+
102
+ const items = fs.readdirSync(source);
103
+
104
+ for (const item of items) {
105
+ if (item === "commit.json") {
106
+ continue;
107
+ }
108
+ const sourcePath = path.join(source, item);
109
+ const destinationPath = path.join(destination, item);
110
+
111
+ const stats = fs.statSync(sourcePath);
112
+
113
+ if (stats.isDirectory()) {
114
+ copyDirectory(sourcePath, destinationPath);
115
+ } else {
116
+ fs.copyFileSync(sourcePath, destinationPath);
117
+ }
118
+ }
119
+ }
120
+
121
+ module.exports = { revertRepo };
@@ -0,0 +1,11 @@
1
+ const { requireAuth } = require("../utils/auth");
2
+
3
+ async function whoami() {
4
+ const config = requireAuth();
5
+ console.log("Logged in as\n");
6
+
7
+ console.log(`Username : ${config.user.username}`);
8
+ console.log(`Email : ${config.user.email}`);
9
+ }
10
+
11
+ module.exports = { whoami };
@@ -0,0 +1,3 @@
1
+ module.exports = {
2
+ BACKEND_URL: "https://code-chronical-latest-backend.onrender.com",
3
+ };
@@ -0,0 +1,24 @@
1
+ const path = require("path");
2
+ const { createClient } = require("@supabase/supabase-js");
3
+ require("dotenv").config({
4
+ path: path.join(__dirname, "..", ".env"),
5
+ quiet : true
6
+ });
7
+
8
+ const supabase = createClient(
9
+ process.env.SUPABASE_URL,
10
+ process.env.SUPABASE_KEY
11
+ );
12
+
13
+ module.exports = supabase;
14
+
15
+ /*
16
+ ---------------------------------------------------------
17
+ Supabase Configuration
18
+ ---------------------------------------------------------
19
+ This file initializes the Supabase client using the
20
+ project URL and secret key stored in the .env file.
21
+ The configured client is exported so it can be used
22
+ throughout the application for Storage operations.
23
+ ---------------------------------------------------------
24
+ */
package/index.js ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ require("dotenv").config({
4
+ quiet: true,
5
+ });
6
+
7
+ //yargs
8
+ const yargs = require("yargs");
9
+ const { hideBin } = require("yargs/helpers");
10
+
11
+ //commands
12
+ const { login } = require("./commands/login");
13
+ const { logout } = require("./commands/logout");
14
+ const { whoami } = require("./commands/whoami");
15
+ const { initRepo } = require("./commands/init");
16
+ const { addRepo } = require("./commands/add");
17
+ const { commitRepo } = require("./commands/commit");
18
+ const { pushRepo } = require("./commands/push");
19
+ const { pullRepo } = require("./commands/pull");
20
+ const { revertRepo } = require("./commands/revert");
21
+ const { cloneRepo } = require("./commands/clone");
22
+
23
+ yargs(hideBin(process.argv))
24
+ .scriptName("chron") //change the name from index.js to chron
25
+ .usage("Usage: chron <command> [options]")
26
+
27
+ .example("chron login", "Login to your Code Chronicle account")
28
+ .example("chron init my-repo", "Initialize a new repository")
29
+ .example("chron add .", "Stage all files")
30
+ .example("chron push", "Push commits to remote")
31
+
32
+ .strict() //gives complete list of commands and verify the unknown commands
33
+
34
+ .command({
35
+ command: "login",
36
+ describe: "Login to Code Chronicle",
37
+ handler: login,
38
+ })
39
+ .command({
40
+ command: "logout",
41
+ describe: "Logout from Code Chronicle",
42
+ handler: logout,
43
+ })
44
+ .command({
45
+ command: "whoami",
46
+ describe: "check the current user detail",
47
+ handler: whoami,
48
+ })
49
+ .command(
50
+ "init <repoName>",
51
+ "Initialize a new repository",
52
+ (yargs) => {
53
+ yargs.positional("repoName", {
54
+ describe: "Repository name",
55
+ type: "string",
56
+ });
57
+ },
58
+ (argv) => {
59
+ initRepo(argv.repoName);
60
+ }
61
+ )
62
+ .command(
63
+ "add <file>",
64
+ "Add file to the repository",
65
+ (yargs) => {
66
+ yargs.positional("file", {
67
+ describe: "File to add to staging area",
68
+ type: "string",
69
+ });
70
+ },
71
+ (argv) => {
72
+ addRepo(argv.file);
73
+ }
74
+ )
75
+ .command(
76
+ "commit <message>",
77
+ "Commit the staged file",
78
+ (yargs) => {
79
+ yargs.positional("message", {
80
+ describe: "Commit message",
81
+ type: "string",
82
+ });
83
+ },
84
+ (argv) => {
85
+ commitRepo(argv.message);
86
+ }
87
+ )
88
+ .command(
89
+ "push",
90
+ "Push the latest commit to the remote repository",
91
+ {},
92
+ pushRepo
93
+ )
94
+ .command("pull", "pull the commits in to local machine", {}, pullRepo)
95
+ .command(
96
+ "revert <commitId>",
97
+ "Restore the project to a specific commit",
98
+ () => {},
99
+ (argv) => {
100
+ revertRepo(argv.commitId);
101
+ }
102
+ )
103
+ .command(
104
+ "clone <repoId>",
105
+ "Clone the Project",
106
+ () => {},
107
+ (argv) => {
108
+ cloneRepo(argv.repoId);
109
+ }
110
+ )
111
+ .help()
112
+ .parse();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "codechronicle-cli",
3
+ "version": "1.0.0",
4
+ "bin": {
5
+ "chron": "./index.js"
6
+ },
7
+ "main": "index.js",
8
+ "scripts": {
9
+ "start": "node index.js"
10
+ },
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "ISC",
14
+ "description": "",
15
+ "dependencies": {
16
+ "@supabase/supabase-js": "^2.111.0",
17
+ "archiver": "^6.0.2",
18
+ "axios": "^1.18.1",
19
+ "dotenv": "^17.4.2",
20
+ "form-data": "^4.0.6",
21
+ "inquirer": "^14.0.2",
22
+ "unzipper": "^0.12.5",
23
+ "uuid": "^14.0.1",
24
+ "yargs": "^18.1.0"
25
+ }
26
+ }
@@ -0,0 +1,11 @@
1
+ const axios = require("axios");
2
+ const config = require("../config/config");
3
+
4
+ const api = axios.create({
5
+ baseURL: config.BACKEND_URL,
6
+ headers: {
7
+ "Content-Type": "application/json",
8
+ },
9
+ });
10
+
11
+ module.exports = api;
@@ -0,0 +1,18 @@
1
+ const api = require("./api");
2
+
3
+ async function login(email, password) {
4
+ try {
5
+ const response = await api.post("/api/auth/login", {
6
+ email,
7
+ password,
8
+ });
9
+
10
+ return response.data;
11
+ } catch (error) {
12
+ throw error;
13
+ }
14
+ }
15
+
16
+ module.exports = {
17
+ login,
18
+ };