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,376 @@
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const { requireAuth } = require("../utils/auth");
4
+
5
+ async function addRepo(filePath) {
6
+ requireAuth();
7
+ const projectRoot = process.cwd();
8
+ const repoPath = path.join(projectRoot, ".chron");
9
+ // path.join → combines segments into a clean, normalized path
10
+ const stagingPath = path.join(repoPath, "staging");
11
+
12
+ try {
13
+ // Ensure the user has initialized a Code Chronicle repository first (.chron exists or not).
14
+ await fs.access(repoPath);
15
+
16
+ // "add ." stages the complete project.
17
+ if (filePath === ".") {
18
+ await copyDirectory(projectRoot, stagingPath, projectRoot);
19
+ console.log("All project files added to the staging area.");
20
+ return;
21
+ }
22
+
23
+ // Convert the provided file path into an absolute path.
24
+ const sourcePath = path.resolve(projectRoot, filePath);
25
+
26
+ // Check whether the requested file actually exists.
27
+ const fileStat = await fs.stat(sourcePath);
28
+
29
+ // Preserve the file's relative directory structure inside staging.
30
+ const relativePath = path.relative(projectRoot, sourcePath);
31
+ // path.relative(from, to) → gives the relative path from 'from' to 'to'
32
+
33
+ // Prevent files outside the current project from being staged.
34
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
35
+ console.log("Cannot add files outside the repository.");
36
+ return;
37
+ }
38
+
39
+ const destinationPath = path.join(stagingPath, relativePath);
40
+
41
+ if (fileStat.isFile()) {
42
+ // If only a file is added
43
+ // Create parent folders when adding files such as src/controllers/user.js.
44
+ await fs.mkdir(path.dirname(destinationPath), { recursive: true });
45
+ await fs.copyFile(sourcePath, destinationPath);
46
+ } else if (fileStat.isDirectory()) {
47
+ // If the whole directory is added
48
+ await copyDirectory(sourcePath, destinationPath, projectRoot);
49
+ } else {
50
+ console.log("Unsupported file type.");
51
+ return;
52
+ }
53
+
54
+ if (fileStat.isFile()) {
55
+ console.log(`File '${relativePath}' added to the staging area.`);
56
+ } else {
57
+ console.log(`Directory '${relativePath}' added to the staging area.`);
58
+ }
59
+ } catch (err) {
60
+ if (err.code === "ENOENT") {
61
+ console.log(
62
+ "Repository or file not found. Initialize the repository first and check the file path."
63
+ );
64
+ return;
65
+ }
66
+ console.error("Error adding file:", err.message);
67
+ }
68
+ }
69
+
70
+ async function copyDirectory(source, destination) {
71
+ await fs.mkdir(destination, { recursive: true });
72
+
73
+ const entries = await fs.readdir(source, {
74
+ withFileTypes: true,
75
+ });
76
+
77
+ for (const entry of entries) {
78
+ if (entry.name === ".chron" || entry.name === "node_modules") {
79
+ continue;
80
+ }
81
+
82
+ const sourcePath = path.join(source, entry.name);
83
+ const destinationPath = path.join(destination, entry.name);
84
+
85
+ if (entry.isDirectory()) {
86
+ await copyDirectory(sourcePath, destinationPath);
87
+ } else {
88
+ await fs.copyFile(sourcePath, destinationPath);
89
+ }
90
+ }
91
+ }
92
+
93
+ module.exports = { addRepo };
94
+
95
+ /*
96
+ ============================================================
97
+ WORKING OF ADD COMMAND
98
+ ============================================================
99
+
100
+ The addRepo() function adds a selected file from the working directory
101
+ to the local staging area of the Code Chronicle version-control system.
102
+
103
+ When the user runs:
104
+ node index.js add <filePath>
105
+
106
+ Example:
107
+ node index.js add src/app.js
108
+
109
+ 1. index.js uses Yargs to detect the "add" command and passes the
110
+ provided file path to addRepo(filePath).
111
+
112
+ 2. process.cwd() identifies the directory where the command was
113
+ executed. This directory is treated as the repository/project root.
114
+
115
+ 3. The function locates the ".vcsGit" directory created earlier by
116
+ the "init" command.
117
+
118
+ 4. fs.access() checks whether ".vcsGit" exists. This ensures that
119
+ the repository has been initialized before files can be staged.
120
+
121
+ 5. path.resolve() converts the provided file path into an absolute
122
+ path so that filesystem operations can reliably locate the file.
123
+
124
+ 6. fs.stat() checks the provided path and verifies that it represents
125
+ a file. Currently, this implementation supports adding individual
126
+ files rather than entire directories.
127
+
128
+ 7. path.relative() calculates the file's location relative to the
129
+ project root. This allows the original directory structure to be
130
+ preserved inside the staging area.
131
+
132
+ For example:
133
+
134
+ Project file:
135
+ src/app.js
136
+
137
+ Staged file:
138
+ .vcsGit/staging/src/app.js
139
+
140
+ 8. Files located outside the current repository are rejected to prevent
141
+ unrelated files from being added to the staging area.
142
+
143
+ 9. Before copying the file, fs.mkdir() with { recursive: true } creates
144
+ any required parent directories inside the staging area.
145
+
146
+ 10. fs.copyFile() copies the selected file from the working directory
147
+ into ".vcsGit/staging".
148
+
149
+ The original file remains unchanged in the working directory. The
150
+ staging area contains a copy representing the version of the file that
151
+ will be included in the next commit.
152
+
153
+ Overall flow:
154
+
155
+ CLI Command
156
+
157
+ node index.js add src/app.js
158
+
159
+ Yargs detects "add"
160
+
161
+ addRepo("src/app.js")
162
+
163
+ Check .vcsGit exists
164
+
165
+ Locate and validate the source file
166
+
167
+ Calculate its relative project path
168
+
169
+ Preserve directory structure
170
+
171
+ Copy file to staging
172
+
173
+ .vcsGit/staging/src/app.js
174
+
175
+ Current VCS Flow:
176
+
177
+ Working Directory
178
+
179
+ │ add
180
+
181
+ .vcsGit/staging/
182
+
183
+ │ commit
184
+
185
+ .vcsGit/commits/
186
+
187
+ │ push
188
+
189
+ Remote Storage (Supabase)
190
+
191
+ Important:
192
+ The "add" command is completely local. It does not create a commit
193
+ and does not communicate with Supabase. Its responsibility is only
194
+ to prepare selected files in the staging area for the next commit.
195
+
196
+ Unlike real Git, which uses an index and a content-addressable object
197
+ database internally, Code Chronicle currently uses physical file copies
198
+ inside the staging directory. This provides a simpler Git-inspired
199
+ version-control implementation.
200
+ ============================================================
201
+ */
202
+
203
+ /*
204
+ ==========================================
205
+ WORKING OF copyDirectory()
206
+ ==========================================
207
+
208
+ Purpose:
209
+ --------
210
+ This helper function recursively copies an entire directory from the source
211
+ location to the destination while preserving the original folder structure.
212
+
213
+ It is mainly used by the "add" command whenever the user stages a complete
214
+ folder instead of a single file.
215
+
216
+ Example:
217
+ --------
218
+ Project Structure
219
+
220
+ controllers/
221
+ ├── add.js
222
+ ├── commit.js
223
+ └── auth/
224
+ ├── login.js
225
+ └── signup.js
226
+
227
+ Command:
228
+ node index.js add controllers
229
+
230
+ Result:
231
+
232
+ .vcsGit/
233
+ └── staging/
234
+ └── controllers/
235
+ ├── add.js
236
+ ├── commit.js
237
+ └── auth/
238
+ ├── login.js
239
+ └── signup.js
240
+
241
+
242
+ Working Flow:
243
+ -------------
244
+
245
+ 1. Create the destination directory.
246
+ ---------------------------------
247
+ fs.mkdir(destination, { recursive: true })
248
+
249
+ Before copying anything, we ensure that the destination folder exists.
250
+ The recursive option automatically creates any missing parent directories.
251
+
252
+
253
+ 2. Read all entries inside the source directory.
254
+ ---------------------------------------------
255
+ fs.readdir(source, { withFileTypes: true })
256
+
257
+ Instead of returning only file names, Node returns Dirent objects.
258
+
259
+ This allows us to distinguish between:
260
+ • Files
261
+ • Directories
262
+
263
+ using methods such as:
264
+ entry.isFile()
265
+ entry.isDirectory()
266
+
267
+
268
+ 3. Iterate over every entry.
269
+ --------------------------
270
+ Each file and folder inside the current directory is processed one by one.
271
+
272
+
273
+ 4. Ignore unnecessary directories.
274
+ -------------------------------
275
+ Certain directories should never be copied into the staging area.
276
+
277
+ Examples:
278
+ .vcsGit
279
+ node_modules
280
+
281
+ The "continue" statement skips these entries and moves to the next one.
282
+
283
+
284
+ 5. Build source and destination paths.
285
+ -----------------------------------
286
+ sourcePath:
287
+ Absolute path of the current file/folder.
288
+
289
+ destinationPath:
290
+ Location where the same item should be copied inside staging.
291
+
292
+ Since only the current entry name is appended, the original project
293
+ hierarchy is naturally preserved.
294
+
295
+
296
+ 6. Check whether the current entry is a directory.
297
+ -----------------------------------------------
298
+ If it is a directory, the function calls itself:
299
+
300
+ copyDirectory(sourcePath, destinationPath)
301
+
302
+ This is called recursion.
303
+
304
+ Every recursive call handles one subdirectory until all nested folders
305
+ have been processed.
306
+
307
+
308
+ 7. Copy files.
309
+ -----------
310
+ If the current entry is a file, it is copied directly using:
311
+
312
+ fs.copyFile(sourcePath, destinationPath)
313
+
314
+ Files represent the base case of recursion because they cannot contain
315
+ any further children.
316
+
317
+
318
+ Recursion Example:
319
+ ------------------
320
+
321
+ copyDirectory(controllers)
322
+
323
+
324
+ ├── add.js
325
+ │ │
326
+ │ ▼
327
+ │ copyFile()
328
+
329
+ ├── commit.js
330
+ │ │
331
+ │ ▼
332
+ │ copyFile()
333
+
334
+ └── auth/
335
+
336
+
337
+ copyDirectory(auth)
338
+
339
+ ├── login.js
340
+ │ │
341
+ │ ▼
342
+ │ copyFile()
343
+
344
+ └── signup.js
345
+
346
+
347
+ copyFile()
348
+
349
+
350
+ Time Complexity:
351
+ ----------------
352
+ O(N)
353
+
354
+ where N is the total number of files and directories being copied.
355
+
356
+ Every file and directory is visited exactly once.
357
+
358
+
359
+ Space Complexity:
360
+ -----------------
361
+ O(H)
362
+
363
+ where H is the maximum depth of the directory tree due to recursive
364
+ function calls.
365
+
366
+
367
+ Interview Explanation:
368
+ ----------------------
369
+ This function recursively copies a complete directory while preserving
370
+ its folder structure. It first creates the destination directory,
371
+ reads all files and subdirectories using fs.readdir() with
372
+ withFileTypes: true, skips ignored directories such as .vcsGit and
373
+ node_modules, copies files directly using fs.copyFile(), and recursively
374
+ calls itself whenever it encounters another directory. This continues
375
+ until every nested folder and file has been copied into the staging area.
376
+ */
@@ -0,0 +1,75 @@
1
+ const fs = require("fs");
2
+ const fsp = require("fs").promises; //file system: we can use asynchronous Promise-based operations like await fs.mkdir(...), await fs.writeFile(...)
3
+ const path = require("path");
4
+ const { getToken } = require("../utils/auth");
5
+ const { requireAuth } = require("../utils/auth");
6
+ const repositoryApi = require("../services/repositoryApi");
7
+ const { unzipDirectory } = require("../utils/unzipDirectory");
8
+
9
+ async function cloneRepo(repoId) {
10
+ let tempPath;
11
+ try {
12
+ requireAuth(); //check authentication
13
+
14
+ if (!repoId) {
15
+ console.log("Repository ID is required.");
16
+ return;
17
+ }
18
+
19
+ //validate repoId format
20
+ const objectIdRegex = /^[0-9a-fA-F]{24}$/;
21
+ if (!objectIdRegex.test(repoId)) {
22
+ console.log("Invalid repository ID.");
23
+ return;
24
+ }
25
+
26
+ //get repository details
27
+ const token = getToken();
28
+ const repo = await repositoryApi.getRepoDetail(repoId, token);
29
+ const repository = repo.data;
30
+
31
+ //check for same repo name folder exists
32
+ const targetPath = path.resolve(process.cwd(), repository.name);
33
+ if (fs.existsSync(targetPath)) {
34
+ console.log(`Directory '${repository.name}' already exists.`);
35
+ return;
36
+ }
37
+
38
+ fs.mkdirSync(targetPath, { recursive: true }); //create repo folder
39
+
40
+ //creating temp folder
41
+ tempPath = path.resolve(targetPath, ".clone-temp");
42
+ fs.mkdirSync(tempPath, { recursive: true });
43
+
44
+ //getting the commits to the local folder
45
+ const response = await repositoryApi.cloneRepository(repoId, token);
46
+ const zipPath = path.join(tempPath, "pull.zip");
47
+
48
+ const writer = fs.createWriteStream(zipPath);
49
+ response.data.pipe(writer);
50
+
51
+ await new Promise((resolve, reject) => {
52
+ writer.on("finish", resolve);
53
+ writer.on("error", reject);
54
+ });
55
+
56
+ await unzipDirectory(zipPath, targetPath);
57
+ console.log("done");
58
+ } catch (err) {
59
+ console.error("Error cloning repository:", err.message);
60
+ } finally {
61
+ //deleting the temp folder
62
+ try {
63
+ if (tempPath && fs.existsSync(tempPath)) {
64
+ fs.rmSync(tempPath, {
65
+ recursive: true,
66
+ force: true,
67
+ });
68
+ }
69
+ } catch (err) {
70
+ console.log(err);
71
+ }
72
+ }
73
+ }
74
+
75
+ module.exports = { cloneRepo };
@@ -0,0 +1,178 @@
1
+ const fs = require("fs").promises;
2
+ const path = require("path");
3
+ const { v4: uuidv4 } = require("uuid");
4
+
5
+ async function commitRepo(message) {
6
+ const repoPath = path.resolve(process.cwd(), ".chron");
7
+ const stagingPath = path.join(repoPath, "staging");
8
+ const commitsPath = path.join(repoPath, "commits");
9
+
10
+ try {
11
+ // Ensure that the repository was initialized before committing.
12
+ await fs.access(repoPath);
13
+
14
+ const stagedFiles = await fs.readdir(stagingPath);
15
+
16
+ // Prevent creating commits when nothing has been staged.
17
+ if (stagedFiles.length === 0) {
18
+ console.log("Nothing to commit. Add files to the staging area first.");
19
+ return;
20
+ }
21
+
22
+ // UUID provides a unique identifier for each commit.
23
+ const commitID = uuidv4();
24
+ const commitDir = path.join(commitsPath, commitID);
25
+ await fs.mkdir(commitDir, { recursive: true });
26
+
27
+ const configPath = path.join(repoPath, "config.json");
28
+ const config = await getConfig(configPath);
29
+ const lastCommit = config.lastCommit;
30
+ if (lastCommit) {
31
+ const previousCommitPath = path.join(commitsPath, lastCommit);
32
+ await copyPreviousCommit(previousCommitPath, commitDir);
33
+ }
34
+
35
+ await copyStagingToCommit(stagingPath, commitDir);
36
+
37
+ // Store metadata describing this specific commit.
38
+ const commitMetadata = {
39
+ id: commitID,
40
+ message,
41
+ date: new Date().toISOString(),
42
+ parentCommit: lastCommit || null,
43
+ branch: "main",
44
+ };
45
+
46
+ await fs.writeFile(
47
+ path.join(commitDir, "commit.json"),
48
+ JSON.stringify(commitMetadata, null, 2),
49
+ "utf-8"
50
+ );
51
+
52
+ config.lastCommit = commitID;
53
+ await fs.writeFile(configPath, JSON.stringify(config, null, 2), "utf8");
54
+
55
+ /*
56
+ * Clear staging after a successful commit.
57
+ * rm() removes the old staging directory and mkdir() recreates it empty.
58
+ */
59
+ await fs.rm(stagingPath, {
60
+ recursive: true,
61
+ force: true,
62
+ });
63
+
64
+ await fs.mkdir(stagingPath, {
65
+ recursive: true,
66
+ });
67
+
68
+ console.log(`Commit ${commitID} created with message: ${message}\n`);
69
+ console.log("Current version supports one commit per push.");
70
+ console.log("Please push your latest commit before creating a new one.");
71
+ console.log("");
72
+ } catch (err) {
73
+ if (err.code === "ENOENT") {
74
+ console.log("Repository not initialized. Run the init command first.");
75
+ return;
76
+ }
77
+
78
+ console.error("Error during commit:", err.message);
79
+ }
80
+ }
81
+
82
+ async function copyStagingToCommit(stagingPath, commitDir) {
83
+ await fs.mkdir(commitDir, { recursive: true });
84
+
85
+ const entries = await fs.readdir(stagingPath, {
86
+ withFileTypes: true,
87
+ });
88
+
89
+ for (const entry of entries) {
90
+ const sourcePath = path.join(stagingPath, entry.name);
91
+ const destinationPath = path.join(commitDir, entry.name);
92
+
93
+ if (entry.isDirectory()) {
94
+ await copyStagingToCommit(sourcePath, destinationPath);
95
+ } else {
96
+ await fs.copyFile(sourcePath, destinationPath);
97
+ }
98
+ }
99
+ }
100
+
101
+ async function getConfig(configPath) {
102
+ return JSON.parse(await fs.readFile(configPath, "utf8"));
103
+ }
104
+
105
+ async function copyPreviousCommit(source, destination) {
106
+ await fs.mkdir(destination, { recursive: true });
107
+
108
+ const entries = await fs.readdir(source, {
109
+ withFileTypes: true,
110
+ });
111
+
112
+ for (const entry of entries) {
113
+ if (entry.name === "commit.json") {
114
+ continue;
115
+ }
116
+
117
+ const sourcePath = path.join(source, entry.name);
118
+ const destinationPath = path.join(destination, entry.name);
119
+
120
+ if (entry.isDirectory()) {
121
+ await copyPreviousCommit(sourcePath, destinationPath);
122
+ } else {
123
+ await fs.copyFile(sourcePath, destinationPath);
124
+ }
125
+ }
126
+ }
127
+
128
+ module.exports = { commitRepo };
129
+
130
+ /*
131
+ |--------------------------------------------------------------------------
132
+ | WORKING OF commitRepo()
133
+ |--------------------------------------------------------------------------
134
+ |
135
+ | The commit command creates a snapshot of the current project state.
136
+ | Unlike incremental commits, each commit stores the complete project
137
+ | snapshot, making it easy to restore any previous version later.
138
+ |
139
+ | Workflow:
140
+ |
141
+ | 1. Verify that the current directory contains an initialized
142
+ | .vcsGit repository.
143
+ |
144
+ | 2. Check whether the staging area contains any files.
145
+ | If nothing has been staged, the commit is aborted.
146
+ |
147
+ | 3. Generate a unique commit ID using UUID and create a new
148
+ | directory inside .vcsGit/commits/.
149
+ |
150
+ | 4. Read config.json to obtain the ID of the previous commit.
151
+ | If a previous commit exists:
152
+ | - Copy its entire snapshot into the new commit directory.
153
+ | - Skip copying commit.json since each commit has its own
154
+ | metadata.
155
+ |
156
+ | 5. Copy every staged file into the new commit directory.
157
+ | New files are added, while modified files overwrite the older
158
+ | versions copied from the previous snapshot.
159
+ |
160
+ | 6. Create commit.json containing metadata such as:
161
+ | - Commit ID
162
+ | - Commit message
163
+ | - Timestamp
164
+ |
165
+ | 7. Update config.json by setting lastCommit to the newly
166
+ | created commit ID so future commits know which snapshot
167
+ | to use as their base.
168
+ |
169
+ | 8. Clear the staging area by deleting the staging directory
170
+ | and recreating it as an empty folder.
171
+ |
172
+ | Result:
173
+ | Every commit represents a complete snapshot of the repository,
174
+ | allowing future commands such as revert, checkout, push, and
175
+ | pull to restore project states easily.
176
+ |
177
+ |--------------------------------------------------------------------------
178
+ */