@mod-computer/cli 0.1.0
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/README.md +125 -0
- package/commands/execute.md +156 -0
- package/commands/overview.md +233 -0
- package/commands/review.md +151 -0
- package/commands/spec.md +169 -0
- package/dist/app.js +227 -0
- package/dist/cli.bundle.js +25824 -0
- package/dist/cli.bundle.js.map +7 -0
- package/dist/cli.js +121 -0
- package/dist/commands/agents-run.js +71 -0
- package/dist/commands/auth.js +151 -0
- package/dist/commands/branch.js +1411 -0
- package/dist/commands/claude-sync.js +772 -0
- package/dist/commands/index.js +43 -0
- package/dist/commands/init.js +378 -0
- package/dist/commands/recover.js +207 -0
- package/dist/commands/spec.js +386 -0
- package/dist/commands/status.js +329 -0
- package/dist/commands/sync.js +95 -0
- package/dist/commands/workspace.js +423 -0
- package/dist/components/conflict-resolution-ui.js +120 -0
- package/dist/components/messages.js +5 -0
- package/dist/components/thread.js +8 -0
- package/dist/config/features.js +72 -0
- package/dist/config/release-profiles/development.json +11 -0
- package/dist/config/release-profiles/mvp.json +12 -0
- package/dist/config/release-profiles/v0.1.json +11 -0
- package/dist/config/release-profiles/v0.2.json +11 -0
- package/dist/containers/branches-container.js +140 -0
- package/dist/containers/directory-container.js +92 -0
- package/dist/containers/thread-container.js +214 -0
- package/dist/containers/threads-container.js +27 -0
- package/dist/containers/workspaces-container.js +27 -0
- package/dist/daemon-worker.js +257 -0
- package/dist/lib/auth-server.js +153 -0
- package/dist/lib/browser.js +35 -0
- package/dist/lib/storage.js +203 -0
- package/dist/services/automatic-file-tracker.js +303 -0
- package/dist/services/cli-orchestrator.js +227 -0
- package/dist/services/feature-flags.js +187 -0
- package/dist/services/file-import-service.js +283 -0
- package/dist/services/file-transformation-service.js +218 -0
- package/dist/services/logger.js +44 -0
- package/dist/services/mod-config.js +61 -0
- package/dist/services/modignore-service.js +326 -0
- package/dist/services/sync-daemon.js +244 -0
- package/dist/services/thread-notification-service.js +50 -0
- package/dist/services/thread-service.js +147 -0
- package/dist/stores/use-directory-store.js +96 -0
- package/dist/stores/use-threads-store.js +46 -0
- package/dist/stores/use-workspaces-store.js +32 -0
- package/dist/types/config.js +16 -0
- package/dist/types/index.js +2 -0
- package/dist/types/workspace-connection.js +2 -0
- package/dist/types.js +1 -0
- package/package.json +67 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import { createModWorkspace } from '@mod/mod-core';
|
|
2
|
+
import { readModConfig, writeModConfig } from '../services/mod-config.js';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
export async function workspaceCommand(args, repo) {
|
|
6
|
+
const [subcommand, ...rest] = args;
|
|
7
|
+
switch (subcommand) {
|
|
8
|
+
case 'create':
|
|
9
|
+
await handleCreateWorkspace(rest, repo);
|
|
10
|
+
break;
|
|
11
|
+
case 'list':
|
|
12
|
+
await handleListWorkspaces(rest, repo);
|
|
13
|
+
break;
|
|
14
|
+
case 'switch':
|
|
15
|
+
await handleSwitchWorkspace(rest, repo);
|
|
16
|
+
break;
|
|
17
|
+
case 'info':
|
|
18
|
+
await handleWorkspaceInfo(rest, repo);
|
|
19
|
+
break;
|
|
20
|
+
case 'delete':
|
|
21
|
+
await handleDeleteWorkspace(rest, repo);
|
|
22
|
+
break;
|
|
23
|
+
case 'rename':
|
|
24
|
+
await handleRenameWorkspace(rest, repo);
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
if (!subcommand) {
|
|
28
|
+
await handleListWorkspaces([], repo);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.error('Usage: mod workspace <create|list|switch|info|delete|rename> [options]');
|
|
32
|
+
console.error('Available commands:');
|
|
33
|
+
console.error(' create <name> Create new workspace and set as active');
|
|
34
|
+
console.error(' list List all available workspaces');
|
|
35
|
+
console.error(' switch <name-or-id> Switch to a different workspace');
|
|
36
|
+
console.error(' info [name-or-id] Show workspace details');
|
|
37
|
+
console.error(' delete <name-or-id> Delete a workspace');
|
|
38
|
+
console.error(' rename <name-or-id> <new-name> Rename a workspace');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
async function handleCreateWorkspace(args, repo) {
|
|
45
|
+
const [name, ...flags] = args;
|
|
46
|
+
if (!name) {
|
|
47
|
+
console.error('Usage: mod workspace create <name> [--description <desc>]');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
let description;
|
|
51
|
+
const descIndex = flags.indexOf('--description');
|
|
52
|
+
if (descIndex !== -1 && descIndex + 1 < flags.length) {
|
|
53
|
+
description = flags[descIndex + 1];
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const modWorkspace = createModWorkspace(repo);
|
|
57
|
+
console.log(`Creating workspace: ${name}...`);
|
|
58
|
+
const workspaceHandle = await modWorkspace.createWorkspace({
|
|
59
|
+
name: name,
|
|
60
|
+
description: description
|
|
61
|
+
});
|
|
62
|
+
// Get the main branch ID for the newly created workspace
|
|
63
|
+
const branches = await workspaceHandle.branch.list();
|
|
64
|
+
const mainBranch = branches.find(b => b.metadata?.type === 'main') || branches[0];
|
|
65
|
+
if (!mainBranch) {
|
|
66
|
+
throw new Error('Failed to find main branch in newly created workspace');
|
|
67
|
+
}
|
|
68
|
+
const config = (readModConfig() || {});
|
|
69
|
+
const updatedConfig = {
|
|
70
|
+
...config,
|
|
71
|
+
workspaceId: workspaceHandle.id,
|
|
72
|
+
workspaceName: name,
|
|
73
|
+
activeBranchId: mainBranch.id, // Set to main branch ID
|
|
74
|
+
lastWorkspaceSwitch: new Date().toISOString()
|
|
75
|
+
};
|
|
76
|
+
const recentWorkspaces = updatedConfig.recentWorkspaces || [];
|
|
77
|
+
const existingIndex = recentWorkspaces.findIndex(w => w.id === workspaceHandle.id);
|
|
78
|
+
const workspaceRef = {
|
|
79
|
+
id: workspaceHandle.id,
|
|
80
|
+
name: name,
|
|
81
|
+
lastAccessed: new Date().toISOString(),
|
|
82
|
+
accessCount: existingIndex !== -1 ? recentWorkspaces[existingIndex].accessCount + 1 : 1
|
|
83
|
+
};
|
|
84
|
+
if (existingIndex !== -1) {
|
|
85
|
+
recentWorkspaces[existingIndex] = workspaceRef;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
recentWorkspaces.unshift(workspaceRef);
|
|
89
|
+
// Keep only last 10 workspaces
|
|
90
|
+
updatedConfig.recentWorkspaces = recentWorkspaces.slice(0, 10);
|
|
91
|
+
}
|
|
92
|
+
writeModConfig(updatedConfig);
|
|
93
|
+
await invalidateWorkspaceCache();
|
|
94
|
+
console.log(`✓ Created and switched to workspace: ${name} (${workspaceHandle.id})`);
|
|
95
|
+
if (description) {
|
|
96
|
+
console.log(` Description: ${description}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error('Failed to create workspace:', error);
|
|
101
|
+
console.log('Try running: mod workspace list to check available workspaces');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function handleListWorkspaces(args, repo) {
|
|
106
|
+
try {
|
|
107
|
+
const modWorkspace = createModWorkspace(repo);
|
|
108
|
+
const workspaces = await getWorkspacesWithCache(modWorkspace);
|
|
109
|
+
if (workspaces.length === 0) {
|
|
110
|
+
console.log('No workspaces found.');
|
|
111
|
+
console.log('Create a new workspace with: mod workspace create <name>');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const config = readModConfig();
|
|
115
|
+
const activeWorkspaceId = config?.workspaceId;
|
|
116
|
+
console.log('Available workspaces:');
|
|
117
|
+
workspaces.forEach((workspace, index) => {
|
|
118
|
+
const isActive = activeWorkspaceId && workspace.id === activeWorkspaceId;
|
|
119
|
+
const indicator = isActive ? '* ' : ' ';
|
|
120
|
+
const quickNumber = index < 9 ? `[${index + 1}] ` : ' ';
|
|
121
|
+
console.log(`${indicator}${quickNumber}${workspace.name} (${workspace.id})`);
|
|
122
|
+
if (workspace.description) {
|
|
123
|
+
console.log(` ${workspace.description}`);
|
|
124
|
+
}
|
|
125
|
+
const stats = [
|
|
126
|
+
`${workspace.fileCount} files`,
|
|
127
|
+
`${workspace.branchCount} branches`,
|
|
128
|
+
`updated ${formatRelativeTime(workspace.lastModified)}`
|
|
129
|
+
].join(', ');
|
|
130
|
+
console.log(` ${stats}`);
|
|
131
|
+
});
|
|
132
|
+
if (workspaces.length > 1) {
|
|
133
|
+
console.log('\nQuick switch: mod workspace switch <number>');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
console.error('Failed to list workspaces:', error);
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async function handleSwitchWorkspace(args, repo) {
|
|
142
|
+
const [nameOrId] = args;
|
|
143
|
+
if (!nameOrId) {
|
|
144
|
+
console.error('Usage: mod workspace switch <name-or-id>');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const modWorkspace = createModWorkspace(repo);
|
|
149
|
+
const targetWorkspace = await resolveWorkspace(nameOrId, modWorkspace);
|
|
150
|
+
if (!targetWorkspace) {
|
|
151
|
+
console.error(`Workspace not found: ${nameOrId}`);
|
|
152
|
+
console.log('Available workspaces:');
|
|
153
|
+
const workspaces = await getWorkspacesWithCache(modWorkspace);
|
|
154
|
+
workspaces.forEach((w) => console.log(` ${w.name} (${w.id})`));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
const config = (readModConfig() || {});
|
|
158
|
+
const updatedConfig = {
|
|
159
|
+
...config,
|
|
160
|
+
workspaceId: targetWorkspace.id,
|
|
161
|
+
workspaceName: targetWorkspace.name,
|
|
162
|
+
activeBranchId: undefined, // Reset to main branch
|
|
163
|
+
lastWorkspaceSwitch: new Date().toISOString()
|
|
164
|
+
};
|
|
165
|
+
const recentWorkspaces = updatedConfig.recentWorkspaces || [];
|
|
166
|
+
const existingIndex = recentWorkspaces.findIndex(w => w.id === targetWorkspace.id);
|
|
167
|
+
const workspaceRef = {
|
|
168
|
+
id: targetWorkspace.id,
|
|
169
|
+
name: targetWorkspace.name,
|
|
170
|
+
lastAccessed: new Date().toISOString(),
|
|
171
|
+
accessCount: existingIndex !== -1 ? recentWorkspaces[existingIndex].accessCount + 1 : 1
|
|
172
|
+
};
|
|
173
|
+
if (existingIndex !== -1) {
|
|
174
|
+
recentWorkspaces[existingIndex] = workspaceRef;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
recentWorkspaces.unshift(workspaceRef);
|
|
178
|
+
updatedConfig.recentWorkspaces = recentWorkspaces.slice(0, 10);
|
|
179
|
+
}
|
|
180
|
+
writeModConfig(updatedConfig);
|
|
181
|
+
console.log(`✓ Switched to workspace: ${targetWorkspace.name} (${targetWorkspace.id})`);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
console.error('Failed to switch workspace:', error);
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function handleWorkspaceInfo(args, repo) {
|
|
189
|
+
const [nameOrId] = args;
|
|
190
|
+
try {
|
|
191
|
+
const modWorkspace = createModWorkspace(repo);
|
|
192
|
+
let targetWorkspace;
|
|
193
|
+
if (nameOrId) {
|
|
194
|
+
const resolved = await resolveWorkspace(nameOrId, modWorkspace);
|
|
195
|
+
if (!resolved) {
|
|
196
|
+
console.error(`Workspace not found: ${nameOrId}`);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
targetWorkspace = resolved;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
const config = readModConfig();
|
|
203
|
+
if (!config?.workspaceId) {
|
|
204
|
+
console.error('No active workspace configured');
|
|
205
|
+
console.log('Set a workspace with: mod workspace switch <name>');
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
const resolved = await resolveWorkspace(config.workspaceId, modWorkspace);
|
|
209
|
+
if (!resolved) {
|
|
210
|
+
console.error('Current workspace not found');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
targetWorkspace = resolved;
|
|
214
|
+
}
|
|
215
|
+
console.log(`Workspace: ${targetWorkspace.name}`);
|
|
216
|
+
console.log(`ID: ${targetWorkspace.id}`);
|
|
217
|
+
if (targetWorkspace.description) {
|
|
218
|
+
console.log(`Description: ${targetWorkspace.description}`);
|
|
219
|
+
}
|
|
220
|
+
console.log(`Files: ${targetWorkspace.fileCount}`);
|
|
221
|
+
console.log(`Branches: ${targetWorkspace.branchCount}`);
|
|
222
|
+
console.log(`Last Updated: ${formatAbsoluteTime(targetWorkspace.lastModified)}`);
|
|
223
|
+
if (targetWorkspace.permissions.length > 0) {
|
|
224
|
+
console.log(`Permissions: ${targetWorkspace.permissions.join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
const config = readModConfig();
|
|
227
|
+
if (config?.workspaceId === targetWorkspace.id && config?.activeBranchId) {
|
|
228
|
+
console.log(`Active Branch: ${config.activeBranchId}`);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.log('Active Branch: main');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
console.error('Failed to get workspace info:', error);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function handleDeleteWorkspace(args, repo) {
|
|
240
|
+
const [nameOrId] = args;
|
|
241
|
+
if (!nameOrId) {
|
|
242
|
+
console.error('Usage: mod workspace delete <name-or-id>');
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const modWorkspace = createModWorkspace(repo);
|
|
247
|
+
const targetWorkspace = await resolveWorkspace(nameOrId, modWorkspace);
|
|
248
|
+
if (!targetWorkspace) {
|
|
249
|
+
console.error(`Workspace not found: ${nameOrId}`);
|
|
250
|
+
process.exit(1);
|
|
251
|
+
}
|
|
252
|
+
console.log(`WARNING: This will permanently delete workspace "${targetWorkspace.name}"`);
|
|
253
|
+
console.log(`Files: ${targetWorkspace.fileCount}, Branches: ${targetWorkspace.branchCount}`);
|
|
254
|
+
console.log('Type "yes" to confirm deletion:');
|
|
255
|
+
// Simple confirmation - in a real implementation you might want to use a proper prompt library
|
|
256
|
+
const confirm = process.env.MOD_AUTO_CONFIRM === 'yes' ? 'yes' : 'no';
|
|
257
|
+
if (confirm !== 'yes') {
|
|
258
|
+
console.log('Deletion cancelled');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Note: Actual deletion would need to be implemented in ModWorkspace interface
|
|
262
|
+
// For now, just remove from config if it's the active workspace
|
|
263
|
+
const config = readModConfig();
|
|
264
|
+
if (config?.workspaceId === targetWorkspace.id) {
|
|
265
|
+
const updatedConfig = { ...config };
|
|
266
|
+
delete updatedConfig.workspaceId;
|
|
267
|
+
delete updatedConfig.workspaceName;
|
|
268
|
+
delete updatedConfig.activeBranchId;
|
|
269
|
+
writeModConfig(updatedConfig);
|
|
270
|
+
}
|
|
271
|
+
await invalidateWorkspaceCache();
|
|
272
|
+
console.log(`✓ Workspace "${targetWorkspace.name}" scheduled for deletion`);
|
|
273
|
+
console.log('Note: Full deletion implementation pending ModWorkspace.deleteWorkspace() interface');
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
console.error('Failed to delete workspace:', error);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function handleRenameWorkspace(args, repo) {
|
|
281
|
+
const [nameOrId, newName] = args;
|
|
282
|
+
if (!nameOrId || !newName) {
|
|
283
|
+
console.error('Usage: mod workspace rename <name-or-id> <new-name>');
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
const modWorkspace = createModWorkspace(repo);
|
|
288
|
+
const targetWorkspace = await resolveWorkspace(nameOrId, modWorkspace);
|
|
289
|
+
if (!targetWorkspace) {
|
|
290
|
+
console.error(`Workspace not found: ${nameOrId}`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
const existingWorkspaces = await getWorkspacesWithCache(modWorkspace);
|
|
294
|
+
const isDuplicate = existingWorkspaces.some((w) => w.name === newName && w.id !== targetWorkspace.id);
|
|
295
|
+
if (isDuplicate) {
|
|
296
|
+
console.error(`Workspace name "${newName}" already exists`);
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
// Note: Actual rename would need to be implemented in ModWorkspace interface
|
|
300
|
+
// For now, update local config if this is the active workspace
|
|
301
|
+
const config = readModConfig();
|
|
302
|
+
if (config?.workspaceId === targetWorkspace.id) {
|
|
303
|
+
const updatedConfig = { ...config, workspaceName: newName };
|
|
304
|
+
writeModConfig(updatedConfig);
|
|
305
|
+
}
|
|
306
|
+
await invalidateWorkspaceCache();
|
|
307
|
+
console.log(`✓ Workspace renamed from "${targetWorkspace.name}" to "${newName}"`);
|
|
308
|
+
console.log('Note: Full rename implementation pending ModWorkspace.renameWorkspace() interface');
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.error('Failed to rename workspace:', error);
|
|
312
|
+
process.exit(1);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function resolveWorkspace(nameOrId, modWorkspace) {
|
|
316
|
+
const workspaces = await getWorkspacesWithCache(modWorkspace);
|
|
317
|
+
// First try exact ID match
|
|
318
|
+
let match = workspaces.find((w) => w.id === nameOrId);
|
|
319
|
+
if (match)
|
|
320
|
+
return match;
|
|
321
|
+
// Then try exact name match
|
|
322
|
+
match = workspaces.find((w) => w.name === nameOrId);
|
|
323
|
+
if (match)
|
|
324
|
+
return match;
|
|
325
|
+
// Try numbered quick-switch (1-9)
|
|
326
|
+
const num = parseInt(nameOrId);
|
|
327
|
+
if (!isNaN(num) && num >= 1 && num <= workspaces.length) {
|
|
328
|
+
return workspaces[num - 1];
|
|
329
|
+
}
|
|
330
|
+
// Finally try fuzzy name matching
|
|
331
|
+
const fuzzyMatches = workspaces.filter((w) => w.name.toLowerCase().includes(nameOrId.toLowerCase()));
|
|
332
|
+
if (fuzzyMatches.length === 1) {
|
|
333
|
+
return fuzzyMatches[0];
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
async function getWorkspacesWithCache(modWorkspace) {
|
|
338
|
+
const cacheDir = '.mod/.cache';
|
|
339
|
+
const cachePath = path.join(cacheDir, 'workspaces.json');
|
|
340
|
+
// Ensure cache directory exists
|
|
341
|
+
if (!fs.existsSync(cacheDir)) {
|
|
342
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
343
|
+
}
|
|
344
|
+
// Check if cache exists and is recent (under 5 minutes old)
|
|
345
|
+
if (fs.existsSync(cachePath)) {
|
|
346
|
+
const stats = fs.statSync(cachePath);
|
|
347
|
+
const ageMinutes = (Date.now() - stats.mtime.getTime()) / (1000 * 60);
|
|
348
|
+
if (ageMinutes < 5) {
|
|
349
|
+
try {
|
|
350
|
+
const cache = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
|
|
351
|
+
return cache.workspaces;
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
// Cache is corrupted, fall through to refresh
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const workspaceHandles = await modWorkspace.listWorkspaces();
|
|
360
|
+
// Convert WorkspaceHandle[] to CachedWorkspace format
|
|
361
|
+
const workspaces = [];
|
|
362
|
+
for (const handle of workspaceHandles) {
|
|
363
|
+
// Get additional workspace data if available
|
|
364
|
+
let fileCount = 0;
|
|
365
|
+
let branchCount = 1;
|
|
366
|
+
try {
|
|
367
|
+
const files = await handle.file.list();
|
|
368
|
+
fileCount = files.length;
|
|
369
|
+
const branches = await handle.branch.list();
|
|
370
|
+
branchCount = branches.length || 1;
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
// Ignore errors when fetching additional data
|
|
374
|
+
}
|
|
375
|
+
workspaces.push({
|
|
376
|
+
id: handle.id,
|
|
377
|
+
name: handle.name,
|
|
378
|
+
description: undefined, // Not available from handle
|
|
379
|
+
fileCount,
|
|
380
|
+
branchCount,
|
|
381
|
+
lastModified: new Date().toISOString(), // Would need to be tracked properly
|
|
382
|
+
permissions: [] // Would need to be populated from permission system
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const cache = {
|
|
386
|
+
lastUpdate: new Date().toISOString(),
|
|
387
|
+
workspaces: workspaces,
|
|
388
|
+
nameIndex: workspaces.reduce((index, w) => {
|
|
389
|
+
index[w.name] = w.id;
|
|
390
|
+
return index;
|
|
391
|
+
}, {})
|
|
392
|
+
};
|
|
393
|
+
fs.writeFileSync(cachePath, JSON.stringify(cache, null, 2));
|
|
394
|
+
return workspaces;
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
console.warn('Failed to fetch workspaces:', error);
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function invalidateWorkspaceCache() {
|
|
402
|
+
const cachePath = '.mod/.cache/workspaces.json';
|
|
403
|
+
if (fs.existsSync(cachePath)) {
|
|
404
|
+
fs.unlinkSync(cachePath);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// Utility functions for time formatting
|
|
408
|
+
function formatRelativeTime(isoString) {
|
|
409
|
+
const date = new Date(isoString);
|
|
410
|
+
const now = new Date();
|
|
411
|
+
const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
|
|
412
|
+
if (diffHours < 1)
|
|
413
|
+
return 'just now';
|
|
414
|
+
if (diffHours < 24)
|
|
415
|
+
return `${diffHours}h ago`;
|
|
416
|
+
if (diffHours < 24 * 7)
|
|
417
|
+
return `${Math.floor(diffHours / 24)}d ago`;
|
|
418
|
+
return `${Math.floor(diffHours / (24 * 7))}w ago`;
|
|
419
|
+
}
|
|
420
|
+
function formatAbsoluteTime(isoString) {
|
|
421
|
+
const date = new Date(isoString);
|
|
422
|
+
return date.toLocaleString();
|
|
423
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
const options = [
|
|
5
|
+
{
|
|
6
|
+
key: 'local',
|
|
7
|
+
label: 'Keep Local',
|
|
8
|
+
description: 'Keep the local version and skip remote',
|
|
9
|
+
color: 'blue',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
key: 'remote',
|
|
13
|
+
label: 'Keep Remote',
|
|
14
|
+
description: 'Overwrite local with remote version',
|
|
15
|
+
color: 'green',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'both',
|
|
19
|
+
label: 'Keep Both',
|
|
20
|
+
description: 'Keep local and save remote with suffix',
|
|
21
|
+
color: 'yellow',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
key: 'skip',
|
|
25
|
+
label: 'Skip',
|
|
26
|
+
description: 'Skip this file for now',
|
|
27
|
+
color: 'gray',
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
export default function ConflictResolutionUI({ conflict, onResolve, onCancel, }) {
|
|
31
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
32
|
+
useInput((input, key) => {
|
|
33
|
+
if (key.downArrow) {
|
|
34
|
+
setSelectedIndex((prev) => (prev + 1) % options.length);
|
|
35
|
+
}
|
|
36
|
+
else if (key.upArrow) {
|
|
37
|
+
setSelectedIndex((prev) => (prev - 1 + options.length) % options.length);
|
|
38
|
+
}
|
|
39
|
+
else if (key.return) {
|
|
40
|
+
onResolve(options[selectedIndex].key);
|
|
41
|
+
}
|
|
42
|
+
else if (key.escape && onCancel) {
|
|
43
|
+
onCancel();
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const formatFileSize = (bytes) => {
|
|
47
|
+
if (!bytes)
|
|
48
|
+
return 'Unknown size';
|
|
49
|
+
if (bytes < 1024)
|
|
50
|
+
return `${bytes} B`;
|
|
51
|
+
if (bytes < 1024 * 1024)
|
|
52
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
53
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
54
|
+
};
|
|
55
|
+
const formatDate = (dateString) => {
|
|
56
|
+
if (!dateString)
|
|
57
|
+
return 'Unknown date';
|
|
58
|
+
try {
|
|
59
|
+
return new Date(dateString).toLocaleString();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return dateString;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "\uD83D\uDD25 File Conflict Detected" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, paddingX: 2, children: [_jsxs(Text, { bold: true, color: "white", children: ["File: ", conflict.fileName] }), _jsxs(Text, { color: "gray", children: ["Path: ", conflict.localPath] })] }), _jsxs(Box, { flexDirection: "row", marginBottom: 1, paddingX: 2, children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsx(Text, { bold: true, color: "blue", children: "\uD83D\uDCC1 Local Version" }), _jsxs(Text, { color: "gray", children: ["Size: ", formatFileSize(conflict.localSize)] }), _jsxs(Text, { color: "gray", children: ["Modified: ", formatDate(conflict.localModified)] })] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsx(Text, { bold: true, color: "green", children: "\u2601\uFE0F Remote Version" }), _jsxs(Text, { color: "gray", children: ["Size: ", formatFileSize(conflict.remoteSize)] }), _jsxs(Text, { color: "gray", children: ["Modified: ", formatDate(conflict.remoteModified)] })] })] }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "white", children: "How would you like to resolve this conflict?" }) }), options.map((option, index) => (_jsxs(Box, { marginLeft: 2, children: [_jsxs(Text, { color: selectedIndex === index ? 'white' : 'gray', backgroundColor: selectedIndex === index ? option.color : undefined, bold: selectedIndex === index, children: [selectedIndex === index ? '▶ ' : ' ', option.label] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "gray", children: ["- ", option.description] }) })] }, option.key)))] }), _jsx(Box, { marginTop: 1, paddingX: 2, children: _jsxs(Text, { color: "gray", dimColor: true, children: ["Use \u2191\u2193 arrow keys to navigate, Enter to select", onCancel ? ', Esc to cancel' : ''] }) })] }));
|
|
66
|
+
}
|
|
67
|
+
export function BatchConflictResolutionUI({ conflicts, onResolveAll, onResolveOne, onCancel, }) {
|
|
68
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
69
|
+
const [resolutions, setResolutions] = useState({});
|
|
70
|
+
const [showBatchOptions, setShowBatchOptions] = useState(false);
|
|
71
|
+
const [selectedBatchOption, setSelectedBatchOption] = useState(0);
|
|
72
|
+
const batchOptions = [
|
|
73
|
+
{ key: 'local', label: 'Keep All Local', description: 'Keep all local versions' },
|
|
74
|
+
{ key: 'remote', label: 'Keep All Remote', description: 'Overwrite all with remote versions' },
|
|
75
|
+
{ key: 'both', label: 'Keep All Both', description: 'Keep all local and save remote with suffix' },
|
|
76
|
+
];
|
|
77
|
+
useInput((input, key) => {
|
|
78
|
+
if (showBatchOptions) {
|
|
79
|
+
if (key.downArrow) {
|
|
80
|
+
setSelectedBatchOption((prev) => (prev + 1) % batchOptions.length);
|
|
81
|
+
}
|
|
82
|
+
else if (key.upArrow) {
|
|
83
|
+
setSelectedBatchOption((prev) => (prev - 1 + batchOptions.length) % batchOptions.length);
|
|
84
|
+
}
|
|
85
|
+
else if (key.return) {
|
|
86
|
+
const resolution = batchOptions[selectedBatchOption].key;
|
|
87
|
+
const allResolutions = {};
|
|
88
|
+
conflicts.forEach(conflict => {
|
|
89
|
+
allResolutions[conflict.fileName] = resolution;
|
|
90
|
+
});
|
|
91
|
+
onResolveAll(allResolutions);
|
|
92
|
+
}
|
|
93
|
+
else if (key.escape) {
|
|
94
|
+
setShowBatchOptions(false);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
if (input === 'a') {
|
|
99
|
+
setShowBatchOptions(true);
|
|
100
|
+
}
|
|
101
|
+
else if (key.escape && onCancel) {
|
|
102
|
+
onCancel();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const handleResolveOne = (resolution) => {
|
|
107
|
+
const conflict = conflicts[currentIndex];
|
|
108
|
+
setResolutions(prev => ({ ...prev, [conflict.fileName]: resolution }));
|
|
109
|
+
onResolveOne(conflict.fileName, resolution);
|
|
110
|
+
if (currentIndex < conflicts.length - 1) {
|
|
111
|
+
setCurrentIndex(prev => prev + 1);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
if (showBatchOptions) {
|
|
115
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "yellow", children: "\uD83D\uDD25 Batch Conflict Resolution" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Apply the same resolution to all ", conflicts.length, " conflicts:"] }) }), batchOptions.map((option, index) => (_jsxs(Box, { marginLeft: 2, marginBottom: 1, children: [_jsxs(Text, { color: selectedBatchOption === index ? 'white' : 'gray', backgroundColor: selectedBatchOption === index ? 'blue' : undefined, bold: selectedBatchOption === index, children: [selectedBatchOption === index ? '▶ ' : ' ', option.label] }), _jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: "gray", children: ["- ", option.description] }) })] }, option.key))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: "gray", dimColor: true, children: "Use \u2191\u2193 arrow keys to navigate, Enter to apply to all, Esc to go back" }) })] }));
|
|
116
|
+
}
|
|
117
|
+
const currentConflict = conflicts[currentIndex];
|
|
118
|
+
const resolvedCount = Object.keys(resolutions).length;
|
|
119
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, paddingX: 1, children: _jsxs(Text, { color: "yellow", children: ["Conflict ", currentIndex + 1, " of ", conflicts.length, " (", resolvedCount, " resolved)"] }) }), _jsx(ConflictResolutionUI, { conflict: currentConflict, onResolve: handleResolveOne }), _jsx(Box, { marginTop: 1, paddingX: 2, children: _jsx(Text, { color: "gray", dimColor: true, children: "Press 'a' for batch resolution options, Esc to cancel" }) })] }));
|
|
120
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
const MessageList = React.memo(({ messages, parseContentSegments, messageKeyProp }) => (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: messages.length === 0 ? (_jsx(Text, { color: "gray", children: "No messages yet. Type to start the thread." })) : (messages.map((m, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "yellow", children: [new Date(m.timestamp).toLocaleTimeString(), " "] }), _jsxs(Text, { color: m.userType === 'user' ? 'cyan' : 'magenta', children: [m.userType === 'user' ? (m.user?.name || 'You') : 'Assistant', ":"] }), ' ', parseContentSegments(m.text).map((seg, idx) => seg.isBold ? (_jsx(Text, { bold: true, color: "whiteBright", children: seg.text }, idx)) : (_jsx(Text, { children: seg.text }, idx)))] }, `${m.id || i}-${m._contentHash || m.text.length}`)))) })));
|
|
5
|
+
export default MessageList;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import MessageList from './messages.js';
|
|
5
|
+
const ThreadView = React.memo(({ activeThread, messages, parseContentSegments, messageKeyProp }) => {
|
|
6
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Thread: ", _jsx(Text, { color: "green", children: activeThread.name })] }), _jsx(MessageList, { messages: messages, parseContentSegments: parseContentSegments, messageKeyProp: messageKeyProp }), _jsxs(Text, { color: "cyan", children: ["Type ", _jsx(Text, { bold: true, children: "/branch" }), " to switch threads."] })] }));
|
|
7
|
+
});
|
|
8
|
+
export default ThreadView;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
export const FEATURES = {
|
|
5
|
+
STATUS: 'status',
|
|
6
|
+
WORKSPACE_MANAGEMENT: 'workspace-management',
|
|
7
|
+
WORKSPACE_BRANCHING: 'workspace-branching',
|
|
8
|
+
TASK_MANAGEMENT: 'task-management',
|
|
9
|
+
FILE_OPERATIONS: 'file-operations',
|
|
10
|
+
AGENT_INTEGRATIONS: 'agent-integrations',
|
|
11
|
+
SYNC_OPERATIONS: 'sync-operations',
|
|
12
|
+
WATCH_OPERATIONS: 'watch-operations',
|
|
13
|
+
CONNECTOR_INTEGRATIONS: 'connector-integrations',
|
|
14
|
+
AUTH: 'auth'
|
|
15
|
+
};
|
|
16
|
+
let releaseProfile = null;
|
|
17
|
+
export function isFeatureEnabled(feature) {
|
|
18
|
+
if (process.env.NODE_ENV === 'development') {
|
|
19
|
+
const envVar = `MOD_FEATURE_${feature.toUpperCase().replace('-', '_')}`;
|
|
20
|
+
if (process.env[envVar] === 'true')
|
|
21
|
+
return true;
|
|
22
|
+
if (process.env[envVar] === 'false')
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (!releaseProfile) {
|
|
26
|
+
releaseProfile = loadReleaseProfile();
|
|
27
|
+
}
|
|
28
|
+
return releaseProfile[feature] ?? false;
|
|
29
|
+
}
|
|
30
|
+
function loadReleaseProfile() {
|
|
31
|
+
const profileName = process.env.MOD_RELEASE_PROFILE || 'development';
|
|
32
|
+
try {
|
|
33
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
34
|
+
const __dirname = path.dirname(__filename);
|
|
35
|
+
const profilePath = path.join(__dirname, 'release-profiles', `${profileName}.json`);
|
|
36
|
+
if (fs.existsSync(profilePath)) {
|
|
37
|
+
const profileData = fs.readFileSync(profilePath, 'utf8');
|
|
38
|
+
return JSON.parse(profileData);
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Profile ${profileName} not found`);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (profileName === 'development') {
|
|
44
|
+
return {
|
|
45
|
+
[FEATURES.STATUS]: true,
|
|
46
|
+
[FEATURES.WORKSPACE_MANAGEMENT]: true,
|
|
47
|
+
[FEATURES.WORKSPACE_BRANCHING]: true,
|
|
48
|
+
[FEATURES.TASK_MANAGEMENT]: true,
|
|
49
|
+
[FEATURES.FILE_OPERATIONS]: true,
|
|
50
|
+
[FEATURES.AGENT_INTEGRATIONS]: true,
|
|
51
|
+
[FEATURES.SYNC_OPERATIONS]: true,
|
|
52
|
+
[FEATURES.WATCH_OPERATIONS]: true,
|
|
53
|
+
[FEATURES.CONNECTOR_INTEGRATIONS]: true
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Fallback to minimal profile for unknown profiles
|
|
57
|
+
return {
|
|
58
|
+
[FEATURES.STATUS]: true,
|
|
59
|
+
[FEATURES.WORKSPACE_MANAGEMENT]: false,
|
|
60
|
+
[FEATURES.WORKSPACE_BRANCHING]: false,
|
|
61
|
+
[FEATURES.TASK_MANAGEMENT]: false,
|
|
62
|
+
[FEATURES.FILE_OPERATIONS]: false,
|
|
63
|
+
[FEATURES.AGENT_INTEGRATIONS]: false,
|
|
64
|
+
[FEATURES.SYNC_OPERATIONS]: false,
|
|
65
|
+
[FEATURES.WATCH_OPERATIONS]: false,
|
|
66
|
+
[FEATURES.CONNECTOR_INTEGRATIONS]: false
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function resetFeatureCache() {
|
|
71
|
+
releaseProfile = null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": true,
|
|
3
|
+
"workspace-management": true,
|
|
4
|
+
"workspace-branching": true,
|
|
5
|
+
"task-management": true,
|
|
6
|
+
"file-operations": true,
|
|
7
|
+
"agent-integrations": true,
|
|
8
|
+
"sync-operations": true,
|
|
9
|
+
"watch-operations": true,
|
|
10
|
+
"connector-integrations": true
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": true,
|
|
3
|
+
"workspace-management": true,
|
|
4
|
+
"workspace-branching": false,
|
|
5
|
+
"task-management": false,
|
|
6
|
+
"file-operations": false,
|
|
7
|
+
"agent-integrations": false,
|
|
8
|
+
"sync-operations": true,
|
|
9
|
+
"watch-operations": true,
|
|
10
|
+
"connector-integrations": false,
|
|
11
|
+
"auth": true
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"status": true,
|
|
3
|
+
"workspace-management": false,
|
|
4
|
+
"workspace-branching": false,
|
|
5
|
+
"task-management": false,
|
|
6
|
+
"file-operations": false,
|
|
7
|
+
"agent-integrations": false,
|
|
8
|
+
"sync-operations": false,
|
|
9
|
+
"watch-operations": false,
|
|
10
|
+
"connector-integrations": false
|
|
11
|
+
}
|