@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
@@ -1,1411 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { createModWorkspace } from '@mod/mod-core/mod-workspace';
4
- import { readModConfig, writeModConfig } from '../services/mod-config.js';
5
- import { AutomaticFileTracker } from '../services/automatic-file-tracker.js';
6
- import { SyncDaemon } from '../services/sync-daemon.js';
7
- class MyersDiffCalculator {
8
- static calculateDiff(baseline, current) {
9
- const baselineLines = baseline.split('\n');
10
- const currentLines = current.split('\n');
11
- // Early exit optimization for identical content
12
- if (baseline === current) {
13
- return { linesAdded: 0, linesRemoved: 0, changes: [] };
14
- }
15
- // Simplified Myers algorithm implementation
16
- const changes = this.computeChanges(baselineLines, currentLines);
17
- let linesAdded = 0;
18
- let linesRemoved = 0;
19
- for (const change of changes) {
20
- if (change.type === 'add')
21
- linesAdded++;
22
- if (change.type === 'remove')
23
- linesRemoved++;
24
- }
25
- return { linesAdded, linesRemoved, changes };
26
- }
27
- static computeChanges(baseline, current) {
28
- const changes = [];
29
- const lcs = this.longestCommonSubsequence(baseline, current);
30
- let baselineIndex = 0;
31
- let currentIndex = 0;
32
- let lcsIndex = 0;
33
- while (baselineIndex < baseline.length || currentIndex < current.length) {
34
- if (lcsIndex < lcs.length &&
35
- baselineIndex < baseline.length &&
36
- baseline[baselineIndex] === lcs[lcsIndex] &&
37
- currentIndex < current.length &&
38
- current[currentIndex] === lcs[lcsIndex]) {
39
- // Lines match - unchanged
40
- changes.push({
41
- type: 'unchanged',
42
- line: baseline[baselineIndex],
43
- lineNumber: baselineIndex + 1
44
- });
45
- baselineIndex++;
46
- currentIndex++;
47
- lcsIndex++;
48
- }
49
- else if (currentIndex < current.length &&
50
- (lcsIndex >= lcs.length || current[currentIndex] !== lcs[lcsIndex])) {
51
- // Line added in current
52
- changes.push({
53
- type: 'add',
54
- line: current[currentIndex],
55
- lineNumber: currentIndex + 1
56
- });
57
- currentIndex++;
58
- }
59
- else if (baselineIndex < baseline.length &&
60
- (lcsIndex >= lcs.length || baseline[baselineIndex] !== lcs[lcsIndex])) {
61
- // Line removed from baseline
62
- changes.push({
63
- type: 'remove',
64
- line: baseline[baselineIndex],
65
- lineNumber: baselineIndex + 1
66
- });
67
- baselineIndex++;
68
- }
69
- }
70
- return changes;
71
- }
72
- static longestCommonSubsequence(a, b) {
73
- const m = a.length;
74
- const n = b.length;
75
- const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));
76
- // Build LCS table
77
- for (let i = 1; i <= m; i++) {
78
- for (let j = 1; j <= n; j++) {
79
- if (a[i - 1] === b[j - 1]) {
80
- dp[i][j] = dp[i - 1][j - 1] + 1;
81
- }
82
- else {
83
- dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
84
- }
85
- }
86
- }
87
- // Backtrack to find LCS
88
- const lcs = [];
89
- let i = m, j = n;
90
- while (i > 0 && j > 0) {
91
- if (a[i - 1] === b[j - 1]) {
92
- lcs.unshift(a[i - 1]);
93
- i--;
94
- j--;
95
- }
96
- else if (dp[i - 1][j] > dp[i][j - 1]) {
97
- i--;
98
- }
99
- else {
100
- j--;
101
- }
102
- }
103
- return lcs;
104
- }
105
- }
106
- class FileFilter {
107
- constructor() {
108
- this.ignorePatterns = [];
109
- this.loadIgnorePatterns();
110
- }
111
- loadIgnorePatterns() {
112
- const patterns = [
113
- // Common build artifacts that should be excluded
114
- '*.js', '*.d.ts', '*.js.map', // TypeScript build outputs
115
- 'node_modules/**',
116
- 'dist/**', 'build/**',
117
- '.cache/**',
118
- '*.log',
119
- '.DS_Store',
120
- 'coverage/**'
121
- ];
122
- // Load .gitignore if exists
123
- const gitignorePath = path.join(process.cwd(), '.gitignore');
124
- if (fs.existsSync(gitignorePath)) {
125
- try {
126
- const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
127
- const gitignorePatterns = gitignoreContent
128
- .split('\n')
129
- .map(line => line.trim())
130
- .filter(line => line && !line.startsWith('#'));
131
- patterns.push(...gitignorePatterns);
132
- }
133
- catch (error) {
134
- console.warn('Could not read .gitignore:', error);
135
- }
136
- }
137
- // Load .modignore if exists
138
- const modignorePath = path.join(process.cwd(), '.modignore');
139
- if (fs.existsSync(modignorePath)) {
140
- try {
141
- const modignoreContent = fs.readFileSync(modignorePath, 'utf8');
142
- const modignorePatterns = modignoreContent
143
- .split('\n')
144
- .map(line => line.trim())
145
- .filter(line => line && !line.startsWith('#'));
146
- patterns.push(...modignorePatterns);
147
- }
148
- catch (error) {
149
- console.warn('Could not read .modignore:', error);
150
- }
151
- }
152
- this.ignorePatterns = patterns;
153
- }
154
- isSourceFile(filePath) {
155
- // Check if file should be ignored
156
- if (this.shouldIgnoreFile(filePath)) {
157
- return false;
158
- }
159
- // Check if it's a source file type
160
- const sourceExtensions = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h', '.hpp', '.md', '.yaml', '.yml', '.json'];
161
- const ext = path.extname(filePath).toLowerCase();
162
- // Special case: exclude .js/.d.ts files if corresponding .ts file exists
163
- if (ext === '.js' || ext === '.d.ts') {
164
- const tsFile = filePath.replace(/\.(js|d\.ts)$/, '.ts');
165
- if (fs.existsSync(tsFile)) {
166
- return false; // This is a build artifact
167
- }
168
- }
169
- return sourceExtensions.includes(ext);
170
- }
171
- shouldIgnoreFile(filePath) {
172
- const normalizedPath = filePath.replace(/\\/g, '/');
173
- return this.ignorePatterns.some(pattern => {
174
- // Simple glob pattern matching
175
- if (pattern.includes('**')) {
176
- const regex = new RegExp(pattern.replace(/\*\*/g, '.*').replace(/\*/g, '[^/]*'));
177
- return regex.test(normalizedPath);
178
- }
179
- else if (pattern.includes('*')) {
180
- const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
181
- return regex.test(path.basename(normalizedPath));
182
- }
183
- else {
184
- return normalizedPath.includes(pattern) || path.basename(normalizedPath) === pattern;
185
- }
186
- });
187
- }
188
- filterSourceFiles(files) {
189
- return files.filter(file => {
190
- const filePath = file.path || file.name || '';
191
- return this.isSourceFile(filePath);
192
- });
193
- }
194
- }
195
- function generateBranchName(input) {
196
- return input
197
- .toLowerCase()
198
- .replace(/[^a-z0-9\s-]/g, '') // Remove special chars except spaces and hyphens
199
- .replace(/\s+/g, '-') // Replace spaces with hyphens
200
- .replace(/-+/g, '-') // Collapse multiple hyphens
201
- .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
202
- .substring(0, 50); // Limit length
203
- }
204
- export async function branchCommand(args, repo) {
205
- const cfg = readModConfig();
206
- if (!cfg?.workspaceId) {
207
- console.error('No active workspace configured in .mod/config.json');
208
- process.exit(1);
209
- }
210
- const modWorkspace = createModWorkspace(repo);
211
- let workspaceHandle;
212
- try {
213
- workspaceHandle = await openExistingWorkspace(modWorkspace, cfg.workspaceId);
214
- }
215
- catch (error) {
216
- console.error('Failed to initialize workspace:', error);
217
- console.log('Try running: mod sync-workspace to repair workspace state');
218
- process.exit(1);
219
- }
220
- const [subcommand, ...rest] = args;
221
- switch (subcommand) {
222
- case 'create':
223
- await handleCreateBranch(workspaceHandle, rest);
224
- break;
225
- case 'switch':
226
- await handleSwitchBranch(workspaceHandle, rest);
227
- break;
228
- case 'list':
229
- await handleListBranches(workspaceHandle, rest);
230
- break;
231
- case 'status':
232
- await handleBranchStatus(workspaceHandle, rest);
233
- break;
234
- case 'coverage':
235
- await handleCoverageAnalysis(workspaceHandle, rest);
236
- break;
237
- case 'tree':
238
- await handleTreeView(workspaceHandle, rest);
239
- break;
240
- case 'auto-track':
241
- await handleAutoTrack(workspaceHandle, rest, repo);
242
- return; // Don't exit when auto-track is running
243
- default:
244
- await handleListBranches(workspaceHandle, []);
245
- }
246
- process.exit(0);
247
- }
248
- function detectBranchType(branchName, hasSpecFlag, hasLightweightFlag) {
249
- // Explicit flags override auto-detection
250
- if (hasSpecFlag)
251
- return 'specification';
252
- if (hasLightweightFlag)
253
- return 'lightweight';
254
- const lightweightPrefixes = ['fix-', 'hotfix-', 'patch-', 'deps-', 'chore-'];
255
- if (lightweightPrefixes.some(prefix => branchName.startsWith(prefix))) {
256
- return 'lightweight';
257
- }
258
- const specPrefixes = ['feature-', 'feat-', 'refactor-', 'implement-'];
259
- if (specPrefixes.some(prefix => branchName.startsWith(prefix))) {
260
- return 'specification';
261
- }
262
- return 'lightweight';
263
- }
264
- async function handleCreateBranch(workspaceHandle, args) {
265
- if (args.length === 0) {
266
- console.error('Usage: mod branch create <branch-name> [--from <parent-branch>] [--spec <spec-path>] [--lightweight]');
267
- process.exit(1);
268
- }
269
- const branchName = args[0];
270
- const fromIndex = args.indexOf('--from');
271
- const specIndex = args.indexOf('--spec');
272
- const lightweightFlag = args.includes('--lightweight');
273
- const fromBranch = fromIndex >= 0 && fromIndex + 1 < args.length ? args[fromIndex + 1] : undefined;
274
- const specPath = specIndex >= 0 && specIndex + 1 < args.length ? args[specIndex + 1] : undefined;
275
- const branchType = detectBranchType(branchName, !!specPath, lightweightFlag);
276
- try {
277
- const typeIndicator = branchType === 'specification' ? 'specification' : 'lightweight';
278
- console.log(`Creating ${typeIndicator} branch: ${branchName}${fromBranch ? ` from ${fromBranch}` : ''}...`);
279
- const createOptions = {
280
- name: branchName,
281
- fromBranch: fromBranch,
282
- spec: (branchType === 'specification' && specPath) ? {
283
- file: specPath,
284
- title: branchName
285
- } : undefined
286
- };
287
- const branch = await workspaceHandle.branch.create(createOptions);
288
- console.log(`[DEBUG] Created branch: ${branch.name} (${branch.id})`);
289
- await workspaceHandle.branch.switchActive(branch.id);
290
- console.log(`[DEBUG] Switched to branch: ${branch.id}`);
291
- // Wait a moment and verify the branch appears in the list
292
- const branches = await workspaceHandle.branch.list();
293
- console.log(`[DEBUG] Branches after creation:`, branches.map((b) => `${b.name} (${b.id})`));
294
- const createdBranchExists = branches.find((b) => b.id === branch.id);
295
- console.log(`[DEBUG] Created branch exists in list: ${!!createdBranchExists}`);
296
- // Add delay and force persistence to ensure branch is saved
297
- await new Promise(resolve => setTimeout(resolve, 500));
298
- const cfg = readModConfig();
299
- const updatedConfig = {
300
- ...cfg,
301
- workspaceId: cfg?.workspaceId,
302
- activeBranchId: branch.id,
303
- lastSpecificationPath: specPath
304
- };
305
- writeModConfig(updatedConfig);
306
- console.log(`✓ Created and switched to ${typeIndicator} branch: ${branchName} (${branch.id})`);
307
- if (specPath && branchType === 'specification') {
308
- console.log(`✓ Linked to specification: ${specPath}`);
309
- }
310
- else if (branchType === 'lightweight') {
311
- console.log(`💡 Lightweight branch - no specification tracking required`);
312
- }
313
- }
314
- catch (error) {
315
- console.error('Failed to create branch:', error);
316
- console.log('Try running: mod branches list --wait to check workspace sync status');
317
- process.exit(1);
318
- }
319
- }
320
- async function handleSwitchBranch(workspaceHandle, args) {
321
- if (args.length === 0) {
322
- console.error('Usage: mod branch switch <branch-name>');
323
- process.exit(1);
324
- }
325
- const targetBranchName = args[0];
326
- try {
327
- const branches = await workspaceHandle.branch.list();
328
- const targetBranch = branches.find((b) => b.name === targetBranchName || b.id === targetBranchName);
329
- if (!targetBranch) {
330
- console.error(`Branch not found: ${targetBranchName}`);
331
- console.log('Available branches:');
332
- branches.forEach((b) => console.log(` ${b.name} (${b.id})`));
333
- process.exit(1);
334
- }
335
- await workspaceHandle.branch.switchActive(targetBranch.id);
336
- const cfg = readModConfig();
337
- const updatedConfig = {
338
- ...cfg,
339
- workspaceId: cfg?.workspaceId,
340
- activeBranchId: targetBranch.id
341
- };
342
- writeModConfig(updatedConfig);
343
- console.log(`✓ Switched to branch: ${targetBranch.name} (${targetBranch.id})`);
344
- }
345
- catch (error) {
346
- console.error('Failed to switch branch:', error);
347
- process.exit(1);
348
- }
349
- }
350
- async function handleListBranches(workspaceHandle, args) {
351
- try {
352
- const branches = await workspaceHandle.branch.list();
353
- const activeBranch = await workspaceHandle.branch.getActive();
354
- if (branches.length === 0) {
355
- console.log('No branches found.');
356
- return;
357
- }
358
- console.log('Available branches:');
359
- branches.forEach((branch) => {
360
- const isActive = activeBranch && branch.id === activeBranch.id;
361
- const indicator = isActive ? '* ' : ' ';
362
- const parent = branch.parentBranchId ? ` ← ${branch.parentBranchId}` : '';
363
- const branchType = detectBranchType(branch.name, false, false);
364
- const typeIndicator = branchType === 'specification' ? '[spec]' : '[lite]';
365
- console.log(`${indicator}${branch.name} ${typeIndicator} (${branch.id})${parent}`);
366
- });
367
- }
368
- catch (error) {
369
- const errorMessage = String(error?.message || error || '');
370
- // Handle specific Automerge corruption errors
371
- if (errorMessage.includes('corrupt deflate stream') || errorMessage.includes('unable to parse chunk')) {
372
- console.error('❌ Document loading error during branch listing');
373
- console.error('Error details:', errorMessage);
374
- // Try to extract the problematic document ID from the error stack
375
- const cfg = readModConfig();
376
- if (cfg?.workspaceId) {
377
- console.log(`\n📍 Workspace ID: ${cfg.workspaceId}`);
378
- console.log('💡 The issue is likely with the branches document referenced by this workspace');
379
- }
380
- console.log('\n🔧 Recovery options:');
381
- console.log(' 1. Reset workspace: `mod recover workspace`');
382
- console.log(' 2. Try switching to a different workspace');
383
- console.log(' 3. Check for stale document references');
384
- console.log('\n💡 This error indicates a stale or invalid document reference, not file corruption');
385
- process.exit(1);
386
- }
387
- console.error('Failed to list branches:', error);
388
- process.exit(1);
389
- }
390
- }
391
- async function getFileChangesSummary(workspaceHandle, branch) {
392
- const fileFilter = new FileFilter();
393
- try {
394
- const currentFiles = await workspaceHandle.file.list();
395
- const filteredCurrentFiles = fileFilter.filterSourceFiles(currentFiles);
396
- // Get baseline from parent branch or empty if this is root
397
- let parentFiles = [];
398
- if (branch.parentBranchId) {
399
- // Temporarily switch to parent to get its files
400
- const currentBranchId = branch.id;
401
- await workspaceHandle.branch.switchActive(branch.parentBranchId);
402
- const allParentFiles = await workspaceHandle.file.list();
403
- parentFiles = fileFilter.filterSourceFiles(allParentFiles);
404
- // Switch back
405
- await workspaceHandle.branch.switchActive(currentBranchId);
406
- }
407
- const parentFileMap = new Map(parentFiles.map((f) => [f.name, f]));
408
- const currentFileMap = new Map(filteredCurrentFiles.map((f) => [f.name, f]));
409
- const newFiles = [];
410
- const modifiedFiles = [];
411
- const deletedFiles = [];
412
- let totalLinesAdded = 0;
413
- let totalLinesRemoved = 0;
414
- // Process new and modified files
415
- for (const currentFile of filteredCurrentFiles) {
416
- const parentFile = parentFileMap.get(currentFile.name);
417
- if (!parentFile) {
418
- const currentContent = await getFileContent(workspaceHandle, currentFile, currentFile.name);
419
- const lines = currentContent ? currentContent.split('\n').length : 0;
420
- newFiles.push({
421
- name: currentFile.name,
422
- path: currentFile.path || currentFile.name,
423
- status: 'A',
424
- linesAdded: lines,
425
- linesRemoved: 0,
426
- isSourceFile: true
427
- });
428
- totalLinesAdded += lines;
429
- }
430
- else {
431
- // Compare content for modifications
432
- const currentContent = await getFileContent(workspaceHandle, currentFile, currentFile.name);
433
- const parentContent = await getFileContent(workspaceHandle, parentFile, parentFile.name);
434
- if (currentContent !== parentContent) {
435
- const diffResult = MyersDiffCalculator.calculateDiff(parentContent || '', currentContent || '');
436
- modifiedFiles.push({
437
- name: currentFile.name,
438
- path: currentFile.path || currentFile.name,
439
- status: 'M',
440
- linesAdded: diffResult.linesAdded,
441
- linesRemoved: diffResult.linesRemoved,
442
- isSourceFile: true
443
- });
444
- totalLinesAdded += diffResult.linesAdded;
445
- totalLinesRemoved += diffResult.linesRemoved;
446
- }
447
- }
448
- }
449
- // Process deleted files
450
- for (const parentFile of parentFiles) {
451
- if (!currentFileMap.has(parentFile.name)) {
452
- const parentContent = await getFileContent(workspaceHandle, parentFile, parentFile.name);
453
- const lines = parentContent ? parentContent.split('\n').length : 0;
454
- deletedFiles.push({
455
- name: parentFile.name,
456
- path: parentFile.path || parentFile.name,
457
- status: 'D',
458
- linesAdded: 0,
459
- linesRemoved: lines,
460
- isSourceFile: true
461
- });
462
- totalLinesRemoved += lines;
463
- }
464
- }
465
- return {
466
- totalFiles: filteredCurrentFiles.length,
467
- totalChanges: newFiles.length + modifiedFiles.length + deletedFiles.length,
468
- totalLinesAdded,
469
- totalLinesRemoved,
470
- newFiles,
471
- modifiedFiles,
472
- deletedFiles,
473
- branchCreatedAt: branch.createdAt || 'unknown'
474
- };
475
- }
476
- catch (error) {
477
- console.warn('Error getting file changes:', error);
478
- return {
479
- totalFiles: 0,
480
- totalChanges: 0,
481
- totalLinesAdded: 0,
482
- totalLinesRemoved: 0,
483
- newFiles: [],
484
- modifiedFiles: [],
485
- deletedFiles: [],
486
- branchCreatedAt: branch.createdAt || 'unknown'
487
- };
488
- }
489
- }
490
- async function handleBranchStatus(workspaceHandle, args) {
491
- try {
492
- // Read active branch from CLI config first, then fall back to workspace default
493
- const cfg = readModConfig();
494
- let activeBranch = null;
495
- if (cfg?.activeBranchId) {
496
- // Find the branch by ID from CLI config
497
- const branches = await workspaceHandle.branch.list();
498
- activeBranch = branches.find((b) => b.id === cfg.activeBranchId);
499
- }
500
- // Fall back to workspace's default active branch if not found in config
501
- if (!activeBranch) {
502
- activeBranch = await workspaceHandle.branch.getActive();
503
- }
504
- if (!activeBranch) {
505
- console.log('No active branch');
506
- return;
507
- }
508
- console.log(`Active branch: ${activeBranch.name} (${activeBranch.id})`);
509
- if (activeBranch.parentBranchId) {
510
- console.log(`Parent branch: ${activeBranch.parentBranchId}`);
511
- }
512
- const changes = await getFileChangesSummary(workspaceHandle, activeBranch);
513
- console.log(`\nFile changes since branch creation:`);
514
- if (changes.totalChanges === 0) {
515
- console.log(' No changes detected');
516
- }
517
- else {
518
- changes.newFiles.forEach(file => {
519
- console.log(` A +${file.linesAdded}/-${file.linesRemoved} ${file.path}`);
520
- });
521
- changes.modifiedFiles.forEach(file => {
522
- console.log(` M +${file.linesAdded}/-${file.linesRemoved} ${file.path}`);
523
- });
524
- changes.deletedFiles.forEach(file => {
525
- console.log(` D +${file.linesAdded}/-${file.linesRemoved} ${file.path}`);
526
- });
527
- console.log(`\nTotal: ${changes.totalChanges} files changed, ${changes.totalLinesAdded} insertions(+), ${changes.totalLinesRemoved} deletions(-)`);
528
- }
529
- const files = await workspaceHandle.file.list();
530
- const trackedFiles = files || [];
531
- console.log(`\nTracked files: ${trackedFiles.length}`);
532
- const daemon = new SyncDaemon();
533
- const daemonStatus = await daemon.status();
534
- console.log('\n📡 Sync Daemon Status:');
535
- if (daemonStatus && daemonStatus.status === 'running') {
536
- const uptimeSeconds = daemonStatus.uptime ? Math.floor(daemonStatus.uptime / 1000) : 0;
537
- const minutes = Math.floor(uptimeSeconds / 60);
538
- const seconds = uptimeSeconds % 60;
539
- console.log(` 🟢 Running (${minutes}m ${seconds}s) - Watching ${daemonStatus.watchedFiles} files`);
540
- console.log(` 📊 Last activity: ${new Date(daemonStatus.lastActivity).toLocaleString()}`);
541
- if (daemonStatus.crashCount > 0) {
542
- console.log(` ⚠️ Crashes: ${daemonStatus.crashCount}`);
543
- }
544
- }
545
- else if (daemonStatus && daemonStatus.status === 'crashed') {
546
- console.log(` 💥 Crashed - ${daemonStatus.lastError || 'Unknown error'}`);
547
- console.log(' 💡 Run `mod sync restart` to restart daemon');
548
- }
549
- else {
550
- console.log(' 🔴 Not running');
551
- console.log(' 💡 Run `mod sync start` to enable real-time file tracking');
552
- }
553
- console.log('\n💡 File synchronization:');
554
- if (daemonStatus && daemonStatus.status === 'running') {
555
- console.log(' - Changes are being automatically synced to this branch in real-time');
556
- console.log(' - Use `mod sync status` for detailed daemon information');
557
- }
558
- else {
559
- console.log(' - Run `mod sync start` to enable background sync daemon');
560
- console.log(' - Changes will be automatically synced to this branch');
561
- }
562
- console.log(' - Use `mod branch auto-track status` for legacy tracking details');
563
- }
564
- catch (error) {
565
- console.error('Failed to get branch status:', error);
566
- process.exit(1);
567
- }
568
- }
569
- async function handleCoverageAnalysis(workspaceHandle, args) {
570
- try {
571
- const activeBranch = await workspaceHandle.branch.getActive();
572
- if (!activeBranch) {
573
- console.error('No active branch found');
574
- process.exit(1);
575
- }
576
- // Get files modified in current branch
577
- const changes = await getFileChangesSummary(workspaceHandle, activeBranch);
578
- const modifiedFiles = [...changes.newFiles, ...changes.modifiedFiles];
579
- if (modifiedFiles.length === 0) {
580
- console.log('No files modified in current branch - no coverage analysis needed');
581
- return;
582
- }
583
- console.log(`Analyzing ${modifiedFiles.length} modified files for @spec traceability...`);
584
- const cacheDir = path.join(process.cwd(), '.mod', '.cache');
585
- const cachePath = path.join(cacheDir, 'coverage.json');
586
- // Ensure cache directory exists
587
- if (!fs.existsSync(cacheDir)) {
588
- fs.mkdirSync(cacheDir, { recursive: true });
589
- }
590
- let totalSpecComments = 0;
591
- let filesWithSpecs = 0;
592
- const specReferences = [];
593
- const invalidRefs = [];
594
- const fileDetails = [];
595
- for (const fileObj of modifiedFiles) {
596
- try {
597
- const fileName = fileObj.name;
598
- const filePath = fileObj.path || fileName;
599
- // Skip non-code files
600
- if (!isCodeFile(fileName))
601
- continue;
602
- const content = await getFileContent(workspaceHandle, fileObj, filePath);
603
- if (!content) {
604
- fileDetails.push({
605
- path: filePath,
606
- specComments: 0,
607
- invalidRefs: 0,
608
- functions: 0,
609
- classes: 0,
610
- untraced: 0,
611
- hasCode: false
612
- });
613
- continue;
614
- }
615
- const analysis = analyzeFileForSpecComments(content, filePath);
616
- const codeStructure = analyzeCodeStructure(content);
617
- const fileInvalidRefs = analysis.invalidRefs.length;
618
- const fileSpecComments = analysis.specComments.length;
619
- if (fileSpecComments > 0) {
620
- filesWithSpecs++;
621
- totalSpecComments += fileSpecComments;
622
- specReferences.push(...analysis.specComments.map(spec => spec.requirementId));
623
- invalidRefs.push(...analysis.invalidRefs);
624
- }
625
- fileDetails.push({
626
- path: filePath,
627
- specComments: fileSpecComments,
628
- invalidRefs: fileInvalidRefs,
629
- functions: codeStructure.functions,
630
- classes: codeStructure.classes,
631
- untraced: Math.max(0, (codeStructure.functions + codeStructure.classes) - fileSpecComments),
632
- hasCode: analysis.hasCode
633
- });
634
- }
635
- catch (error) {
636
- console.warn(`Could not analyze file: ${fileObj.name}`);
637
- fileDetails.push({
638
- path: fileObj.name,
639
- specComments: 0,
640
- invalidRefs: 0,
641
- functions: 0,
642
- classes: 0,
643
- untraced: 0,
644
- hasCode: false
645
- });
646
- }
647
- }
648
- // Only count code files among the modified files
649
- const totalCodeFiles = fileDetails.filter(f => f.hasCode).length;
650
- const untracedFiles = fileDetails.filter(f => f.hasCode && f.specComments === 0);
651
- const coveragePercentage = totalCodeFiles > 0 ?
652
- Math.round((filesWithSpecs / totalCodeFiles) * 100) : 0;
653
- console.log(`\nCoverage Analysis Results (Branch: ${activeBranch.name || activeBranch.branchId}):`);
654
- console.log(` Modified code files analyzed: ${totalCodeFiles}`);
655
- console.log(` Files with @spec comments: ${filesWithSpecs}`);
656
- console.log(` Total @spec comments found: ${totalSpecComments}`);
657
- console.log(` Coverage percentage: ${coveragePercentage}%`);
658
- console.log(`\nPer-file traceability analysis:`);
659
- fileDetails
660
- .filter(f => f.hasCode)
661
- .sort((a, b) => b.specComments - a.specComments)
662
- .forEach(file => {
663
- const status = file.specComments > 0 ? '✓' : '✗';
664
- const tracedIndicator = file.untraced > 0 ? ` (${file.untraced} untraced)` : '';
665
- const invalidIndicator = file.invalidRefs > 0 ? ` ⚠️${file.invalidRefs}` : '';
666
- console.log(` ${status} ${file.path} - ${file.specComments} traces, ${file.functions}f/${file.classes}c${tracedIndicator}${invalidIndicator}`);
667
- });
668
- if (invalidRefs.length > 0) {
669
- console.log(`\nInvalid @spec references found:`);
670
- invalidRefs.forEach(ref => console.log(` ⚠️ ${ref}`));
671
- }
672
- if (untracedFiles.length > 0) {
673
- console.log(`\nFiles lacking @spec traceability (${untracedFiles.length}):`);
674
- untracedFiles.forEach(file => {
675
- console.log(` - ${file.path} (${file.functions} functions, ${file.classes} classes)`);
676
- });
677
- }
678
- const analyzedFiles = {};
679
- fileDetails.forEach(file => {
680
- analyzedFiles[file.path] = {
681
- specReferences: [], // Would need detailed parsing for individual refs
682
- lastModified: new Date(),
683
- hash: '', // Would need content hash
684
- linesAdded: 0, // From file change summary
685
- linesRemoved: 0, // From file change summary
686
- isSourceFile: file.hasCode
687
- };
688
- });
689
- const cacheData = {
690
- specificationPath: readModConfig()?.lastSpecificationPath || '',
691
- lastAnalysis: new Date(),
692
- analyzedFiles,
693
- coverageResults: {
694
- totalRequirements: totalSpecComments,
695
- implementedRequirements: [...new Set(specReferences)],
696
- unimplementedRequirements: untracedFiles.map(f => f.path),
697
- coveragePercentage
698
- },
699
- unspecifiedFiles: untracedFiles.map(f => f.path)
700
- };
701
- fs.writeFileSync(cachePath, JSON.stringify(cacheData, null, 2));
702
- }
703
- catch (error) {
704
- console.error('Failed to analyze coverage:', error);
705
- process.exit(1);
706
- }
707
- }
708
- async function scanWorkingDirectoryForCodeFiles() {
709
- const files = [];
710
- const cwd = process.cwd();
711
- try {
712
- const scanDir = (dir) => {
713
- const items = fs.readdirSync(dir);
714
- for (const item of items) {
715
- const fullPath = path.join(dir, item);
716
- const relativePath = path.relative(cwd, fullPath);
717
- // Skip node_modules, .git, and other common excludes
718
- if (item.startsWith('.') || item === 'node_modules')
719
- continue;
720
- const stat = fs.statSync(fullPath);
721
- if (stat.isDirectory()) {
722
- scanDir(fullPath);
723
- }
724
- else if (isCodeFile(item)) {
725
- files.push({
726
- name: item,
727
- path: relativePath,
728
- fullPath
729
- });
730
- }
731
- }
732
- };
733
- scanDir(cwd);
734
- }
735
- catch (error) {
736
- console.warn('Error scanning working directory:', error);
737
- }
738
- return files;
739
- }
740
- function isCodeFile(fileName) {
741
- const fileFilter = new FileFilter();
742
- return fileFilter.isSourceFile(fileName);
743
- }
744
- async function getFileContent(workspaceHandle, file, filePath) {
745
- try {
746
- // Try to get from workspace first
747
- if (file.id) {
748
- const docHandle = await workspaceHandle.file.get(file.id);
749
- if (docHandle?.head?.content) {
750
- return docHandle.head.content;
751
- }
752
- }
753
- // Fallback to filesystem
754
- if (file.fullPath && fs.existsSync(file.fullPath)) {
755
- return fs.readFileSync(file.fullPath, 'utf8');
756
- }
757
- // Try relative path from cwd
758
- const fullPath = path.resolve(process.cwd(), filePath);
759
- if (fs.existsSync(fullPath)) {
760
- return fs.readFileSync(fullPath, 'utf8');
761
- }
762
- // Try searching for the file in common locations
763
- const commonPaths = [
764
- path.resolve(process.cwd(), `packages/mod-cli/source/commands/${file.name}`),
765
- path.resolve(process.cwd(), `packages/mod-cli/source/${file.name}`),
766
- path.resolve(process.cwd(), `packages/mod-cli/${file.name}`),
767
- path.resolve(process.cwd(), `src/${file.name}`),
768
- path.resolve(process.cwd(), `source/${file.name}`)
769
- ];
770
- for (const tryPath of commonPaths) {
771
- if (fs.existsSync(tryPath)) {
772
- return fs.readFileSync(tryPath, 'utf8');
773
- }
774
- }
775
- return null;
776
- }
777
- catch (error) {
778
- return null;
779
- }
780
- }
781
- function analyzeFileForSpecComments(content, filePath) {
782
- const lines = content.split('\n');
783
- const specComments = [];
784
- const invalidRefs = [];
785
- let hasCode = false;
786
- const specCommentRegex = /\/\/\s*@spec\s+([^\s]+)\s+(.+)/i;
787
- for (let i = 0; i < lines.length; i++) {
788
- const line = lines[i].trim();
789
- // Check if line has actual code (not just comments/whitespace)
790
- if (line && !line.startsWith('//') && !line.startsWith('/*') && !line.startsWith('*')) {
791
- hasCode = true;
792
- }
793
- const match = line.match(specCommentRegex);
794
- if (match) {
795
- const [, requirementId, description] = match;
796
- const isValid = validateSpecReference(requirementId);
797
- if (isValid) {
798
- specComments.push({
799
- requirementId,
800
- lineNumber: i + 1,
801
- description: description.trim(),
802
- valid: true
803
- });
804
- }
805
- else {
806
- invalidRefs.push(`${filePath}:${i + 1} - Invalid ref: ${requirementId}`);
807
- specComments.push({
808
- requirementId,
809
- lineNumber: i + 1,
810
- description: description.trim(),
811
- valid: false
812
- });
813
- }
814
- }
815
- }
816
- return {
817
- specComments,
818
- invalidRefs,
819
- hasCode
820
- };
821
- }
822
- function analyzeCodeStructure(content) {
823
- const lines = content.split('\n');
824
- let functions = 0;
825
- let classes = 0;
826
- for (const line of lines) {
827
- const trimmedLine = line.trim();
828
- // Skip comments
829
- if (trimmedLine.startsWith('//') || trimmedLine.startsWith('/*') || trimmedLine.startsWith('*')) {
830
- continue;
831
- }
832
- // Count function declarations (various patterns)
833
- if (/^(export\s+)?(async\s+)?function\s+\w+/.test(trimmedLine) ||
834
- /^\w+\s*:\s*(async\s+)?\([^)]*\)\s*=>/.test(trimmedLine) ||
835
- /^(async\s+)?\w+\s*\([^)]*\)\s*\{/.test(trimmedLine) ||
836
- /^\w+\s*=\s*(async\s+)?\([^)]*\)\s*=>/.test(trimmedLine)) {
837
- functions++;
838
- }
839
- // Count class declarations
840
- if (/^(export\s+)?(abstract\s+)?class\s+\w+/.test(trimmedLine)) {
841
- classes++;
842
- }
843
- // Count interface declarations (treat as classes for traceability purposes)
844
- if (/^(export\s+)?interface\s+\w+/.test(trimmedLine)) {
845
- classes++;
846
- }
847
- }
848
- return { functions, classes };
849
- }
850
- function validateSpecReference(requirementId) {
851
- // Basic validation - should be in format: spec-name/REQ-CATEGORY-NUMBER or spec-name.spec.md/REQ-CATEGORY-NUMBER
852
- const specRefPattern = /^[a-zA-Z0-9\-_]+(\.spec\.md)?\/REQ-[A-Z]+-\d+(\.\d+)*$/;
853
- return specRefPattern.test(requirementId);
854
- }
855
- async function handleAutoTrack(workspaceHandle, args, repo) {
856
- const [action] = args;
857
- if (!action || !['start', 'stop', 'status'].includes(action)) {
858
- console.error('Usage: mod branch auto-track <start|stop|status>');
859
- console.error(' start - Enable automatic file tracking');
860
- console.error(' stop - Disable automatic file tracking');
861
- console.error(' status - Show tracking status');
862
- process.exit(1);
863
- }
864
- try {
865
- const cfg = readModConfig();
866
- if (!cfg?.workspaceId) {
867
- throw new Error('No workspace configured');
868
- }
869
- const tracker = new AutomaticFileTracker(repo);
870
- switch (action) {
871
- case 'start':
872
- console.log('🚀 Starting automatic file tracking...');
873
- await tracker.enableAutoTracking({
874
- workspaceId: cfg.workspaceId,
875
- workspaceHandle: workspaceHandle,
876
- watchDirectory: process.cwd(),
877
- debounceMs: 500,
878
- verbose: true
879
- });
880
- console.log('✅ Automatic file tracking is now active!');
881
- console.log('💡 Edit any file in this directory and it will be automatically tracked.');
882
- console.log('💡 Run "mod branch status" to see changes.');
883
- console.log('💡 Press Ctrl+C to stop tracking and exit.');
884
- // Keep the process running to maintain file watching
885
- process.on('SIGINT', async () => {
886
- console.log('\n🛑 Stopping automatic file tracking...');
887
- await tracker.disableAutoTracking();
888
- process.exit(0);
889
- });
890
- // Don't exit the process - keep watching
891
- await new Promise(() => { }); // Wait indefinitely
892
- break;
893
- case 'stop':
894
- await tracker.disableAutoTracking();
895
- console.log('🛑 Automatic file tracking stopped');
896
- break;
897
- case 'status':
898
- const status = tracker.getTrackingStatus();
899
- console.log(`Tracking status: ${status.isTracking ? '🟢 Active' : '🔴 Inactive'}`);
900
- console.log(`Tracked files: ${status.trackedFiles}`);
901
- break;
902
- }
903
- }
904
- catch (error) {
905
- console.error('Failed to manage auto-tracking:', error);
906
- process.exit(1);
907
- }
908
- }
909
- async function openExistingWorkspace(modWorkspace, workspaceId) {
910
- // This function provides a proper way to open existing workspaces until openWorkspace is added to ModWorkspace
911
- // We temporarily access the internal method to create a handle, but this maintains the interface boundary
912
- const repo = modWorkspace.repo;
913
- const wsHandle = await repo.find(workspaceId);
914
- const workspace = await wsHandle.doc();
915
- if (!workspace || !workspace.branchesDocId) {
916
- throw new Error('Invalid workspace: missing branchesDocId');
917
- }
918
- const handle = modWorkspace.createWorkspaceHandle(workspaceId, workspace.title || 'Workspace', workspace.branchesDocId);
919
- const cfg = readModConfig();
920
- if (cfg?.activeBranchId) {
921
- try {
922
- await handle.branch.switchActive(cfg.activeBranchId);
923
- }
924
- catch (error) {
925
- console.warn(`Could not restore saved active branch ${cfg.activeBranchId}:`, error);
926
- }
927
- }
928
- return handle;
929
- }
930
- export function generateBranchNameForSpec(specTitle, packageName) {
931
- const baseName = generateBranchName(specTitle);
932
- if (packageName) {
933
- const packageSlug = generateBranchName(packageName);
934
- return `feature-${packageSlug}-${baseName}`;
935
- }
936
- return `feature-${baseName}`;
937
- }
938
- export async function createBranchForSpec(repo, specTitle) {
939
- const branchName = generateBranchNameForSpec(specTitle);
940
- // Use existing branch creation logic
941
- await branchCommand(['create', branchName, '--spec', `.mod/specs/${specTitle}.spec.md`], repo);
942
- return { branchName, branchId: branchName }; // Simplified return
943
- }
944
- export function getActiveBranchSpec() {
945
- const cfg = readModConfig();
946
- return cfg?.lastSpecificationPath || null;
947
- }
948
- export async function switchToSpecBranch(repo, specFileName) {
949
- const baseSpecName = specFileName.replace(/\.spec\.md$/, '');
950
- // Extract package name from path like "mod-cli/branching-cli.spec.md"
951
- let packageName;
952
- let specTitle = baseSpecName;
953
- if (baseSpecName.includes('/')) {
954
- const parts = baseSpecName.split('/');
955
- packageName = parts[0];
956
- specTitle = parts[1] || parts[0];
957
- }
958
- const expectedBranchName = generateBranchNameForSpec(specTitle, packageName);
959
- try {
960
- // Use existing branch switch logic
961
- await branchCommand(['switch', expectedBranchName], repo);
962
- return { success: true, branchName: expectedBranchName };
963
- }
964
- catch (error) {
965
- // Branch doesn't exist, create it
966
- try {
967
- await branchCommand(['create', expectedBranchName, '--spec', `.mod/specs/${specFileName}`], repo);
968
- return { success: true, branchName: expectedBranchName };
969
- }
970
- catch (createError) {
971
- console.error(`Failed to create/switch to branch for ${specFileName}:`, createError);
972
- return { success: false, branchName: expectedBranchName };
973
- }
974
- }
975
- }
976
- async function handleTreeView(workspaceHandle, args) {
977
- try {
978
- const options = parseTreeCommandOptions(args);
979
- const activeBranch = await workspaceHandle.branch.getActive();
980
- if (!activeBranch) {
981
- console.error('No active branch. Run \'mod branch list\' to see available branches');
982
- process.exit(1);
983
- }
984
- const files = await workspaceHandle.file.list();
985
- const filteredFiles = filterSystemFiles(files);
986
- if (filteredFiles.length === 0) {
987
- console.log(`Branch '${activeBranch.name}': 0 files`);
988
- console.log('(empty branch)');
989
- return;
990
- }
991
- const changesSummary = await getFileChangesSummary(workspaceHandle, activeBranch);
992
- let displayFiles = filteredFiles;
993
- if (options.changesOnly) {
994
- const changedFilePaths = new Set([
995
- ...changesSummary.newFiles.map(f => f.path),
996
- ...changesSummary.modifiedFiles.map(f => f.path),
997
- ...changesSummary.deletedFiles.map(f => f.path)
998
- ]);
999
- displayFiles = filteredFiles.filter(file => changedFilePaths.has(file.path || file.name));
1000
- if (displayFiles.length === 0) {
1001
- console.log(`Branch '${activeBranch.name}': no changes to display`);
1002
- return;
1003
- }
1004
- }
1005
- const rootNode = await buildTreeStructure(displayFiles, workspaceHandle, changesSummary, options);
1006
- const context = await createDisplayContext(activeBranch, workspaceHandle, rootNode, changesSummary);
1007
- displayTreeHeader(context, options);
1008
- displayTreeNodes(rootNode.children, '', options, context);
1009
- }
1010
- catch (error) {
1011
- if (error.message?.includes('Branch not found')) {
1012
- console.error('Branch not found. Run \'mod branch list\' to see available branches');
1013
- }
1014
- else if (error.message?.includes('timeout')) {
1015
- console.error('Tree generation timed out. Try using --depth or --filter to limit scope');
1016
- }
1017
- else {
1018
- console.error('Failed to generate tree view:', error.message);
1019
- }
1020
- process.exit(1);
1021
- }
1022
- }
1023
- function parseTreeCommandOptions(args) {
1024
- const options = {};
1025
- for (let i = 0; i < args.length; i++) {
1026
- const arg = args[i];
1027
- switch (arg) {
1028
- case '--depth':
1029
- if (i + 1 < args.length) {
1030
- const depthValue = parseInt(args[i + 1]);
1031
- if (!isNaN(depthValue) && depthValue > 0) {
1032
- options.depth = depthValue;
1033
- i++; // Skip next argument
1034
- }
1035
- else {
1036
- console.error('Invalid depth value. Must be a positive number.');
1037
- process.exit(1);
1038
- }
1039
- }
1040
- break;
1041
- case '--dirs-only':
1042
- options.dirsOnly = true;
1043
- break;
1044
- case '--filter':
1045
- if (i + 1 < args.length) {
1046
- options.filter = args[i + 1];
1047
- i++; // Skip next argument
1048
- }
1049
- break;
1050
- case '--size':
1051
- options.showSize = true;
1052
- break;
1053
- case '--changes-only':
1054
- options.changesOnly = true;
1055
- break;
1056
- case '--stat':
1057
- options.showStats = true;
1058
- break;
1059
- case '--no-color':
1060
- options.noColor = true;
1061
- break;
1062
- case '--help':
1063
- displayTreeHelp();
1064
- process.exit(0);
1065
- break;
1066
- default:
1067
- if (arg.startsWith('--')) {
1068
- console.error(`Unknown option: ${arg}`);
1069
- console.error('Use --help for usage information');
1070
- process.exit(1);
1071
- }
1072
- }
1073
- }
1074
- if (options.dirsOnly && options.showStats) {
1075
- console.error('Cannot use --dirs-only with --stat (directories have no line counts)');
1076
- process.exit(1);
1077
- }
1078
- if (options.filter) {
1079
- try {
1080
- // Simple validation - ensure no invalid regex characters
1081
- new RegExp(options.filter.replace(/\*/g, '.*').replace(/\?/g, '.'));
1082
- }
1083
- catch {
1084
- console.error(`Invalid filter pattern: ${options.filter}`);
1085
- console.error('Use standard glob patterns like "*.ts" or "src/**"');
1086
- process.exit(1);
1087
- }
1088
- }
1089
- return options;
1090
- }
1091
- function displayTreeHelp() {
1092
- console.log('Usage: mod branch tree [options]');
1093
- console.log('');
1094
- console.log('Display directory tree of current branch');
1095
- console.log('');
1096
- console.log('Options:');
1097
- console.log(' --depth <N> Limit tree depth to N levels');
1098
- console.log(' --dirs-only Show only directory structure, no files');
1099
- console.log(' --filter <pattern> Show only paths matching glob pattern');
1100
- console.log(' --size Include file sizes in human-readable format');
1101
- console.log(' --changes-only Show only files with changes in current branch');
1102
- console.log(' --stat Include line change counts (+N/-M) for modified files');
1103
- console.log(' --filesystem Show filesystem files instead of workspace files');
1104
- console.log(' --no-color Disable color output');
1105
- console.log(' --help Show this help message');
1106
- console.log('');
1107
- console.log('Examples:');
1108
- console.log(' mod branch tree --depth 3');
1109
- console.log(' mod branch tree --filter "*.ts"');
1110
- console.log(' mod branch tree --changes-only --stat');
1111
- console.log(' mod branch tree --filesystem # Show all files in current directory');
1112
- }
1113
- function filterSystemFiles(files) {
1114
- return files.filter(file => {
1115
- const fileName = file.name || '';
1116
- // Skip automerge internal files
1117
- if (fileName.startsWith('automerge-') || fileName.includes('.automerge')) {
1118
- return false;
1119
- }
1120
- // Skip system files
1121
- if (fileName.startsWith('.') && fileName !== '.gitignore' && fileName !== '.modignore') {
1122
- return false;
1123
- }
1124
- // For workspace files, use simpler extension-based filtering since we don't have filesystem context
1125
- const ext = path.extname(fileName).toLowerCase();
1126
- const allowedExtensions = ['.ts', '.tsx', '.js', '.jsx', '.vue', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h', '.hpp', '.md', '.yaml', '.yml', '.json', '.txt'];
1127
- return allowedExtensions.includes(ext) || fileName === '.gitignore' || fileName === '.modignore';
1128
- });
1129
- }
1130
- async function buildTreeStructure(files, workspaceHandle, changesSummary, options) {
1131
- // Create root node
1132
- const root = {
1133
- name: '',
1134
- path: '',
1135
- type: 'directory',
1136
- children: [],
1137
- metadata: {}
1138
- };
1139
- let filteredFiles = files;
1140
- if (options.filter) {
1141
- const pattern = options.filter.replace(/\*/g, '.*').replace(/\?/g, '.');
1142
- const regex = new RegExp(pattern, 'i');
1143
- filteredFiles = files.filter(file => regex.test(file.path || file.name));
1144
- }
1145
- // Create change status lookup for efficient access
1146
- const changeStatusMap = new Map();
1147
- [...changesSummary.newFiles, ...changesSummary.modifiedFiles, ...changesSummary.deletedFiles].forEach(change => {
1148
- changeStatusMap.set(change.path, {
1149
- status: change.status,
1150
- stats: { additions: change.linesAdded, deletions: change.linesRemoved }
1151
- });
1152
- });
1153
- // Get folder information to reconstruct paths for hierarchical files
1154
- const folderMap = new Map();
1155
- try {
1156
- const folders = await workspaceHandle.folder.list();
1157
- folders.forEach((folder) => {
1158
- if (folder.folderId) {
1159
- // Map logical folder ID to folder data
1160
- folderMap.set(folder.folderId, folder);
1161
- }
1162
- });
1163
- }
1164
- catch (error) {
1165
- // Silently continue if folder list fails - fallback will handle path reconstruction
1166
- }
1167
- for (const file of filteredFiles) {
1168
- // For hierarchical files, reconstruct path from folder information
1169
- let filePath = file.path || file.name;
1170
- if (file.folderId && !file.path) {
1171
- // Look up folder info to reconstruct path
1172
- const folder = folderMap.get(file.folderId);
1173
- if (folder && folder.folderPath) {
1174
- filePath = folder.folderPath + '/' + file.name;
1175
- }
1176
- else {
1177
- // Fallback: parse folder ID to extract path
1178
- const folderIdMatch = file.folderId.match(/folder-(.+)-\d+$/);
1179
- if (folderIdMatch) {
1180
- const folderPath = folderIdMatch[1].replace(/-/g, '/');
1181
- filePath = folderPath + '/' + file.name;
1182
- }
1183
- }
1184
- }
1185
- const pathParts = filePath.split('/').filter(part => part.length > 0);
1186
- let currentNode = root;
1187
- // Create directory structure
1188
- for (let i = 0; i < pathParts.length - 1; i++) {
1189
- const dirName = pathParts[i];
1190
- const dirPath = pathParts.slice(0, i + 1).join('/');
1191
- let dirNode = currentNode.children.find(child => child.name === dirName && child.type === 'directory');
1192
- if (!dirNode) {
1193
- dirNode = {
1194
- name: dirName,
1195
- path: dirPath,
1196
- type: 'directory',
1197
- children: [],
1198
- metadata: {}
1199
- };
1200
- currentNode.children.push(dirNode);
1201
- }
1202
- currentNode = dirNode;
1203
- }
1204
- if (!options.dirsOnly) {
1205
- // Add file node
1206
- const fileName = pathParts[pathParts.length - 1];
1207
- const changeInfo = changeStatusMap.get(filePath);
1208
- const fileNode = {
1209
- name: fileName,
1210
- path: filePath,
1211
- type: 'file',
1212
- children: [],
1213
- metadata: {
1214
- fileId: file.id,
1215
- changeStatus: changeInfo?.status,
1216
- changeStats: changeInfo?.stats,
1217
- mimeType: getMimeType(fileName)
1218
- }
1219
- };
1220
- if (options.showSize) {
1221
- fileNode.metadata.size = await getFileSize(workspaceHandle, file);
1222
- }
1223
- currentNode.children.push(fileNode);
1224
- }
1225
- }
1226
- sortTreeNodes(root);
1227
- return root;
1228
- }
1229
- function sortTreeNodes(node) {
1230
- node.children.sort((a, b) => {
1231
- // Files before directories at root level
1232
- if (a.type !== b.type) {
1233
- if (node.path === '') { // Root level
1234
- return a.type === 'file' ? -1 : 1;
1235
- }
1236
- else { // Nested levels - directories first
1237
- return a.type === 'directory' ? -1 : 1;
1238
- }
1239
- }
1240
- // Alphabetical within same type
1241
- return a.name.localeCompare(b.name);
1242
- });
1243
- // Recursively sort children
1244
- node.children.forEach(child => {
1245
- if (child.type === 'directory') {
1246
- sortTreeNodes(child);
1247
- }
1248
- });
1249
- }
1250
- async function createDisplayContext(branch, workspaceHandle, rootNode, changesSummary) {
1251
- // Count files and directories recursively
1252
- const counts = countFilesAndDirectories(rootNode);
1253
- return {
1254
- branchName: branch.name,
1255
- branchId: branch.id,
1256
- totalFiles: counts.files,
1257
- totalDirectories: counts.directories,
1258
- changeSummary: {
1259
- modified: changesSummary.modifiedFiles.length,
1260
- added: changesSummary.newFiles.length,
1261
- deleted: changesSummary.deletedFiles.length
1262
- },
1263
- workspaceName: 'Workspace', // Could be enhanced to get actual workspace name
1264
- specificationPath: detectBranchType(branch.name, false, false) === 'specification' ?
1265
- `${branch.name}.spec.md` : undefined
1266
- };
1267
- }
1268
- // Helper function to count files and directories
1269
- function countFilesAndDirectories(node) {
1270
- let files = 0;
1271
- let directories = 0;
1272
- for (const child of node.children) {
1273
- if (child.type === 'file') {
1274
- files++;
1275
- }
1276
- else if (child.type === 'directory') {
1277
- directories++;
1278
- const childCounts = countFilesAndDirectories(child);
1279
- files += childCounts.files;
1280
- directories += childCounts.directories;
1281
- }
1282
- }
1283
- return { files, directories };
1284
- }
1285
- function displayTreeHeader(context, options) {
1286
- console.log(`Branch '${context.branchName}': ${context.totalFiles} files`);
1287
- if (context.specificationPath) {
1288
- console.log(`Specification: ${context.specificationPath}`);
1289
- }
1290
- if (context.changeSummary.added > 0 || context.changeSummary.modified > 0 || context.changeSummary.deleted > 0) {
1291
- const changes = [];
1292
- if (context.changeSummary.added > 0)
1293
- changes.push(`${context.changeSummary.added} added`);
1294
- if (context.changeSummary.modified > 0)
1295
- changes.push(`${context.changeSummary.modified} modified`);
1296
- if (context.changeSummary.deleted > 0)
1297
- changes.push(`${context.changeSummary.deleted} deleted`);
1298
- console.log(`Changes: ${changes.join(', ')}`);
1299
- }
1300
- console.log('');
1301
- }
1302
- function displayTreeNodes(nodes, prefix, options, context, currentDepth = 0) {
1303
- if (options.depth && currentDepth >= options.depth) {
1304
- return;
1305
- }
1306
- for (let i = 0; i < nodes.length; i++) {
1307
- const node = nodes[i];
1308
- const isLast = i === nodes.length - 1;
1309
- const connector = isLast ? '└── ' : '├── ';
1310
- const newPrefix = prefix + (isLast ? ' ' : '│ ');
1311
- displaySingleNode(node, prefix + connector, options, context);
1312
- if (node.type === 'directory') {
1313
- if (node.children.length === 0) {
1314
- console.log(prefix + (isLast ? ' ' : '│ ') + '[empty]');
1315
- }
1316
- else {
1317
- displayTreeNodes(node.children, newPrefix, options, context, currentDepth + 1);
1318
- }
1319
- }
1320
- }
1321
- }
1322
- function displaySingleNode(node, prefix, options, context) {
1323
- let output = prefix;
1324
- if (node.metadata.changeStatus) {
1325
- const colorCode = !options.noColor ? getStatusColor(node.metadata.changeStatus) : '';
1326
- const resetCode = !options.noColor ? '\x1b[0m' : '';
1327
- output += `${colorCode}${node.metadata.changeStatus}${resetCode} `;
1328
- }
1329
- const nameColor = !options.noColor ? getFileTypeColor(node.metadata.mimeType) : '';
1330
- const resetColor = !options.noColor ? '\x1b[0m' : '';
1331
- const displayName = node.type === 'directory' ? `${node.name}/` : node.name;
1332
- output += `${nameColor}${displayName}${resetColor}`;
1333
- if (options.showSize && node.metadata.size && node.type === 'file') {
1334
- output += ` (${formatFileSize(node.metadata.size)})`;
1335
- }
1336
- if (options.showStats && node.metadata.changeStats && node.type === 'file') {
1337
- const stats = node.metadata.changeStats;
1338
- output += ` (+${stats.additions}/-${stats.deletions})`;
1339
- }
1340
- console.log(output);
1341
- }
1342
- function getStatusColor(status) {
1343
- switch (status) {
1344
- case 'A': return '\x1b[32m'; // Green for added
1345
- case 'M': return '\x1b[33m'; // Yellow for modified
1346
- case 'D': return '\x1b[31m'; // Red for deleted
1347
- default: return '';
1348
- }
1349
- }
1350
- function getFileTypeColor(mimeType) {
1351
- if (!mimeType)
1352
- return '';
1353
- if (mimeType.includes('code') || mimeType.includes('typescript') || mimeType.includes('javascript')) {
1354
- return '\x1b[34m'; // Blue for code
1355
- }
1356
- else if (mimeType.includes('text') || mimeType.includes('markdown')) {
1357
- return '\x1b[32m'; // Green for docs
1358
- }
1359
- else if (mimeType.includes('json') || mimeType.includes('yaml') || mimeType.includes('config')) {
1360
- return '\x1b[33m'; // Yellow for config
1361
- }
1362
- return '';
1363
- }
1364
- function formatFileSize(bytes) {
1365
- if (bytes < 1024)
1366
- return `${bytes}B`;
1367
- if (bytes < 1024 * 1024)
1368
- return `${Math.round(bytes / 1024)}KB`;
1369
- return `${Math.round(bytes / (1024 * 1024))}MB`;
1370
- }
1371
- // Simple MIME type detection for file type coloring
1372
- function getMimeType(fileName) {
1373
- const ext = path.extname(fileName).toLowerCase();
1374
- switch (ext) {
1375
- case '.ts':
1376
- case '.tsx': return 'code/typescript';
1377
- case '.js':
1378
- case '.jsx': return 'code/javascript';
1379
- case '.py': return 'code/python';
1380
- case '.go': return 'code/go';
1381
- case '.rs': return 'code/rust';
1382
- case '.java': return 'code/java';
1383
- case '.md': return 'text/markdown';
1384
- case '.json': return 'config/json';
1385
- case '.yaml':
1386
- case '.yml': return 'config/yaml';
1387
- case '.txt': return 'text/plain';
1388
- default: return 'application/octet-stream';
1389
- }
1390
- }
1391
- // Get file size from workspace handle or filesystem
1392
- async function getFileSize(workspaceHandle, file) {
1393
- try {
1394
- if (file.id) {
1395
- const docHandle = await workspaceHandle.file.get(file.id);
1396
- if (docHandle?.head?.content) {
1397
- return Buffer.byteLength(docHandle.head.content, 'utf8');
1398
- }
1399
- }
1400
- // Fallback to filesystem if available
1401
- const filePath = file.fullPath || file.path;
1402
- if (filePath && fs.existsSync(filePath)) {
1403
- const stats = fs.statSync(filePath);
1404
- return stats.size;
1405
- }
1406
- }
1407
- catch (error) {
1408
- // Ignore errors, return 0
1409
- }
1410
- return 0;
1411
- }