@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.
Files changed (56) hide show
  1. package/README.md +125 -0
  2. package/commands/execute.md +156 -0
  3. package/commands/overview.md +233 -0
  4. package/commands/review.md +151 -0
  5. package/commands/spec.md +169 -0
  6. package/dist/app.js +227 -0
  7. package/dist/cli.bundle.js +25824 -0
  8. package/dist/cli.bundle.js.map +7 -0
  9. package/dist/cli.js +121 -0
  10. package/dist/commands/agents-run.js +71 -0
  11. package/dist/commands/auth.js +151 -0
  12. package/dist/commands/branch.js +1411 -0
  13. package/dist/commands/claude-sync.js +772 -0
  14. package/dist/commands/index.js +43 -0
  15. package/dist/commands/init.js +378 -0
  16. package/dist/commands/recover.js +207 -0
  17. package/dist/commands/spec.js +386 -0
  18. package/dist/commands/status.js +329 -0
  19. package/dist/commands/sync.js +95 -0
  20. package/dist/commands/workspace.js +423 -0
  21. package/dist/components/conflict-resolution-ui.js +120 -0
  22. package/dist/components/messages.js +5 -0
  23. package/dist/components/thread.js +8 -0
  24. package/dist/config/features.js +72 -0
  25. package/dist/config/release-profiles/development.json +11 -0
  26. package/dist/config/release-profiles/mvp.json +12 -0
  27. package/dist/config/release-profiles/v0.1.json +11 -0
  28. package/dist/config/release-profiles/v0.2.json +11 -0
  29. package/dist/containers/branches-container.js +140 -0
  30. package/dist/containers/directory-container.js +92 -0
  31. package/dist/containers/thread-container.js +214 -0
  32. package/dist/containers/threads-container.js +27 -0
  33. package/dist/containers/workspaces-container.js +27 -0
  34. package/dist/daemon-worker.js +257 -0
  35. package/dist/lib/auth-server.js +153 -0
  36. package/dist/lib/browser.js +35 -0
  37. package/dist/lib/storage.js +203 -0
  38. package/dist/services/automatic-file-tracker.js +303 -0
  39. package/dist/services/cli-orchestrator.js +227 -0
  40. package/dist/services/feature-flags.js +187 -0
  41. package/dist/services/file-import-service.js +283 -0
  42. package/dist/services/file-transformation-service.js +218 -0
  43. package/dist/services/logger.js +44 -0
  44. package/dist/services/mod-config.js +61 -0
  45. package/dist/services/modignore-service.js +326 -0
  46. package/dist/services/sync-daemon.js +244 -0
  47. package/dist/services/thread-notification-service.js +50 -0
  48. package/dist/services/thread-service.js +147 -0
  49. package/dist/stores/use-directory-store.js +96 -0
  50. package/dist/stores/use-threads-store.js +46 -0
  51. package/dist/stores/use-workspaces-store.js +32 -0
  52. package/dist/types/config.js +16 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/workspace-connection.js +2 -0
  55. package/dist/types.js +1 -0
  56. package/package.json +67 -0
@@ -0,0 +1,43 @@
1
+ import { agentsRunCommand } from './agents-run.js';
2
+ import { authCommand } from './auth.js';
3
+ import { branchCommand } from './branch.js';
4
+ import { specCommand } from './spec.js';
5
+ import { workspaceCommand } from './workspace.js';
6
+ import { claudeSyncCommand } from './claude-sync.js';
7
+ import { syncCommand } from './sync.js';
8
+ import { initCommand } from './init.js';
9
+ import { recoverCommand } from './recover.js';
10
+ import { FEATURES, isFeatureEnabled } from '../config/features.js';
11
+ const allCommands = {
12
+ 'auth': authCommand,
13
+ 'spec': specCommand,
14
+ 'sync': syncCommand,
15
+ 'agents-run': agentsRunCommand,
16
+ 'claude-sync': claudeSyncCommand,
17
+ 'branch': branchCommand,
18
+ 'workspace': workspaceCommand,
19
+ 'init': initCommand,
20
+ 'recover': recoverCommand,
21
+ };
22
+ const commandFeatureMapping = {
23
+ 'auth': FEATURES.AUTH,
24
+ 'spec': FEATURES.TASK_MANAGEMENT, // Spec tracking, not in MVP
25
+ 'sync': FEATURES.WATCH_OPERATIONS,
26
+ 'agents-run': FEATURES.AGENT_INTEGRATIONS,
27
+ 'claude-sync': FEATURES.AGENT_INTEGRATIONS, // Claude projects sync, separate from file sync daemon
28
+ 'branch': FEATURES.WORKSPACE_BRANCHING,
29
+ 'workspace': FEATURES.WORKSPACE_MANAGEMENT,
30
+ 'init': FEATURES.SYNC_OPERATIONS, // Use existing feature flag for command installation
31
+ 'recover': FEATURES.STATUS, // Recovery command always available when status is enabled
32
+ };
33
+ const registry = {};
34
+ for (const [commandName, commandHandler] of Object.entries(allCommands)) {
35
+ const requiredFeature = commandFeatureMapping[commandName];
36
+ if (requiredFeature && isFeatureEnabled(requiredFeature)) {
37
+ registry[commandName] = commandHandler;
38
+ }
39
+ }
40
+ export function buildCommandRegistry() {
41
+ return registry;
42
+ }
43
+ export default registry;
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Init command for installing Claude commands and workspace initialization
4
+ * Installs versioned command files from CLI package to user's .claude/commands directory
5
+ * Creates a new workspace if none exists in the current directory
6
+ */
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import { createModWorkspace } from '@mod/mod-core';
11
+ import { readModConfig, writeModConfig } from '../services/mod-config.js';
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ export async function initCommand(args, repo) {
15
+ const subcommand = args[0];
16
+ if (subcommand === 'list' || args.includes('--list')) {
17
+ await listAvailableCommands();
18
+ process.exit(0);
19
+ }
20
+ try {
21
+ const workspaceStatus = await checkWorkspaceStatus(repo);
22
+ await ensureClaudeCommandsDirectory();
23
+ const availableCommands = await discoverAvailableCommands();
24
+ const installedCommands = await getInstalledCommands();
25
+ const commandsToInstall = await determineCommandsToInstall(availableCommands, installedCommands, args.includes('--force'));
26
+ if (commandsToInstall.length > 0) {
27
+ displayInstallationPlan(commandsToInstall);
28
+ const shouldProceed = args.includes('--force') || await confirmInstallation(commandsToInstall);
29
+ if (!shouldProceed) {
30
+ console.log('Installation cancelled');
31
+ process.exit(0);
32
+ }
33
+ await installCommands(commandsToInstall);
34
+ }
35
+ const finalWorkspaceStatus = await handleWorkspaceInitialization(workspaceStatus, repo);
36
+ displayInitializationSuccess(commandsToInstall, finalWorkspaceStatus);
37
+ process.exit(0);
38
+ }
39
+ catch (error) {
40
+ handleInstallationError(error);
41
+ }
42
+ }
43
+ async function ensureClaudeCommandsDirectory() {
44
+ const claudeDir = path.join(process.cwd(), '.claude');
45
+ const commandsDir = path.join(claudeDir, 'commands');
46
+ try {
47
+ if (!fs.existsSync(claudeDir)) {
48
+ fs.mkdirSync(claudeDir, { recursive: true, mode: 0o755 });
49
+ }
50
+ if (!fs.existsSync(commandsDir)) {
51
+ fs.mkdirSync(commandsDir, { recursive: true, mode: 0o755 });
52
+ }
53
+ }
54
+ catch (error) {
55
+ throw new Error(`Failed to create .claude/commands directory: ${error.message}`);
56
+ }
57
+ }
58
+ async function discoverAvailableCommands() {
59
+ // Navigate from dist/commands/init.js to package root commands/
60
+ // __dirname is dist/commands, so go up two levels to package root
61
+ const packageRoot = path.resolve(__dirname, '../..');
62
+ const commandsSourceDir = path.join(packageRoot, 'commands');
63
+ if (!fs.existsSync(commandsSourceDir)) {
64
+ console.warn('āš ļø No commands directory found in CLI package');
65
+ return [];
66
+ }
67
+ const commands = [];
68
+ const files = fs.readdirSync(commandsSourceDir).filter(f => f.endsWith('.md'));
69
+ for (const filename of files) {
70
+ const filePath = path.join(commandsSourceDir, filename);
71
+ const content = fs.readFileSync(filePath, 'utf8');
72
+ const metadata = parseFrontmatter(content);
73
+ commands.push({
74
+ filename,
75
+ path: filePath,
76
+ metadata,
77
+ content
78
+ });
79
+ }
80
+ return commands;
81
+ }
82
+ async function getInstalledCommands() {
83
+ const commandsDir = path.join(process.cwd(), '.claude', 'commands');
84
+ const installed = new Map();
85
+ if (!fs.existsSync(commandsDir)) {
86
+ return installed;
87
+ }
88
+ const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
89
+ for (const filename of files) {
90
+ const filePath = path.join(commandsDir, filename);
91
+ try {
92
+ const content = fs.readFileSync(filePath, 'utf8');
93
+ const metadata = parseFrontmatter(content);
94
+ installed.set(filename, metadata);
95
+ }
96
+ catch (error) {
97
+ console.warn(`āš ļø Could not read metadata from ${filename}: ${error.message}`);
98
+ }
99
+ }
100
+ return installed;
101
+ }
102
+ function parseFrontmatter(content) {
103
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
104
+ if (!frontmatterMatch) {
105
+ // Return default metadata for files without frontmatter - they need to be updated
106
+ return {
107
+ version: '0.0.0', // Old version to trigger update
108
+ updated: '1970-01-01',
109
+ checksum: '',
110
+ description: undefined
111
+ };
112
+ }
113
+ const frontmatterText = frontmatterMatch[1];
114
+ const metadata = {};
115
+ // Simple YAML parsing for required fields
116
+ const lines = frontmatterText.split('\n');
117
+ for (const line of lines) {
118
+ const match = line.match(/^(\w+):\s*(.+)$/);
119
+ if (match) {
120
+ const [, key, value] = match;
121
+ metadata[key] = value.trim().replace(/^["']|["']$/g, '');
122
+ }
123
+ }
124
+ return {
125
+ version: metadata.version || '1.0.0',
126
+ updated: metadata.updated || new Date().toISOString(),
127
+ checksum: metadata.checksum || '',
128
+ description: metadata.description
129
+ };
130
+ }
131
+ async function determineCommandsToInstall(available, installed, force) {
132
+ const toInstall = [];
133
+ for (const command of available) {
134
+ const installedMetadata = installed.get(command.filename);
135
+ if (force || !installedMetadata) {
136
+ toInstall.push(command);
137
+ continue;
138
+ }
139
+ // Compare versions
140
+ if (compareVersions(command.metadata.version, installedMetadata.version) > 0) {
141
+ toInstall.push(command);
142
+ }
143
+ }
144
+ return toInstall;
145
+ }
146
+ function compareVersions(v1, v2) {
147
+ const parts1 = v1.split('.').map(n => parseInt(n, 10));
148
+ const parts2 = v2.split('.').map(n => parseInt(n, 10));
149
+ for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
150
+ const part1 = parts1[i] || 0;
151
+ const part2 = parts2[i] || 0;
152
+ if (part1 > part2)
153
+ return 1;
154
+ if (part1 < part2)
155
+ return -1;
156
+ }
157
+ return 0;
158
+ }
159
+ async function listAvailableCommands() {
160
+ console.log('šŸ“‹ Available Claude Commands:\n');
161
+ try {
162
+ const commands = await discoverAvailableCommands();
163
+ if (commands.length === 0) {
164
+ console.log(' No commands available');
165
+ return;
166
+ }
167
+ for (const command of commands) {
168
+ console.log(` ${command.filename.replace('.md', '')}`);
169
+ console.log(` ā”œā”€ā”€ Version: ${command.metadata.version}`);
170
+ if (command.metadata.description) {
171
+ console.log(` └── Description: ${command.metadata.description}`);
172
+ }
173
+ else {
174
+ console.log(` └── Updated: ${command.metadata.updated}`);
175
+ }
176
+ console.log();
177
+ }
178
+ }
179
+ catch (error) {
180
+ console.error('āŒ Error listing commands:', error.message);
181
+ }
182
+ }
183
+ function displayInstallationPlan(commands) {
184
+ console.log('šŸ“¦ Installing Claude Commands:\n');
185
+ for (const command of commands) {
186
+ console.log(` ${command.filename.replace('.md', '')} (v${command.metadata.version})`);
187
+ }
188
+ console.log();
189
+ }
190
+ async function confirmInstallation(commands) {
191
+ // In a real implementation, this would use an interactive prompt library
192
+ // For now, we'll assume confirmation
193
+ return true;
194
+ }
195
+ async function installCommands(commands) {
196
+ const commandsDir = path.join(process.cwd(), '.claude', 'commands');
197
+ const tempFiles = [];
198
+ try {
199
+ // Install to temporary files first for atomic operation
200
+ for (const command of commands) {
201
+ const tempPath = path.join(commandsDir, `${command.filename}.tmp`);
202
+ const finalPath = path.join(commandsDir, command.filename);
203
+ const contentWithChecksum = await updateChecksumInContent(command.content);
204
+ fs.writeFileSync(tempPath, contentWithChecksum, 'utf8');
205
+ tempFiles.push(tempPath);
206
+ }
207
+ // Move all temp files to final locations atomically
208
+ for (let i = 0; i < commands.length; i++) {
209
+ const tempPath = tempFiles[i];
210
+ const finalPath = path.join(commandsDir, commands[i].filename);
211
+ fs.renameSync(tempPath, finalPath);
212
+ }
213
+ }
214
+ catch (error) {
215
+ cleanup(tempFiles);
216
+ throw error;
217
+ }
218
+ }
219
+ async function updateChecksumInContent(content) {
220
+ // Use dynamic import for crypto in ESM
221
+ const crypto = await import('crypto');
222
+ const contentHash = crypto
223
+ .createHash('sha256')
224
+ .update(content)
225
+ .digest('hex')
226
+ .substring(0, 16);
227
+ // Update checksum in frontmatter
228
+ // First, remove any existing checksum lines
229
+ const withoutChecksum = content.replace(/^(\s*)checksum:\s*\S*$/gm, '');
230
+ // Then add the new checksum after the frontmatter opening
231
+ return withoutChecksum.replace(/^(---\n)([\s\S]*?)(---)/, `$1$2checksum: ${contentHash}\n$3`);
232
+ }
233
+ function cleanup(tempFiles) {
234
+ for (const file of tempFiles) {
235
+ try {
236
+ if (fs.existsSync(file)) {
237
+ fs.unlinkSync(file);
238
+ }
239
+ }
240
+ catch (error) {
241
+ console.warn(`āš ļø Could not clean up temporary file ${file}: ${error.message}`);
242
+ }
243
+ }
244
+ }
245
+ function displayInitializationSuccess(commands, workspaceStatus) {
246
+ console.log('āœ… Initialization Complete!\n');
247
+ if (commands.length > 0) {
248
+ console.log(`šŸ“¦ Installed ${commands.length} Claude command(s):\n`);
249
+ for (const command of commands) {
250
+ const commandName = command.filename.replace('.md', '');
251
+ console.log(` /${commandName} - ${command.metadata.description || 'Mod workflow command'}`);
252
+ }
253
+ console.log();
254
+ }
255
+ else {
256
+ console.log('āœ… All Claude commands are up to date\n');
257
+ }
258
+ if (workspaceStatus.created) {
259
+ console.log(`šŸš€ Created new workspace: "${workspaceStatus.workspaceName}"`);
260
+ }
261
+ else {
262
+ console.log(`šŸ“ Using existing workspace: "${workspaceStatus.workspaceName}"`);
263
+ }
264
+ console.log('\nšŸ’” Usage:');
265
+ console.log(' Open Claude Code and type "/" to see available commands');
266
+ console.log(' Use "mod workspace list" to see all workspaces');
267
+ console.log(' Commands are available in your .claude/commands directory\n');
268
+ }
269
+ async function checkWorkspaceStatus(repo) {
270
+ try {
271
+ const config = await readModConfig();
272
+ if (config && config.workspaceId) {
273
+ let workspaceName = 'Existing Workspace';
274
+ // Try to get the actual workspace name from the workspace service
275
+ if (repo) {
276
+ try {
277
+ const modWorkspace = createModWorkspace(repo);
278
+ const workspaces = await modWorkspace.listWorkspaces();
279
+ const currentWorkspace = workspaces.find(w => w.id === config.workspaceId);
280
+ if (currentWorkspace) {
281
+ workspaceName = currentWorkspace.name;
282
+ }
283
+ }
284
+ catch (error) {
285
+ // Fallback to generic name if we can't fetch workspace details
286
+ console.warn('āš ļø Could not fetch workspace name, using generic name');
287
+ }
288
+ }
289
+ return {
290
+ exists: true,
291
+ created: false,
292
+ workspaceName,
293
+ workspaceId: config.workspaceId
294
+ };
295
+ }
296
+ // No existing workspace found
297
+ return {
298
+ exists: false,
299
+ created: false,
300
+ workspaceName: 'New Workspace',
301
+ workspaceId: undefined
302
+ };
303
+ }
304
+ catch (error) {
305
+ // No config file exists, so no workspace
306
+ return {
307
+ exists: false,
308
+ created: false,
309
+ workspaceName: 'New Workspace',
310
+ workspaceId: undefined
311
+ };
312
+ }
313
+ }
314
+ async function handleWorkspaceInitialization(workspaceStatus, repo) {
315
+ if (workspaceStatus.exists) {
316
+ return workspaceStatus;
317
+ }
318
+ try {
319
+ const modWorkspace = createModWorkspace(repo);
320
+ const workspaceName = generateWorkspaceName();
321
+ console.log(`šŸš€ Creating new workspace: "${workspaceName}"...`);
322
+ const workspace = await modWorkspace.createWorkspace({
323
+ name: workspaceName
324
+ });
325
+ const config = {
326
+ workspaceId: workspace.id,
327
+ activeBranchId: undefined
328
+ };
329
+ await writeModConfig(config);
330
+ return {
331
+ exists: false,
332
+ created: true,
333
+ workspaceName: workspace.name,
334
+ workspaceId: workspace.id
335
+ };
336
+ }
337
+ catch (error) {
338
+ console.warn(`āš ļø Failed to create workspace: ${error.message}`);
339
+ console.log(' Continuing with command installation only.');
340
+ return {
341
+ exists: false,
342
+ created: false,
343
+ workspaceName: 'Failed to create',
344
+ workspaceId: undefined
345
+ };
346
+ }
347
+ }
348
+ function generateWorkspaceName() {
349
+ const currentDir = path.basename(process.cwd());
350
+ const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
351
+ // Use directory name if it's meaningful, otherwise use timestamp
352
+ if (currentDir && currentDir !== '.' && currentDir !== '/' && currentDir.length > 1) {
353
+ return currentDir.charAt(0).toUpperCase() + currentDir.slice(1);
354
+ }
355
+ return `Workspace-${timestamp}`;
356
+ }
357
+ function handleInstallationError(error) {
358
+ console.error('āŒ Initialization failed:');
359
+ if (error.code === 'EACCES' || error.code === 'EPERM') {
360
+ console.error(' Permission denied. Try running with appropriate permissions.');
361
+ console.error(' Make sure you have write access to the .claude/commands directory.');
362
+ }
363
+ else if (error.code === 'ENOSPC') {
364
+ console.error(' Insufficient disk space.');
365
+ }
366
+ else if (error.code === 'ENOTDIR') {
367
+ console.error(' Invalid directory structure. Ensure .claude is a directory, not a file.');
368
+ }
369
+ else {
370
+ console.error(` ${error.message}`);
371
+ }
372
+ console.error('\nšŸ”§ Troubleshooting:');
373
+ console.error(' - Ensure you have write permissions in the current directory');
374
+ console.error(' - Try running "mod init --force" to overwrite existing files');
375
+ console.error(' - Check that .claude is not a file (should be a directory)');
376
+ console.error(' - For workspace issues, try "mod workspace create <name>" separately');
377
+ process.exit(1);
378
+ }
@@ -0,0 +1,207 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { readModConfig, writeModConfig } from '../services/mod-config.js';
4
+ export async function recoverCommand(args, repo) {
5
+ const [action] = args;
6
+ if (!action || !['automerge', 'workspace', 'branches'].includes(action)) {
7
+ console.error('Usage: mod recover <automerge|workspace|branches>');
8
+ console.error(' automerge - Attempt to recover corrupted Automerge storage');
9
+ console.error(' workspace - Reset workspace configuration');
10
+ console.error(' branches - Recreate corrupted branch documents');
11
+ process.exit(1);
12
+ }
13
+ switch (action) {
14
+ case 'automerge':
15
+ await handleAutomergeRecovery(repo);
16
+ break;
17
+ case 'workspace':
18
+ await handleWorkspaceRecovery();
19
+ break;
20
+ case 'branches':
21
+ await handleBranchRecovery(repo);
22
+ break;
23
+ }
24
+ }
25
+ async function handleAutomergeRecovery(repo) {
26
+ console.log('šŸ”§ Attempting Automerge storage recovery...');
27
+ const storageDir = process.env.MOD_AUTOMERGE_STORAGE_DIR
28
+ ? path.resolve(process.env.MOD_AUTOMERGE_STORAGE_DIR)
29
+ : path.resolve('./.automerge-data');
30
+ if (!fs.existsSync(storageDir)) {
31
+ console.log('No Automerge storage directory found. Nothing to recover.');
32
+ return;
33
+ }
34
+ console.log(`Scanning storage directory: ${storageDir}`);
35
+ // Create backup of corrupted storage
36
+ const backupDir = `${storageDir}.backup.${Date.now()}`;
37
+ try {
38
+ console.log(`Creating backup at: ${backupDir}`);
39
+ fs.cpSync(storageDir, backupDir, { recursive: true });
40
+ }
41
+ catch (error) {
42
+ console.warn('Could not create backup:', error);
43
+ }
44
+ // Test document loading to identify corrupted documents
45
+ const corruptedDocs = [];
46
+ const validDocs = [];
47
+ const allDocIds = [];
48
+ const scanDirectory = (dir) => {
49
+ try {
50
+ const items = fs.readdirSync(dir);
51
+ for (const item of items) {
52
+ const itemPath = path.join(dir, item);
53
+ const stat = fs.statSync(itemPath);
54
+ if (stat.isDirectory()) {
55
+ scanDirectory(itemPath);
56
+ }
57
+ else if (itemPath.includes('/incremental/')) {
58
+ // This is an Automerge incremental storage file
59
+ // Extract document ID from path like: 3K/9QuxnF8NM8g71nvphZYzgxsxxJ/incremental/...
60
+ const pathParts = path.relative(storageDir, itemPath).split(path.sep);
61
+ if (pathParts.length >= 3 && pathParts[2] === 'incremental') {
62
+ const docId = `${pathParts[0]}${pathParts[1]}`;
63
+ if (!allDocIds.includes(docId)) {
64
+ allDocIds.push(docId);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch (error) {
71
+ console.warn(`Error scanning directory ${dir}:`, error);
72
+ }
73
+ };
74
+ scanDirectory(storageDir);
75
+ console.log(`Found ${allDocIds.length} document files to test...`);
76
+ // Test each document by actually trying to load it
77
+ for (const docId of allDocIds) {
78
+ try {
79
+ const handle = await repo.find(docId);
80
+ const doc = await handle.doc();
81
+ if (doc) {
82
+ validDocs.push(docId);
83
+ }
84
+ }
85
+ catch (error) {
86
+ const errorMessage = String(error?.message || error || '');
87
+ if (errorMessage.includes('corrupt') || errorMessage.includes('deflate') || errorMessage.includes('unable to parse chunk')) {
88
+ corruptedDocs.push(docId);
89
+ console.log(` šŸ’„ Corrupted: ${docId}`);
90
+ }
91
+ else {
92
+ // Other errors might not be corruption
93
+ console.warn(` āš ļø Error loading ${docId}: ${errorMessage.substring(0, 100)}`);
94
+ }
95
+ }
96
+ }
97
+ console.log(`Found ${validDocs.length} valid documents and ${corruptedDocs.length} potentially corrupted documents`);
98
+ if (corruptedDocs.length > 0) {
99
+ console.log('\nCorrupted documents detected:');
100
+ corruptedDocs.slice(0, 5).forEach(docId => {
101
+ console.log(` ${docId}`);
102
+ });
103
+ if (corruptedDocs.length > 5) {
104
+ console.log(` ... and ${corruptedDocs.length - 5} more`);
105
+ }
106
+ // Offer to remove corrupted documents
107
+ console.log('\nāš ļø This will remove corrupted documents and may cause data loss.');
108
+ console.log('šŸ’” Backup created at:', backupDir);
109
+ // Auto-proceed with recovery
110
+ console.log('Proceeding with corruption cleanup...');
111
+ let removedCount = 0;
112
+ for (const docId of corruptedDocs) {
113
+ try {
114
+ // Find and remove the corrupted document files
115
+ // DocId format: 3K9QuxnF8NM8g71nvphZYzgxsxxJ -> 3K/9QuxnF8NM8g71nvphZYzgxsxxJ
116
+ const prefix = docId.substring(0, 2);
117
+ const suffix = docId.substring(2);
118
+ const docDir = path.join(storageDir, prefix, suffix);
119
+ if (fs.existsSync(docDir)) {
120
+ // Remove the entire document directory
121
+ fs.rmSync(docDir, { recursive: true, force: true });
122
+ removedCount++;
123
+ console.log(` šŸ—‘ļø Removed document directory: ${prefix}/${suffix}`);
124
+ }
125
+ // Check if parent directory is empty
126
+ const parentDir = path.join(storageDir, prefix);
127
+ try {
128
+ const remainingFiles = fs.readdirSync(parentDir);
129
+ if (remainingFiles.length === 0) {
130
+ fs.rmdirSync(parentDir);
131
+ console.log(` šŸ—‘ļø Removed empty prefix directory: ${prefix}`);
132
+ }
133
+ }
134
+ catch { }
135
+ }
136
+ catch (error) {
137
+ console.warn(`Could not remove corrupted document ${docId}:`, error);
138
+ }
139
+ }
140
+ console.log(`āœ… Removed ${removedCount} corrupted document files`);
141
+ console.log('šŸ”„ Please restart the application and try your command again');
142
+ }
143
+ else {
144
+ console.log('āœ… No corrupted documents detected in Automerge storage');
145
+ }
146
+ }
147
+ async function handleWorkspaceRecovery() {
148
+ console.log('šŸ”§ Resetting workspace configuration...');
149
+ const configPath = '.mod/config.json';
150
+ if (fs.existsSync(configPath)) {
151
+ console.log(`Backing up current config to ${configPath}.backup`);
152
+ fs.copyFileSync(configPath, `${configPath}.backup`);
153
+ }
154
+ // Reset to minimal config
155
+ const minimalConfig = {};
156
+ writeModConfig(minimalConfig);
157
+ console.log('āœ… Workspace configuration reset');
158
+ console.log('šŸ’” Run "mod workspace list" to see available workspaces');
159
+ console.log('šŸ’” Run "mod workspace switch <workspace-name>" to select a workspace');
160
+ }
161
+ async function handleBranchRecovery(repo) {
162
+ console.log('šŸ”§ Attempting branch recovery...');
163
+ const cfg = readModConfig();
164
+ if (!cfg?.workspaceId) {
165
+ console.error('No workspace configured. Run "mod recover workspace" first.');
166
+ process.exit(1);
167
+ }
168
+ try {
169
+ // Try to access the workspace
170
+ const wsHandle = await repo.find(cfg.workspaceId);
171
+ const workspace = await wsHandle.doc();
172
+ if (!workspace) {
173
+ console.error('Workspace document is corrupted or missing');
174
+ console.log('šŸ’” Run "mod recover automerge" to fix storage corruption');
175
+ process.exit(1);
176
+ }
177
+ const workspaceData = workspace;
178
+ console.log(`āœ… Workspace "${workspaceData.title || 'Untitled'}" is accessible`);
179
+ // Try to access branches document
180
+ if (workspaceData.branchesDocId) {
181
+ try {
182
+ const branchesHandle = await repo.find(workspaceData.branchesDocId);
183
+ const branchesDoc = await branchesHandle.doc();
184
+ const branchesData = branchesDoc;
185
+ if (branchesData && branchesData.branches) {
186
+ console.log(`āœ… Found ${Object.keys(branchesData.branches).length} branches`);
187
+ console.log('Branches appear to be intact');
188
+ }
189
+ else {
190
+ console.log('āš ļø Branches document exists but appears empty');
191
+ }
192
+ }
193
+ catch (error) {
194
+ console.error('Branches document is corrupted:', error);
195
+ console.log('šŸ’” Run "mod recover automerge" to fix storage corruption');
196
+ }
197
+ }
198
+ else {
199
+ console.log('āš ļø Workspace has no branches document ID');
200
+ }
201
+ }
202
+ catch (error) {
203
+ console.error('Failed to access workspace:', error);
204
+ console.log('šŸ’” Run "mod recover automerge" to fix storage corruption');
205
+ process.exit(1);
206
+ }
207
+ }