@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,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
|
+
}
|