@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.
- package/package.json +3 -3
- package/dist/app.js +0 -227
- package/dist/cli.bundle.js.map +0 -7
- package/dist/cli.js +0 -132
- package/dist/commands/add.js +0 -245
- package/dist/commands/agents-run.js +0 -71
- package/dist/commands/auth.js +0 -259
- package/dist/commands/branch.js +0 -1411
- package/dist/commands/claude-sync.js +0 -772
- package/dist/commands/comment.js +0 -568
- package/dist/commands/diff.js +0 -182
- package/dist/commands/index.js +0 -73
- package/dist/commands/init.js +0 -597
- package/dist/commands/ls.js +0 -135
- package/dist/commands/members.js +0 -687
- package/dist/commands/mv.js +0 -282
- package/dist/commands/recover.js +0 -207
- package/dist/commands/rm.js +0 -257
- package/dist/commands/spec.js +0 -386
- package/dist/commands/status.js +0 -296
- package/dist/commands/sync.js +0 -119
- package/dist/commands/trace.js +0 -1752
- package/dist/commands/workspace.js +0 -447
- package/dist/components/conflict-resolution-ui.js +0 -120
- package/dist/components/messages.js +0 -5
- package/dist/components/thread.js +0 -8
- package/dist/config/features.js +0 -83
- package/dist/containers/branches-container.js +0 -140
- package/dist/containers/directory-container.js +0 -92
- package/dist/containers/thread-container.js +0 -214
- package/dist/containers/threads-container.js +0 -27
- package/dist/containers/workspaces-container.js +0 -27
- package/dist/daemon/conflict-resolution.js +0 -172
- package/dist/daemon/content-hash.js +0 -31
- package/dist/daemon/file-sync.js +0 -985
- package/dist/daemon/index.js +0 -203
- package/dist/daemon/mime-types.js +0 -166
- package/dist/daemon/offline-queue.js +0 -211
- package/dist/daemon/path-utils.js +0 -64
- package/dist/daemon/share-policy.js +0 -83
- package/dist/daemon/wasm-errors.js +0 -189
- package/dist/daemon/worker.js +0 -557
- package/dist/daemon-worker.js +0 -258
- package/dist/errors/workspace-errors.js +0 -48
- package/dist/lib/auth-server.js +0 -216
- package/dist/lib/browser.js +0 -35
- package/dist/lib/diff.js +0 -284
- package/dist/lib/formatters.js +0 -204
- package/dist/lib/git.js +0 -137
- package/dist/lib/local-fs.js +0 -201
- package/dist/lib/prompts.js +0 -56
- package/dist/lib/storage.js +0 -213
- package/dist/lib/trace-formatters.js +0 -314
- package/dist/services/add-service.js +0 -554
- package/dist/services/add-validation.js +0 -124
- package/dist/services/automatic-file-tracker.js +0 -303
- package/dist/services/cli-orchestrator.js +0 -227
- package/dist/services/feature-flags.js +0 -187
- package/dist/services/file-import-service.js +0 -283
- package/dist/services/file-transformation-service.js +0 -218
- package/dist/services/logger.js +0 -44
- package/dist/services/mod-config.js +0 -67
- package/dist/services/modignore-service.js +0 -328
- package/dist/services/sync-daemon.js +0 -244
- package/dist/services/thread-notification-service.js +0 -50
- package/dist/services/thread-service.js +0 -147
- package/dist/stores/use-directory-store.js +0 -96
- package/dist/stores/use-threads-store.js +0 -46
- package/dist/stores/use-workspaces-store.js +0 -54
- package/dist/types/add-types.js +0 -99
- package/dist/types/config.js +0 -16
- package/dist/types/index.js +0 -2
- package/dist/types/workspace-connection.js +0 -53
- package/dist/types.js +0 -1
package/dist/commands/branch.js
DELETED
|
@@ -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
|
-
}
|