gent-cli 2.1.0 → 5.0.1

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/src/index.js CHANGED
@@ -1,25 +1,46 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Gent CLI - A Git-like version control system
4
+ * ============================================================================
5
+ * Gent CLI - A Git-like version control system with cloud backend
5
6
  * Main entry point for the CLI application
6
- *
7
- * @author Your Name
8
- * @version 1.0.0
7
+ * ============================================================================
8
+ *
9
+ * COMMANDS:
10
+ * Repository: init, clone
11
+ * Staging: add, rm, reset, status, diff
12
+ * History: commit, log, show, tag
13
+ * Branching: branch, checkout, merge, stash
14
+ * Remote: remote, push, pull
15
+ * Auth: register, login, logout, whoami
16
+ *
17
+ * @author Abdalrahman Kanawati
18
+ * @version 2.0.0
9
19
  */
10
20
 
11
21
  const { program } = require('commander');
12
22
  const chalk = require('chalk');
13
23
  const packageJson = require('../package.json');
14
24
 
15
- // Import commands
25
+ // Import core commands
16
26
  const initCommand = require('./commands/init');
27
+ const cloneCommand = require('./commands/clone');
17
28
  const statusCommand = require('./commands/status');
18
29
  const addCommand = require('./commands/add');
30
+ const rmCommand = require('./commands/rm');
31
+ const resetCommand = require('./commands/reset');
32
+ const diffCommand = require('./commands/diff');
19
33
  const commitCommand = require('./commands/commit');
20
34
  const logCommand = require('./commands/log');
35
+ const showCommand = require('./commands/show');
36
+ const tagCommand = require('./commands/tag');
21
37
  const branchCommand = require('./commands/branch');
22
38
  const checkoutCommand = require('./commands/checkout');
39
+ const mergeCommand = require('./commands/merge');
40
+ const stashCommand = require('./commands/stash');
41
+ const remoteCommand = require('./commands/remote');
42
+ const pushCommand = require('./commands/push');
43
+ const pullCommand = require('./commands/pull');
23
44
 
24
45
  // Import auth commands
25
46
  const registerCommand = require('./commands/register');
@@ -27,30 +48,27 @@ const loginCommand = require('./commands/login');
27
48
  const logoutCommand = require('./commands/logout');
28
49
  const whoamiCommand = require('./commands/whoami');
29
50
 
30
- // Import cloud repository commands
31
- const createCommand = require('./commands/create');
32
- const listCommand = require('./commands/list');
33
- const cloneCommand = require('./commands/clone');
34
- const pushCommand = require('./commands/push');
35
- const pullCommand = require('./commands/pull');
36
- const remoteCommand = require('./commands/remote');
37
-
38
51
  // Configure CLI
39
52
  program
40
53
  .name('gent')
41
- .description(chalk.cyan('🚀 Gent - A Git-like version control CLI'))
54
+ .description(chalk.cyan('Gent - A Git-like version control CLI with cloud backend'))
42
55
  .version(packageJson.version, '-v, --version', 'Output the current version');
43
56
 
44
- // Register commands
57
+ // ─── Repository Setup ───────────────────────────────────
58
+
45
59
  program
46
60
  .command('init')
47
61
  .description('Initialize a new gent repository')
48
62
  .option('-y, --yes', 'Skip prompts and use defaults')
49
- .option('--cloud', 'Create a corresponding cloud repository')
50
- .option('-d, --description <description>', 'Repository description (for cloud)')
51
- .option('-p, --private', 'Make cloud repository private')
52
63
  .action(initCommand);
53
64
 
65
+ program
66
+ .command('clone <url> [directory]')
67
+ .description('Clone a remote repository')
68
+ .action(cloneCommand);
69
+
70
+ // ─── Staging & Working Tree ─────────────────────────────
71
+
54
72
  program
55
73
  .command('status')
56
74
  .description('Show the working tree status')
@@ -63,12 +81,33 @@ program
63
81
  .option('-A, --all', 'Add all files')
64
82
  .action(addCommand);
65
83
 
84
+ program
85
+ .command('rm <files...>')
86
+ .description('Remove files from working tree and staging')
87
+ .option('--cached', 'Only remove from staging, keep file on disk')
88
+ .action(rmCommand);
89
+
90
+ program
91
+ .command('reset [files...]')
92
+ .description('Unstage files or reset HEAD to a commit')
93
+ .option('--hard <hash>', 'Reset HEAD and working tree to commit')
94
+ .option('--soft <hash>', 'Reset HEAD but keep staging')
95
+ .action(resetCommand);
96
+
97
+ program
98
+ .command('diff [files...]')
99
+ .description('Show changes between working tree, staging, and commits')
100
+ .option('--staged', 'Show staged changes vs last commit')
101
+ .option('--stat', 'Show diffstat summary only')
102
+ .action(diffCommand);
103
+
104
+ // ─── History ────────────────────────────────────────────
105
+
66
106
  program
67
107
  .command('commit')
68
108
  .description('Record changes to the repository')
69
109
  .option('-m, --message <message>', 'Commit message')
70
110
  .option('-a, --all', 'Automatically stage all modified files')
71
- .option('--push', 'Push to remote after commit')
72
111
  .action(commitCommand);
73
112
 
74
113
  program
@@ -76,8 +115,24 @@ program
76
115
  .description('Show commit logs')
77
116
  .option('-n, --number <count>', 'Limit the number of commits to show', '10')
78
117
  .option('--oneline', 'Show each commit on a single line')
118
+ .option('--stat', 'Show file change statistics')
79
119
  .action(logCommand);
80
120
 
121
+ program
122
+ .command('show [ref]')
123
+ .description('Show commit details and diff')
124
+ .option('--no-patch', 'Suppress diff output')
125
+ .action(showCommand);
126
+
127
+ program
128
+ .command('tag [name]')
129
+ .description('Create, list, or delete tags')
130
+ .option('-m, --message <message>', 'Create annotated tag with message')
131
+ .option('-d, --delete <name>', 'Delete a tag')
132
+ .action(tagCommand);
133
+
134
+ // ─── Branching & Merging ────────────────────────────────
135
+
81
136
  program
82
137
  .command('branch')
83
138
  .description('List, create, or delete branches')
@@ -92,7 +147,40 @@ program
92
147
  .option('-b, --create', 'Create a new branch')
93
148
  .action(checkoutCommand);
94
149
 
95
- // Authentication commands
150
+ program
151
+ .command('merge <branch>')
152
+ .description('Merge a branch into the current branch (3-way smart merge)')
153
+ .option('-m, --message <message>', 'Merge commit message')
154
+ .action(mergeCommand);
155
+
156
+ program
157
+ .command('stash [subcommand]')
158
+ .description('Stash working tree changes (pop|list|drop|apply)')
159
+ .option('-m, --message <message>', 'Stash message')
160
+ .option('-i, --index <index>', 'Stash index for pop/apply/drop')
161
+ .action(stashCommand);
162
+
163
+ // ─── Remote & Sync ──────────────────────────────────────
164
+
165
+ program
166
+ .command('remote [subcommand] [args...]')
167
+ .description('Manage remote connections (add|remove|set-url)')
168
+ .option('-v, --verbose', 'Show remote URLs')
169
+ .action(remoteCommand);
170
+
171
+ program
172
+ .command('push [remote] [branch]')
173
+ .description('Push local commits to remote')
174
+ .option('-f, --force', 'Force push (overwrite remote)')
175
+ .action(pushCommand);
176
+
177
+ program
178
+ .command('pull [remote] [branch]')
179
+ .description('Pull and merge remote commits')
180
+ .action(pullCommand);
181
+
182
+ // ─── Authentication ─────────────────────────────────────
183
+
96
184
  program
97
185
  .command('register')
98
186
  .description('Create a new user account')
@@ -115,43 +203,6 @@ program
115
203
  .description('Display current user information')
116
204
  .action(whoamiCommand);
117
205
 
118
- // Cloud repository commands
119
- program
120
- .command('create <repo-name>')
121
- .description('Create a new cloud repository')
122
- .option('-d, --description <description>', 'Repository description')
123
- .option('-p, --private', 'Make repository private')
124
- .option('-y, --yes', 'Skip prompts and use defaults')
125
- .option('--init-local', 'Initialize local repository and link remote')
126
- .action(createCommand);
127
-
128
- program
129
- .command('list')
130
- .alias('ls')
131
- .description('List all your cloud repositories')
132
- .action(listCommand);
133
-
134
- program
135
- .command('clone <repo-url> [directory]')
136
- .description('Clone a cloud repository (format: owner_id/repo_name)')
137
- .action(cloneCommand);
138
-
139
- program
140
- .command('push [remote] [branch]')
141
- .description('Push local commits to cloud')
142
- .action(pushCommand);
143
-
144
- program
145
- .command('pull [remote] [branch]')
146
- .description('Pull commits from cloud to local')
147
- .action(pullCommand);
148
-
149
- program
150
- .command('remote [action] [name] [url]')
151
- .description('Manage remote repositories')
152
- .option('-v, --verbose', 'Show verbose output')
153
- .action(remoteCommand);
154
-
155
206
  // Help command
156
207
  program
157
208
  .command('help [command]')
@@ -11,42 +11,26 @@ module.exports = {
11
11
  COMMITS_FILE: 'commits.json',
12
12
  HEAD_FILE: 'HEAD',
13
13
  AUTH_FILE: 'auth.json',
14
- OBJECTS_DIR: 'objects',
15
- REMOTE_CONFIG_FILE: 'remote.json',
16
14
 
17
15
  // API Configuration
18
16
  API_BASE_URL: 'https://gent-api.onrender.com',
19
17
  API_ENDPOINTS: {
20
- // Auth endpoints
21
18
  LOGIN: '/api/auth/login/',
22
19
  REGISTER: '/api/auth/register/',
23
20
  LOGOUT: '/api/auth/logout/',
24
21
  REFRESH: '/api/auth/token/refresh/',
25
22
  PROFILE: '/api/auth/profile/',
26
23
 
27
- // Repository endpoints
28
- REPOS_LIST: '/api/repos/',
29
- REPOS_CREATE: '/api/repos/create/',
30
- REPOS_GET: '/api/repos/{owner_id}/{name}/',
31
- REPOS_DELETE: '/api/repos/{owner_id}/{name}/delete/',
32
-
33
- // Blob endpoints
34
- BLOB_GET: '/api/repos/{owner_id}/{name}/blob/{sha}/',
35
- BLOB_CREATE: '/api/repos/{owner_id}/{name}/blob/create/',
36
-
37
- // Tree endpoints
38
- TREE_GET: '/api/repos/{owner_id}/{name}/tree/{sha}/',
39
- TREE_CREATE: '/api/repos/{owner_id}/{name}/tree/create/',
40
-
41
- // Commit endpoints
42
- COMMITS_LIST: '/api/repos/{owner_id}/{name}/commits/',
43
- COMMITS_GET: '/api/repos/{owner_id}/{name}/commits/{sha}/',
44
- COMMITS_CREATE: '/api/repos/{owner_id}/{name}/commits/create/',
45
-
46
- // Branch endpoints
47
- BRANCHES_LIST: '/api/repos/{owner_id}/{name}/branches/',
48
- BRANCHES_GET: '/api/repos/{owner_id}/{name}/branches/{branch_name}/',
49
- BRANCHES_CREATE: '/api/repos/{owner_id}/{name}/branches/create/'
24
+ // Repository endpoints (used by push/pull/clone)
25
+ // Base: /api/repos/:id/
26
+ REPOS: '/api/repos/',
27
+ REPO_PUSH: '/push/', // POST - upload commits + objects
28
+ REPO_PULL: '/pull/', // GET - download commits + objects since hash
29
+ REPO_CLONE: '/clone/', // GET - full repo download
30
+ REPO_REFS: '/refs/', // GET - list remote branch refs
31
+ REPO_TAGS: '/tags/', // GET/POST/DELETE - tag management
32
+ REPO_MERGE: '/merge/', // POST - server-side merge request
33
+ REPO_COMMITS: '/commits/', // GET - commit history
50
34
  },
51
35
 
52
36
  // Default ignore patterns
@@ -0,0 +1,236 @@
1
+ /**
2
+ * ============================================================================
3
+ * Diff Engine - Line-level LCS diff with hunk generation and unified format
4
+ * ============================================================================
5
+ *
6
+ * PURPOSE:
7
+ * Compute minimal edit scripts between two text files, classify each line
8
+ * as insert / delete / equal, generate unified diff output.
9
+ *
10
+ * ALGORITHM: Longest Common Subsequence (LCS)
11
+ * - Build M×N dynamic programming matrix where M,N = line counts
12
+ * - dp[i][j] = length of LCS of first i lines of A and first j lines of B
13
+ * - Recurrence:
14
+ * if A[i] == B[j]: dp[i][j] = dp[i-1][j-1] + 1
15
+ * else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
16
+ * - Backtrack from dp[M][N] to produce edit operations
17
+ * - Time: O(M*N), Space: O(M*N) — uses Uint32Array for memory efficiency
18
+ *
19
+ * INSERTION/DELETION CLASSIFICATION:
20
+ * During backtrack:
21
+ * - A[i]==B[j] → EQUAL (line unchanged)
22
+ * - Move up (i-1) → DELETE (line only in old version)
23
+ * - Move left (j-1) → INSERT (line only in new version)
24
+ *
25
+ * HUNK GENERATION:
26
+ * Groups adjacent changes with N context lines (default 3) into hunks.
27
+ * Changes within 2*N+1 lines of each other merge into one hunk.
28
+ * Output format matches unified diff:
29
+ * @@ -oldStart,oldCount +newStart,newCount @@
30
+ *
31
+ * BACKEND EXPECTATIONS:
32
+ * Diffs are computed locally. Backend does NOT need diff support.
33
+ * Backend stores blob objects; clients compute diffs on demand.
34
+ *
35
+ * ============================================================================
36
+ */
37
+
38
+ const { splitLines } = require('./hash-engine');
39
+
40
+ // ─── Core LCS / Diff ────────────────────────────────────
41
+
42
+ /**
43
+ * Build LCS length matrix.
44
+ * @param {String[]} a
45
+ * @param {String[]} b
46
+ * @returns {Array<Uint32Array>}
47
+ */
48
+ function buildLcsMatrix(a, b) {
49
+ const rows = a.length + 1;
50
+ const cols = b.length + 1;
51
+ const matrix = Array.from({ length: rows }, () => new Uint32Array(cols));
52
+
53
+ for (let i = 1; i < rows; i++) {
54
+ for (let j = 1; j < cols; j++) {
55
+ if (a[i - 1] === b[j - 1]) {
56
+ matrix[i][j] = matrix[i - 1][j - 1] + 1;
57
+ } else {
58
+ matrix[i][j] = Math.max(matrix[i - 1][j], matrix[i][j - 1]);
59
+ }
60
+ }
61
+ }
62
+ return matrix;
63
+ }
64
+
65
+ /**
66
+ * Backtrack LCS matrix → line operations.
67
+ * @param {String[]} oldLines
68
+ * @param {String[]} newLines
69
+ * @returns {Array<{type: 'equal'|'insert'|'delete', oldLine: number, newLine: number, content: String}>}
70
+ */
71
+ function buildLineOperations(oldLines, newLines) {
72
+ const matrix = buildLcsMatrix(oldLines, newLines);
73
+ const ops = [];
74
+ let i = oldLines.length;
75
+ let j = newLines.length;
76
+
77
+ while (i > 0 || j > 0) {
78
+ if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) {
79
+ ops.push({ type: 'equal', oldLine: i, newLine: j, content: oldLines[i - 1] });
80
+ i--; j--;
81
+ } else if (j > 0 && (i === 0 || matrix[i][j - 1] >= matrix[i - 1][j])) {
82
+ ops.push({ type: 'insert', oldLine: i, newLine: j, content: newLines[j - 1] });
83
+ j--;
84
+ } else {
85
+ ops.push({ type: 'delete', oldLine: i, newLine: j, content: oldLines[i - 1] });
86
+ i--;
87
+ }
88
+ }
89
+
90
+ return ops.reverse();
91
+ }
92
+
93
+ /**
94
+ * Diff two texts → operations + stats.
95
+ * @param {String} oldText
96
+ * @param {String} newText
97
+ * @returns {{ algorithm: string, operations: Array, stats: Object }}
98
+ */
99
+ function diffText(oldText, newText) {
100
+ const oldLines = splitLines(oldText);
101
+ const newLines = splitLines(newText);
102
+ const operations = buildLineOperations(oldLines, newLines);
103
+ const stats = summarizeOperations(operations);
104
+ return { algorithm: 'lcs-line-v1', operations, stats };
105
+ }
106
+
107
+ /**
108
+ * Count insert/delete/equal ops.
109
+ * @param {Array} operations
110
+ * @returns {{insertions, deletions, unchanged, changes}}
111
+ */
112
+ function summarizeOperations(operations) {
113
+ let insertions = 0, deletions = 0, unchanged = 0;
114
+ for (const op of operations) {
115
+ if (op.type === 'insert') insertions++;
116
+ else if (op.type === 'delete') deletions++;
117
+ else unchanged++;
118
+ }
119
+ return { insertions, deletions, unchanged, changes: insertions + deletions };
120
+ }
121
+
122
+ // ─── Hunk Generation ────────────────────────────────────
123
+
124
+ /**
125
+ * Group diff ops into hunks with context lines (like git diff).
126
+ * @param {Array} ops - From buildLineOperations
127
+ * @param {Number} contextLines - Context around changes (default 3)
128
+ * @returns {Array<{oldStart, oldCount, newStart, newCount, lines: String[]}>}
129
+ */
130
+ function generateHunks(ops, contextLines = 3) {
131
+ const changeIndices = [];
132
+ for (let i = 0; i < ops.length; i++) {
133
+ if (ops[i].type !== 'equal') changeIndices.push(i);
134
+ }
135
+ if (changeIndices.length === 0) return [];
136
+
137
+ // Group changes within contextLines*2 of each other
138
+ const groups = [];
139
+ let group = [changeIndices[0]];
140
+ for (let i = 1; i < changeIndices.length; i++) {
141
+ if (changeIndices[i] - changeIndices[i - 1] <= contextLines * 2 + 1) {
142
+ group.push(changeIndices[i]);
143
+ } else {
144
+ groups.push(group);
145
+ group = [changeIndices[i]];
146
+ }
147
+ }
148
+ groups.push(group);
149
+
150
+ const hunks = [];
151
+ for (const g of groups) {
152
+ const first = g[0];
153
+ const last = g[g.length - 1];
154
+ const start = Math.max(0, first - contextLines);
155
+ const end = Math.min(ops.length - 1, last + contextLines);
156
+
157
+ let oldLine = 0, newLine = 0;
158
+ for (let i = 0; i < start; i++) {
159
+ if (ops[i].type === 'equal' || ops[i].type === 'delete') oldLine++;
160
+ if (ops[i].type === 'equal' || ops[i].type === 'insert') newLine++;
161
+ }
162
+
163
+ const hunkOldStart = oldLine + 1;
164
+ const hunkNewStart = newLine + 1;
165
+ let hunkOldCount = 0, hunkNewCount = 0;
166
+ const lines = [];
167
+
168
+ for (let i = start; i <= end; i++) {
169
+ const op = ops[i];
170
+ if (op.type === 'equal') {
171
+ lines.push(` ${op.content}`);
172
+ hunkOldCount++; hunkNewCount++;
173
+ } else if (op.type === 'delete') {
174
+ lines.push(`-${op.content}`);
175
+ hunkOldCount++;
176
+ } else {
177
+ lines.push(`+${op.content}`);
178
+ hunkNewCount++;
179
+ }
180
+ }
181
+
182
+ hunks.push({ oldStart: hunkOldStart, oldCount: hunkOldCount, newStart: hunkNewStart, newCount: hunkNewCount, lines });
183
+ }
184
+ return hunks;
185
+ }
186
+
187
+ // ─── Unified Diff Format ────────────────────────────────
188
+
189
+ /**
190
+ * Format as unified diff string (like `git diff`).
191
+ * @param {String} filePath
192
+ * @param {String} oldText
193
+ * @param {String} newText
194
+ * @returns {String}
195
+ */
196
+ function formatUnifiedDiff(filePath, oldText, newText) {
197
+ const oldLines = splitLines(oldText);
198
+ const newLines = splitLines(newText);
199
+ const ops = buildLineOperations(oldLines, newLines);
200
+ const hunks = generateHunks(ops);
201
+ if (hunks.length === 0) return '';
202
+
203
+ const out = [`--- a/${filePath}`, `+++ b/${filePath}`];
204
+ for (const h of hunks) {
205
+ out.push(`@@ -${h.oldStart},${h.oldCount} +${h.newStart},${h.newCount} @@`);
206
+ out.push(...h.lines);
207
+ }
208
+ return out.join('\n');
209
+ }
210
+
211
+ // ─── Patch Application ──────────────────────────────────
212
+
213
+ /**
214
+ * Apply operations to reconstruct target from source.
215
+ * @param {Array} ops
216
+ * @returns {String[]} Reconstructed lines
217
+ */
218
+ function applyOperations(ops) {
219
+ const result = [];
220
+ for (const op of ops) {
221
+ if (op.type === 'equal' || op.type === 'insert') {
222
+ result.push(op.content);
223
+ }
224
+ }
225
+ return result;
226
+ }
227
+
228
+ module.exports = {
229
+ diffText,
230
+ summarizeOperations,
231
+ buildLcsMatrix,
232
+ buildLineOperations,
233
+ generateHunks,
234
+ formatUnifiedDiff,
235
+ applyOperations
236
+ };
@@ -5,7 +5,7 @@
5
5
 
6
6
  const fs = require('fs').promises;
7
7
  const path = require('path');
8
- const { GENT_DIR, DEFAULT_IGNORE_PATTERNS, IGNORE_FILE, OBJECTS_DIR } = require('./constants');
8
+ const { GENT_DIR, DEFAULT_IGNORE_PATTERNS, IGNORE_FILE } = require('./constants');
9
9
 
10
10
  /**
11
11
  * Check if a path exists
@@ -113,31 +113,23 @@ async function getAllFiles(dir, ignorePatterns = []) {
113
113
  */
114
114
  function shouldIgnore(filePath, patterns) {
115
115
  const normalizedPath = filePath.replace(/\\/g, '/');
116
- const segments = normalizedPath.split('/');
117
116
 
118
117
  for (const pattern of patterns) {
119
- const normalizedPattern = pattern.replace(/\/$/, '');
120
-
121
- // Check if any segment matches the pattern (for directory-level ignores like node_modules)
122
- if (segments.some(segment => segment === normalizedPattern)) {
123
- return true;
124
- }
125
-
126
- // Exact match or starts with pattern
127
- if (normalizedPath === normalizedPattern || normalizedPath.startsWith(normalizedPattern + '/')) {
118
+ // Exact match
119
+ if (normalizedPath === pattern || normalizedPath.startsWith(pattern + '/')) {
128
120
  return true;
129
121
  }
130
122
 
131
123
  // Wildcard match
132
- if (normalizedPattern.includes('*')) {
133
- const regex = new RegExp('^' + normalizedPattern.replace(/\*/g, '.*') + '$');
124
+ if (pattern.includes('*')) {
125
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
134
126
  if (regex.test(normalizedPath)) {
135
127
  return true;
136
128
  }
137
129
  }
138
130
 
139
131
  // Extension match
140
- if (normalizedPattern.startsWith('*.') && normalizedPath.endsWith(normalizedPattern.substring(1))) {
132
+ if (pattern.startsWith('*.') && normalizedPath.endsWith(pattern.substring(1))) {
141
133
  return true;
142
134
  }
143
135
  }
@@ -178,53 +170,11 @@ async function getTrackedFiles(gentPath, commitHash) {
178
170
  }
179
171
 
180
172
  const repository = await readJSON(path.join(gentPath, 'commits.json'));
181
- const commit = repository.commits.find(c => c.sha === commitHash);
173
+ const commit = repository.commits.find(c => c.hash === commitHash);
182
174
 
183
175
  return commit ? commit.files : [];
184
176
  }
185
177
 
186
- /**
187
- * Get path to the objects directory
188
- * @returns {Promise<String>}
189
- */
190
- async function getObjectsPath() {
191
- const gentPath = await getGentPath();
192
- const objectsPath = path.join(gentPath, OBJECTS_DIR);
193
- await ensureDir(objectsPath);
194
- return objectsPath;
195
- }
196
-
197
- /**
198
- * Save file content as a blob
199
- * @param {String} content - File content
200
- * @param {String} hash - Content hash
201
- */
202
- async function saveBlob(content, hash) {
203
- const objectsPath = await getObjectsPath();
204
- const blobPath = path.join(objectsPath, hash);
205
-
206
- // Only write if it doesn't exist (content-addressable, immutable)
207
- if (!await pathExists(blobPath)) {
208
- await fs.writeFile(blobPath, content, 'utf-8');
209
- }
210
- }
211
-
212
- /**
213
- * Get file content from blob
214
- * @param {String} hash - Content hash
215
- * @returns {Promise<String>}
216
- */
217
- async function getBlob(hash) {
218
- const objectsPath = await getObjectsPath();
219
- const blobPath = path.join(objectsPath, hash);
220
-
221
- if (!await pathExists(blobPath)) {
222
- return ''; // Return empty string if blob missing (shouldn't happen in healthy repo)
223
- }
224
-
225
- return fs.readFile(blobPath, 'utf-8');
226
- }
227
-
228
178
  module.exports = {
229
179
  pathExists,
230
180
  ensureDir,
@@ -234,7 +184,5 @@ module.exports = {
234
184
  getAllFiles,
235
185
  shouldIgnore,
236
186
  getIgnorePatterns,
237
- getTrackedFiles,
238
- saveBlob,
239
- getBlob
187
+ getTrackedFiles
240
188
  };