@mod-computer/cli 0.2.3 → 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 (75) hide show
  1. package/dist/cli.bundle.js +216 -36371
  2. package/package.json +3 -3
  3. package/dist/app.js +0 -227
  4. package/dist/cli.bundle.js.map +0 -7
  5. package/dist/cli.js +0 -132
  6. package/dist/commands/add.js +0 -245
  7. package/dist/commands/agents-run.js +0 -71
  8. package/dist/commands/auth.js +0 -259
  9. package/dist/commands/branch.js +0 -1411
  10. package/dist/commands/claude-sync.js +0 -772
  11. package/dist/commands/comment.js +0 -568
  12. package/dist/commands/diff.js +0 -182
  13. package/dist/commands/index.js +0 -73
  14. package/dist/commands/init.js +0 -597
  15. package/dist/commands/ls.js +0 -135
  16. package/dist/commands/members.js +0 -687
  17. package/dist/commands/mv.js +0 -282
  18. package/dist/commands/recover.js +0 -207
  19. package/dist/commands/rm.js +0 -257
  20. package/dist/commands/spec.js +0 -386
  21. package/dist/commands/status.js +0 -296
  22. package/dist/commands/sync.js +0 -119
  23. package/dist/commands/trace.js +0 -1752
  24. package/dist/commands/workspace.js +0 -447
  25. package/dist/components/conflict-resolution-ui.js +0 -120
  26. package/dist/components/messages.js +0 -5
  27. package/dist/components/thread.js +0 -8
  28. package/dist/config/features.js +0 -83
  29. package/dist/containers/branches-container.js +0 -140
  30. package/dist/containers/directory-container.js +0 -92
  31. package/dist/containers/thread-container.js +0 -214
  32. package/dist/containers/threads-container.js +0 -27
  33. package/dist/containers/workspaces-container.js +0 -27
  34. package/dist/daemon/conflict-resolution.js +0 -172
  35. package/dist/daemon/content-hash.js +0 -31
  36. package/dist/daemon/file-sync.js +0 -985
  37. package/dist/daemon/index.js +0 -203
  38. package/dist/daemon/mime-types.js +0 -166
  39. package/dist/daemon/offline-queue.js +0 -211
  40. package/dist/daemon/path-utils.js +0 -64
  41. package/dist/daemon/share-policy.js +0 -83
  42. package/dist/daemon/wasm-errors.js +0 -189
  43. package/dist/daemon/worker.js +0 -557
  44. package/dist/daemon-worker.js +0 -258
  45. package/dist/errors/workspace-errors.js +0 -48
  46. package/dist/lib/auth-server.js +0 -216
  47. package/dist/lib/browser.js +0 -35
  48. package/dist/lib/diff.js +0 -284
  49. package/dist/lib/formatters.js +0 -204
  50. package/dist/lib/git.js +0 -137
  51. package/dist/lib/local-fs.js +0 -201
  52. package/dist/lib/prompts.js +0 -56
  53. package/dist/lib/storage.js +0 -213
  54. package/dist/lib/trace-formatters.js +0 -314
  55. package/dist/services/add-service.js +0 -554
  56. package/dist/services/add-validation.js +0 -124
  57. package/dist/services/automatic-file-tracker.js +0 -303
  58. package/dist/services/cli-orchestrator.js +0 -227
  59. package/dist/services/feature-flags.js +0 -187
  60. package/dist/services/file-import-service.js +0 -283
  61. package/dist/services/file-transformation-service.js +0 -218
  62. package/dist/services/logger.js +0 -44
  63. package/dist/services/mod-config.js +0 -67
  64. package/dist/services/modignore-service.js +0 -328
  65. package/dist/services/sync-daemon.js +0 -244
  66. package/dist/services/thread-notification-service.js +0 -50
  67. package/dist/services/thread-service.js +0 -147
  68. package/dist/stores/use-directory-store.js +0 -96
  69. package/dist/stores/use-threads-store.js +0 -46
  70. package/dist/stores/use-workspaces-store.js +0 -54
  71. package/dist/types/add-types.js +0 -99
  72. package/dist/types/config.js +0 -16
  73. package/dist/types/index.js +0 -2
  74. package/dist/types/workspace-connection.js +0 -53
  75. 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
- }