@mod-computer/cli 0.2.4 → 0.2.5

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.
Files changed (74) hide show
  1. package/package.json +3 -3
  2. package/dist/app.js +0 -227
  3. package/dist/cli.bundle.js.map +0 -7
  4. package/dist/cli.js +0 -132
  5. package/dist/commands/add.js +0 -245
  6. package/dist/commands/agents-run.js +0 -71
  7. package/dist/commands/auth.js +0 -259
  8. package/dist/commands/branch.js +0 -1411
  9. package/dist/commands/claude-sync.js +0 -772
  10. package/dist/commands/comment.js +0 -568
  11. package/dist/commands/diff.js +0 -182
  12. package/dist/commands/index.js +0 -73
  13. package/dist/commands/init.js +0 -597
  14. package/dist/commands/ls.js +0 -135
  15. package/dist/commands/members.js +0 -687
  16. package/dist/commands/mv.js +0 -282
  17. package/dist/commands/recover.js +0 -207
  18. package/dist/commands/rm.js +0 -257
  19. package/dist/commands/spec.js +0 -386
  20. package/dist/commands/status.js +0 -296
  21. package/dist/commands/sync.js +0 -119
  22. package/dist/commands/trace.js +0 -1752
  23. package/dist/commands/workspace.js +0 -447
  24. package/dist/components/conflict-resolution-ui.js +0 -120
  25. package/dist/components/messages.js +0 -5
  26. package/dist/components/thread.js +0 -8
  27. package/dist/config/features.js +0 -83
  28. package/dist/containers/branches-container.js +0 -140
  29. package/dist/containers/directory-container.js +0 -92
  30. package/dist/containers/thread-container.js +0 -214
  31. package/dist/containers/threads-container.js +0 -27
  32. package/dist/containers/workspaces-container.js +0 -27
  33. package/dist/daemon/conflict-resolution.js +0 -172
  34. package/dist/daemon/content-hash.js +0 -31
  35. package/dist/daemon/file-sync.js +0 -985
  36. package/dist/daemon/index.js +0 -203
  37. package/dist/daemon/mime-types.js +0 -166
  38. package/dist/daemon/offline-queue.js +0 -211
  39. package/dist/daemon/path-utils.js +0 -64
  40. package/dist/daemon/share-policy.js +0 -83
  41. package/dist/daemon/wasm-errors.js +0 -189
  42. package/dist/daemon/worker.js +0 -557
  43. package/dist/daemon-worker.js +0 -258
  44. package/dist/errors/workspace-errors.js +0 -48
  45. package/dist/lib/auth-server.js +0 -216
  46. package/dist/lib/browser.js +0 -35
  47. package/dist/lib/diff.js +0 -284
  48. package/dist/lib/formatters.js +0 -204
  49. package/dist/lib/git.js +0 -137
  50. package/dist/lib/local-fs.js +0 -201
  51. package/dist/lib/prompts.js +0 -56
  52. package/dist/lib/storage.js +0 -213
  53. package/dist/lib/trace-formatters.js +0 -314
  54. package/dist/services/add-service.js +0 -554
  55. package/dist/services/add-validation.js +0 -124
  56. package/dist/services/automatic-file-tracker.js +0 -303
  57. package/dist/services/cli-orchestrator.js +0 -227
  58. package/dist/services/feature-flags.js +0 -187
  59. package/dist/services/file-import-service.js +0 -283
  60. package/dist/services/file-transformation-service.js +0 -218
  61. package/dist/services/logger.js +0 -44
  62. package/dist/services/mod-config.js +0 -67
  63. package/dist/services/modignore-service.js +0 -328
  64. package/dist/services/sync-daemon.js +0 -244
  65. package/dist/services/thread-notification-service.js +0 -50
  66. package/dist/services/thread-service.js +0 -147
  67. package/dist/stores/use-directory-store.js +0 -96
  68. package/dist/stores/use-threads-store.js +0 -46
  69. package/dist/stores/use-workspaces-store.js +0 -54
  70. package/dist/types/add-types.js +0 -99
  71. package/dist/types/config.js +0 -16
  72. package/dist/types/index.js +0 -2
  73. package/dist/types/workspace-connection.js +0 -53
  74. package/dist/types.js +0 -1
package/dist/lib/diff.js DELETED
@@ -1,284 +0,0 @@
1
- // glassware[type="implementation", id="impl-cli-fd-diff--22cb6c01", requirements="requirement-cli-fd-diff-text--54d22be4,requirement-cli-fd-diff-binary--4aeb6fda,requirement-cli-fd-diff-automerge--8310553b"]
2
- // spec: packages/mod-cli/specs/file-directory.md
3
- /**
4
- * Extract text content from ModFile content structure
5
- */
6
- // glassware[type="implementation", id="impl-cli-fd-diff-automerge--19e08758", requirements="requirement-cli-fd-diff-automerge--8310553b"]
7
- export function getWorkspaceContent(content) {
8
- if (typeof content === 'string') {
9
- return content;
10
- }
11
- if (content?.text) {
12
- // TextFileContent or CodeFileContent
13
- if (typeof content.text === 'string') {
14
- return content.text;
15
- }
16
- // Automerge Text type - convert to string
17
- if (content.text.toString) {
18
- return content.text.toString();
19
- }
20
- }
21
- return '';
22
- }
23
- /**
24
- * Check if content appears to be binary
25
- */
26
- // glassware[type="implementation", id="impl-cli-fd-diff-binary--80add1e0", requirements="requirement-cli-fd-diff-binary--4aeb6fda"]
27
- export function isBinaryContent(content) {
28
- // Check for null bytes or high proportion of non-printable characters
29
- let nonPrintable = 0;
30
- const checkLength = Math.min(content.length, 8000);
31
- for (let i = 0; i < checkLength; i++) {
32
- const code = content.charCodeAt(i);
33
- if (code === 0) {
34
- return true; // Null byte = definitely binary
35
- }
36
- // Non-printable excluding common whitespace
37
- if (code < 32 && code !== 9 && code !== 10 && code !== 13) {
38
- nonPrintable++;
39
- }
40
- }
41
- return nonPrintable / checkLength > 0.3;
42
- }
43
- /**
44
- * Compute unified diff between two strings
45
- */
46
- // glassware[type="implementation", id="impl-cli-fd-diff-text--9c318f0e", requirements="requirement-cli-fd-diff-text--54d22be4"]
47
- export function computeDiff(oldContent, newContent, contextLines = 3) {
48
- const oldLines = oldContent.split('\n');
49
- const newLines = newContent.split('\n');
50
- // Simple LCS-based diff algorithm
51
- const lcs = computeLCS(oldLines, newLines);
52
- const hunks = [];
53
- let oldIndex = 0;
54
- let newIndex = 0;
55
- let lcsIndex = 0;
56
- let currentHunk = null;
57
- while (oldIndex < oldLines.length || newIndex < newLines.length) {
58
- // Check if current lines match LCS
59
- const matchesLCS = lcsIndex < lcs.length &&
60
- oldIndex < oldLines.length &&
61
- newIndex < newLines.length &&
62
- oldLines[oldIndex] === lcs[lcsIndex] &&
63
- newLines[newIndex] === lcs[lcsIndex];
64
- if (matchesLCS) {
65
- // Context line
66
- if (currentHunk) {
67
- currentHunk.lines.push({
68
- type: 'context',
69
- content: oldLines[oldIndex],
70
- oldLineNumber: oldIndex + 1,
71
- newLineNumber: newIndex + 1,
72
- });
73
- }
74
- oldIndex++;
75
- newIndex++;
76
- lcsIndex++;
77
- }
78
- else {
79
- // Start a new hunk if needed
80
- if (!currentHunk) {
81
- const startOld = Math.max(0, oldIndex - contextLines);
82
- const startNew = Math.max(0, newIndex - contextLines);
83
- currentHunk = {
84
- oldStart: startOld + 1,
85
- oldLines: 0,
86
- newStart: startNew + 1,
87
- newLines: 0,
88
- lines: [],
89
- };
90
- // Add leading context
91
- for (let i = startOld; i < oldIndex; i++) {
92
- currentHunk.lines.push({
93
- type: 'context',
94
- content: oldLines[i],
95
- oldLineNumber: i + 1,
96
- newLineNumber: startNew + (i - startOld) + 1,
97
- });
98
- }
99
- }
100
- // Handle deletions
101
- while (oldIndex < oldLines.length &&
102
- (lcsIndex >= lcs.length || oldLines[oldIndex] !== lcs[lcsIndex])) {
103
- currentHunk.lines.push({
104
- type: 'deletion',
105
- content: oldLines[oldIndex],
106
- oldLineNumber: oldIndex + 1,
107
- });
108
- oldIndex++;
109
- }
110
- // Handle additions
111
- while (newIndex < newLines.length &&
112
- (lcsIndex >= lcs.length || newLines[newIndex] !== lcs[lcsIndex])) {
113
- currentHunk.lines.push({
114
- type: 'addition',
115
- content: newLines[newIndex],
116
- newLineNumber: newIndex + 1,
117
- });
118
- newIndex++;
119
- }
120
- // Check if we should close the hunk (no more changes for a while)
121
- const nextChange = findNextChange(oldLines, newLines, lcs, oldIndex, newIndex, lcsIndex);
122
- if (nextChange > contextLines * 2 || (oldIndex >= oldLines.length && newIndex >= newLines.length)) {
123
- // Add trailing context
124
- const endContext = Math.min(contextLines, oldLines.length - oldIndex);
125
- for (let i = 0; i < endContext; i++) {
126
- if (oldIndex + i < oldLines.length && lcsIndex + i < lcs.length) {
127
- currentHunk.lines.push({
128
- type: 'context',
129
- content: oldLines[oldIndex + i],
130
- oldLineNumber: oldIndex + i + 1,
131
- newLineNumber: newIndex + i + 1,
132
- });
133
- }
134
- }
135
- // Calculate hunk line counts
136
- currentHunk.oldLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'deletion').length;
137
- currentHunk.newLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'addition').length;
138
- hunks.push(currentHunk);
139
- currentHunk = null;
140
- // Skip context we just added
141
- oldIndex += endContext;
142
- newIndex += endContext;
143
- lcsIndex += endContext;
144
- }
145
- }
146
- }
147
- // Close any remaining hunk
148
- if (currentHunk && currentHunk.lines.length > 0) {
149
- currentHunk.oldLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'deletion').length;
150
- currentHunk.newLines = currentHunk.lines.filter(l => l.type === 'context' || l.type === 'addition').length;
151
- hunks.push(currentHunk);
152
- }
153
- return hunks;
154
- }
155
- /**
156
- * Compute LCS (Longest Common Subsequence) of two arrays
157
- */
158
- function computeLCS(a, b) {
159
- const m = a.length;
160
- const n = b.length;
161
- // DP table
162
- const dp = Array(m + 1)
163
- .fill(null)
164
- .map(() => Array(n + 1).fill(0));
165
- for (let i = 1; i <= m; i++) {
166
- for (let j = 1; j <= n; j++) {
167
- if (a[i - 1] === b[j - 1]) {
168
- dp[i][j] = dp[i - 1][j - 1] + 1;
169
- }
170
- else {
171
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
172
- }
173
- }
174
- }
175
- // Backtrack to find LCS
176
- const lcs = [];
177
- let i = m;
178
- let j = n;
179
- while (i > 0 && j > 0) {
180
- if (a[i - 1] === b[j - 1]) {
181
- lcs.unshift(a[i - 1]);
182
- i--;
183
- j--;
184
- }
185
- else if (dp[i - 1][j] > dp[i][j - 1]) {
186
- i--;
187
- }
188
- else {
189
- j--;
190
- }
191
- }
192
- return lcs;
193
- }
194
- /**
195
- * Find distance to next change
196
- */
197
- function findNextChange(oldLines, newLines, lcs, oldIndex, newIndex, lcsIndex) {
198
- let count = 0;
199
- while (oldIndex + count < oldLines.length &&
200
- newIndex + count < newLines.length &&
201
- lcsIndex + count < lcs.length &&
202
- oldLines[oldIndex + count] === lcs[lcsIndex + count] &&
203
- newLines[newIndex + count] === lcs[lcsIndex + count]) {
204
- count++;
205
- }
206
- return count;
207
- }
208
- /**
209
- * Format diff hunks as unified diff string
210
- */
211
- export function formatUnifiedDiff(oldPath, newPath, hunks, color = true) {
212
- if (hunks.length === 0) {
213
- return '';
214
- }
215
- const lines = [];
216
- // Header
217
- const oldHeader = `--- ${oldPath}`;
218
- const newHeader = `+++ ${newPath}`;
219
- lines.push(color ? `\x1b[1m${oldHeader}\x1b[0m` : oldHeader);
220
- lines.push(color ? `\x1b[1m${newHeader}\x1b[0m` : newHeader);
221
- for (const hunk of hunks) {
222
- // Hunk header
223
- const hunkHeader = `@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@`;
224
- lines.push(color ? `\x1b[36m${hunkHeader}\x1b[0m` : hunkHeader);
225
- for (const line of hunk.lines) {
226
- switch (line.type) {
227
- case 'context':
228
- lines.push(` ${line.content}`);
229
- break;
230
- case 'deletion':
231
- const delLine = `-${line.content}`;
232
- lines.push(color ? `\x1b[31m${delLine}\x1b[0m` : delLine);
233
- break;
234
- case 'addition':
235
- const addLine = `+${line.content}`;
236
- lines.push(color ? `\x1b[32m${addLine}\x1b[0m` : addLine);
237
- break;
238
- }
239
- }
240
- }
241
- return lines.join('\n');
242
- }
243
- /**
244
- * Calculate diff statistics
245
- */
246
- export function calculateDiffStats(hunks) {
247
- let additions = 0;
248
- let deletions = 0;
249
- for (const hunk of hunks) {
250
- for (const line of hunk.lines) {
251
- if (line.type === 'addition')
252
- additions++;
253
- if (line.type === 'deletion')
254
- deletions++;
255
- }
256
- }
257
- return { additions, deletions };
258
- }
259
- /**
260
- * Format diffstat summary
261
- */
262
- export function formatDiffStat(files, color = true) {
263
- const lines = [];
264
- let totalAdditions = 0;
265
- let totalDeletions = 0;
266
- const maxPathLen = Math.max(...files.map(f => f.path.length), 20);
267
- for (const file of files) {
268
- const total = file.additions + file.deletions;
269
- const plusStr = '+'.repeat(Math.min(file.additions, 20));
270
- const minusStr = '-'.repeat(Math.min(file.deletions, 20));
271
- let line = ` ${file.path.padEnd(maxPathLen)} | ${String(total).padStart(4)} `;
272
- if (color) {
273
- line += `\x1b[32m${plusStr}\x1b[0m\x1b[31m${minusStr}\x1b[0m`;
274
- }
275
- else {
276
- line += `${plusStr}${minusStr}`;
277
- }
278
- lines.push(line);
279
- totalAdditions += file.additions;
280
- totalDeletions += file.deletions;
281
- }
282
- lines.push(` ${files.length} file${files.length === 1 ? '' : 's'} changed, ${totalAdditions} insertion${totalAdditions === 1 ? '' : 's'}(+), ${totalDeletions} deletion${totalDeletions === 1 ? '' : 's'}(-)`);
283
- return lines.join('\n');
284
- }
@@ -1,204 +0,0 @@
1
- // glassware[type="implementation", id="impl-cli-fd-formatters--faafb955", requirements="requirement-cli-ls-output-default--6e584280,requirement-cli-ls-output-tree--43babfa8,requirement-cli-ls-output-json--d62469fe"]
2
- // spec: packages/mod-cli/specs/file-directory.md
3
- /**
4
- * Format file size in human readable format
5
- */
6
- export function formatSize(bytes) {
7
- if (bytes === 0)
8
- return '0 B';
9
- const units = ['B', 'KB', 'MB', 'GB'];
10
- const k = 1024;
11
- const i = Math.floor(Math.log(bytes) / Math.log(k));
12
- if (i === 0)
13
- return `${bytes} B`;
14
- const size = bytes / Math.pow(k, i);
15
- return `${size.toFixed(1)} ${units[i]}`;
16
- }
17
- /**
18
- * Format relative time
19
- */
20
- export function formatRelativeTime(isoString) {
21
- const date = new Date(isoString);
22
- const now = new Date();
23
- const diffMs = now.getTime() - date.getTime();
24
- const minutes = Math.floor(diffMs / (1000 * 60));
25
- const hours = Math.floor(diffMs / (1000 * 60 * 60));
26
- const days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
27
- if (minutes < 1)
28
- return 'just now';
29
- if (minutes < 60)
30
- return `${minutes} min ago`;
31
- if (hours < 24)
32
- return `${hours} hour${hours === 1 ? '' : 's'} ago`;
33
- if (days < 7)
34
- return `${days} day${days === 1 ? '' : 's'} ago`;
35
- return date.toLocaleDateString();
36
- }
37
- /**
38
- * Format default table output for file list
39
- */
40
- // glassware[type="implementation", id="impl-cli-ls-output-default--8774cf55", requirements="requirement-cli-ls-output-default--6e584280"]
41
- export function formatFileTable(files) {
42
- if (files.length === 0) {
43
- return 'No files in workspace.\n\nRun `mod init` to import files from this directory.';
44
- }
45
- const lines = [];
46
- // Calculate column widths
47
- const pathWidth = Math.max(4, Math.min(50, Math.max(...files.map(f => f.path.length))));
48
- // Header
49
- lines.push(`${'PATH'.padEnd(pathWidth)} ${'SIZE'.padStart(10)} MODIFIED`);
50
- // Rows
51
- for (const file of files) {
52
- const path = file.path.length > pathWidth
53
- ? '...' + file.path.slice(-(pathWidth - 3))
54
- : file.path.padEnd(pathWidth);
55
- const size = formatSize(file.size).padStart(10);
56
- const modified = formatRelativeTime(file.updatedAt);
57
- lines.push(`${path} ${size} ${modified}`);
58
- }
59
- // Summary
60
- const totalSize = files.reduce((sum, f) => sum + f.size, 0);
61
- lines.push('');
62
- lines.push(`${files.length} file${files.length === 1 ? '' : 's'} (${formatSize(totalSize)} total)`);
63
- return lines.join('\n');
64
- }
65
- function buildTree(files) {
66
- const root = { name: '.', isFolder: true, children: new Map() };
67
- for (const file of files) {
68
- const parts = file.path.split('/');
69
- let current = root;
70
- for (let i = 0; i < parts.length; i++) {
71
- const part = parts[i];
72
- const isLast = i === parts.length - 1;
73
- if (!current.children.has(part)) {
74
- current.children.set(part, {
75
- name: part,
76
- isFolder: !isLast,
77
- size: isLast ? file.size : undefined,
78
- children: new Map(),
79
- });
80
- }
81
- current = current.children.get(part);
82
- }
83
- }
84
- return root;
85
- }
86
- /**
87
- * Format tree output
88
- */
89
- // glassware[type="implementation", id="impl-cli-ls-output-tree--c098fff8", requirements="requirement-cli-ls-output-tree--43babfa8"]
90
- export function formatFileTree(files) {
91
- if (files.length === 0) {
92
- return 'No files in workspace.\n\nRun `mod init` to import files from this directory.';
93
- }
94
- const tree = buildTree(files);
95
- const lines = ['.'];
96
- function renderNode(node, prefix, isLast) {
97
- const children = Array.from(node.children.values());
98
- // Sort: folders first, then alphabetically
99
- children.sort((a, b) => {
100
- if (a.isFolder !== b.isFolder)
101
- return a.isFolder ? -1 : 1;
102
- return a.name.localeCompare(b.name);
103
- });
104
- for (let i = 0; i < children.length; i++) {
105
- const child = children[i];
106
- const isLastChild = i === children.length - 1;
107
- const connector = isLastChild ? '\\u2514\\u2500\\u2500 ' : '\\u251c\\u2500\\u2500 ';
108
- const nextPrefix = prefix + (isLastChild ? ' ' : '\\u2502 ');
109
- if (child.isFolder) {
110
- lines.push(`${prefix}${connector}${child.name}/`);
111
- renderNode(child, nextPrefix, isLastChild);
112
- }
113
- else {
114
- const sizeStr = child.size !== undefined ? ` (${formatSize(child.size)})` : '';
115
- lines.push(`${prefix}${connector}${child.name}${sizeStr}`);
116
- }
117
- }
118
- }
119
- renderNode(tree, '', true);
120
- return lines.join('\n')
121
- .replace(/\\u2514/g, '\u2514')
122
- .replace(/\\u2500/g, '\u2500')
123
- .replace(/\\u251c/g, '\u251c')
124
- .replace(/\\u2502/g, '\u2502');
125
- }
126
- /**
127
- * Format JSON output
128
- */
129
- // glassware[type="implementation", id="impl-cli-ls-output-json--75e73ac5", requirements="requirement-cli-ls-output-json--d62469fe"]
130
- export function formatFileJson(files) {
131
- const totalSize = files.reduce((sum, f) => sum + f.size, 0);
132
- const output = {
133
- files: files.map(f => ({
134
- path: f.path,
135
- size: f.size,
136
- mimeType: f.mimeType,
137
- updatedAt: f.updatedAt,
138
- })),
139
- totalFiles: files.length,
140
- totalSize,
141
- };
142
- return JSON.stringify(output, null, 2);
143
- }
144
- /**
145
- * Format file paths only (quiet mode)
146
- */
147
- export function formatFilePaths(files) {
148
- return files.map(f => f.path).join('\n');
149
- }
150
- /**
151
- * Format long/detailed output
152
- */
153
- export function formatFileLong(files) {
154
- if (files.length === 0) {
155
- return 'No files in workspace.';
156
- }
157
- const lines = [];
158
- // Header
159
- lines.push('ID PATH SIZE MIME TYPE');
160
- for (const file of files) {
161
- const id = file.id.slice(0, 36).padEnd(36);
162
- const path = file.path.length > 32
163
- ? '...' + file.path.slice(-29)
164
- : file.path.padEnd(32);
165
- const size = formatSize(file.size).padStart(10);
166
- const mime = file.mimeType;
167
- lines.push(`${id} ${path} ${size} ${mime}`);
168
- }
169
- return lines.join('\n');
170
- }
171
- /**
172
- * Determine MIME type category
173
- */
174
- export function getMimeCategory(mimeType) {
175
- if (mimeType.startsWith('text/')) {
176
- if (mimeType.includes('typescript') ||
177
- mimeType.includes('javascript') ||
178
- mimeType.includes('python') ||
179
- mimeType.includes('java') ||
180
- mimeType.includes('c') ||
181
- mimeType.includes('rust') ||
182
- mimeType.includes('go')) {
183
- return 'code';
184
- }
185
- return 'text';
186
- }
187
- if (mimeType.startsWith('application/json') ||
188
- mimeType.includes('yaml') ||
189
- mimeType.includes('xml') ||
190
- mimeType.includes('csv')) {
191
- return 'data';
192
- }
193
- if (mimeType.includes('javascript') ||
194
- mimeType.includes('typescript')) {
195
- return 'code';
196
- }
197
- if (mimeType.startsWith('image/') ||
198
- mimeType.startsWith('audio/') ||
199
- mimeType.startsWith('video/') ||
200
- mimeType === 'application/octet-stream') {
201
- return 'binary';
202
- }
203
- return 'unknown';
204
- }
package/dist/lib/git.js DELETED
@@ -1,137 +0,0 @@
1
- // glassware[type="implementation", id="impl-cli-git-utils--7e4f29ca", requirements="requirement-cli-git-app-1--f5a33b8c,requirement-cli-git-app-2--4f3b552c,requirement-cli-git-qual-1--98d1bacb,requirement-cli-git-qual-2--503addb9,requirement-cli-git-qual-3--5af20852,requirement-cli-git-qual-4--7c40baf1"]
2
- import fs from 'fs';
3
- import path from 'path';
4
- /**
5
- * Detect if a directory is inside a git repository.
6
- * Walks up the directory tree looking for a .git directory.
7
- */
8
- // glassware[type="implementation", id="impl-cli-detect-git-repo--86f0b85f", specifications="specification-spec-is-git-repo--8d217687"]
9
- export function detectGitRepo(directory) {
10
- let currentDir = path.resolve(directory);
11
- const root = path.parse(currentDir).root;
12
- while (currentDir !== root) {
13
- const gitDir = path.join(currentDir, '.git');
14
- if (fs.existsSync(gitDir)) {
15
- const headFile = path.join(gitDir, 'HEAD');
16
- const branchResult = readCurrentBranchWithDetached(headFile);
17
- return {
18
- isGitRepo: true,
19
- branch: branchResult.branch,
20
- gitDir,
21
- headFile,
22
- isDetached: branchResult.isDetached,
23
- };
24
- }
25
- currentDir = path.dirname(currentDir);
26
- }
27
- return {
28
- isGitRepo: false,
29
- branch: null,
30
- gitDir: null,
31
- headFile: null,
32
- isDetached: false,
33
- };
34
- }
35
- /**
36
- * Read the current branch name from .git/HEAD with detached state info.
37
- * Returns branch name and whether in detached HEAD state.
38
- */
39
- // glassware[type="implementation", id="impl-cli-read-branch-detached--42ec893d", specifications="specification-spec-detect-branch--76e466f2,specification-spec-detached-head--0fce4d44"]
40
- export function readCurrentBranchWithDetached(headFile) {
41
- try {
42
- if (!fs.existsSync(headFile)) {
43
- return { branch: null, isDetached: false };
44
- }
45
- const content = fs.readFileSync(headFile, 'utf8').trim();
46
- // Check if it's a symbolic ref (normal branch)
47
- // Format: "ref: refs/heads/branch-name"
48
- if (content.startsWith('ref: refs/heads/')) {
49
- return {
50
- branch: content.replace('ref: refs/heads/', ''),
51
- isDetached: false,
52
- };
53
- }
54
- // Detached HEAD (commit hash) - return detached-{hash} format per spec
55
- if (/^[0-9a-f]{40}$/i.test(content)) {
56
- return {
57
- branch: `detached-${content.substring(0, 7)}`,
58
- isDetached: true,
59
- };
60
- }
61
- return { branch: null, isDetached: false };
62
- }
63
- catch (error) {
64
- return { branch: null, isDetached: false };
65
- }
66
- }
67
- /**
68
- * Read the current branch name from .git/HEAD.
69
- * Returns null if detached HEAD or unable to read.
70
- */
71
- export function readCurrentBranch(headFile) {
72
- return readCurrentBranchWithDetached(headFile).branch;
73
- }
74
- /**
75
- * Get git branch for a directory.
76
- * Convenience function that returns just the branch name or null.
77
- */
78
- export function getGitBranch(directory) {
79
- const gitInfo = detectGitRepo(directory);
80
- return gitInfo.branch;
81
- }
82
- /**
83
- * Check if a path should be ignored based on git patterns.
84
- * This is a simplified check for common patterns.
85
- */
86
- export function isGitIgnoredPath(relativePath) {
87
- // Always ignore .git directory contents
88
- if (relativePath.startsWith('.git/') || relativePath === '.git') {
89
- return true;
90
- }
91
- // Ignore common patterns
92
- const ignoredPatterns = [
93
- /^node_modules\//,
94
- /^\.git\//,
95
- /^dist\//,
96
- /^build\//,
97
- /^\.next\//,
98
- /^\.nuxt\//,
99
- /^coverage\//,
100
- /^\.cache\//,
101
- /^\.automerge-data\//,
102
- /\.log$/,
103
- /\.lock$/,
104
- /^\.DS_Store$/,
105
- /^Thumbs\.db$/,
106
- ];
107
- return ignoredPatterns.some(pattern => pattern.test(relativePath));
108
- }
109
- /**
110
- * Check if a git operation is in progress (rebase, merge, etc).
111
- * When active git operations are in progress, syncing should be paused.
112
- */
113
- // glassware[type="implementation", id="impl-cli-git-operation-check--c31739c8", specifications="specification-spec-git-operations--addf801f"]
114
- export function isGitOperationInProgress(directory) {
115
- const gitInfo = detectGitRepo(directory);
116
- if (!gitInfo.isGitRepo || !gitInfo.gitDir) {
117
- return false;
118
- }
119
- const gitDir = gitInfo.gitDir;
120
- // Check for various git operations in progress
121
- const operationIndicators = [
122
- path.join(gitDir, 'rebase-merge'), // git rebase in progress
123
- path.join(gitDir, 'rebase-apply'), // git am or rebase --apply in progress
124
- path.join(gitDir, 'MERGE_HEAD'), // git merge in progress
125
- path.join(gitDir, 'CHERRY_PICK_HEAD'), // git cherry-pick in progress
126
- path.join(gitDir, 'REVERT_HEAD'), // git revert in progress
127
- path.join(gitDir, 'BISECT_LOG'), // git bisect in progress
128
- ];
129
- return operationIndicators.some(indicator => fs.existsSync(indicator));
130
- }
131
- /**
132
- * Check if the .git directory exists and is valid.
133
- */
134
- export function isGitRepo(directory) {
135
- const gitInfo = detectGitRepo(directory);
136
- return gitInfo.isGitRepo;
137
- }