claude-comms 1.0.1 → 1.0.2
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/CHANGELOG.md +53 -0
- package/README.md +64 -23
- package/bin/claude-setup.js +118 -38
- package/package.json +65 -18
- package/src/cli/index.js +30 -0
- package/src/cli/interfaces.js +386 -0
- package/src/cli/parser.js +158 -0
- package/src/cli/prompts.js +317 -0
- package/src/cli/runner.js +436 -0
- package/src/fetcher/github.js +676 -0
- package/src/index.js +221 -0
- package/src/installer/dependency-check.js +19 -0
- package/src/installer/file-writer.js +648 -0
- package/src/orchestrator/installer.js +849 -0
- package/src/orchestrator/main.js +267 -0
- package/src/orchestrator/mocks/index.js +388 -0
- package/src/utils/config.js +430 -0
- package/src/utils/constants.js +397 -0
- package/src/utils/errors.js +361 -0
- package/src/utils/index.js +46 -0
- package/src/utils/logger.js +177 -0
- package/src/utils/platform.js +362 -0
- package/src/utils/retry.js +358 -0
- package/templates/settings.local.json +1 -0
- package/.claude/hooks/comms/messages_UmaMatrix.json +0 -10
- package/.claude/hooks/comms/messages_VictorSpiral.json +0 -10
- package/.claude/hooks/comms/messages_WendyQuantum.json +0 -10
- package/.claude/hooks/comms/messages_XavierPrism.json +0 -10
- package/templates/get_unread_messages.py +0 -54
- package/templates/send_message.py +0 -75
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Setup Installer - Main Module
|
|
3
|
+
*
|
|
4
|
+
* This module orchestrates the complete installation flow:
|
|
5
|
+
* 1. Validates target directory and dependencies
|
|
6
|
+
* 2. Fetches latest setup files from GitHub
|
|
7
|
+
* 3. Installs .claude directory and CLAUDE.md
|
|
8
|
+
* 4. Configures local settings
|
|
9
|
+
* 5. Provides post-installation guidance
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const fs = require('fs-extra');
|
|
14
|
+
const ora = require('ora');
|
|
15
|
+
|
|
16
|
+
// Import core modules (these will be implemented in WP02-06)
|
|
17
|
+
const logger = require('./utils/logger');
|
|
18
|
+
const { InstallationError } = require('./utils/errors');
|
|
19
|
+
const fetcher = require('./fetcher/github');
|
|
20
|
+
const fileWriter = require('./installer/file-writer');
|
|
21
|
+
const dependencyChecker = require('./installer/dependency-check');
|
|
22
|
+
const prompts = require('./cli/prompts');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Main installation function
|
|
26
|
+
* @param {Object} options - Installation options
|
|
27
|
+
* @param {string} options.targetDir - Target directory for installation
|
|
28
|
+
* @param {string} options.gitRef - Git reference (branch/tag/commit) to install
|
|
29
|
+
* @param {boolean} options.force - Force overwrite existing files
|
|
30
|
+
* @param {boolean} options.checkPython - Whether to check Python/uv dependencies
|
|
31
|
+
* @param {boolean} options.dev - Development mode with verbose logging
|
|
32
|
+
* @param {boolean} options.verify - Verification mode (no changes)
|
|
33
|
+
* @param {boolean} options.dryRun - Dry run mode (show what would be done)
|
|
34
|
+
*/
|
|
35
|
+
async function install(options = {}) {
|
|
36
|
+
const {
|
|
37
|
+
targetDir = process.cwd(),
|
|
38
|
+
gitRef = 'main',
|
|
39
|
+
force = false,
|
|
40
|
+
checkPython = true,
|
|
41
|
+
dev = false,
|
|
42
|
+
verify = false,
|
|
43
|
+
dryRun = false
|
|
44
|
+
} = options;
|
|
45
|
+
|
|
46
|
+
// Configure logger for development mode
|
|
47
|
+
logger.configure({ verbose: dev, verify, dryRun });
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
logger.info('Starting Claude Code installation...');
|
|
51
|
+
|
|
52
|
+
// Phase 1: Pre-installation validation
|
|
53
|
+
await validateEnvironment(targetDir, checkPython);
|
|
54
|
+
|
|
55
|
+
// Phase 2: Handle existing files
|
|
56
|
+
await handleExistingFiles(targetDir, force, verify, dryRun);
|
|
57
|
+
|
|
58
|
+
// Phase 3: Fetch setup files from GitHub
|
|
59
|
+
const setupFiles = await fetchSetupFiles(gitRef);
|
|
60
|
+
|
|
61
|
+
// Phase 4: Install files to target directory
|
|
62
|
+
await installFiles(setupFiles, targetDir, verify, dryRun);
|
|
63
|
+
|
|
64
|
+
// Phase 5: Post-installation configuration
|
|
65
|
+
await configureLocalSettings(targetDir, verify, dryRun);
|
|
66
|
+
|
|
67
|
+
// Phase 6: Verify installation
|
|
68
|
+
await verifyInstallation(targetDir);
|
|
69
|
+
|
|
70
|
+
logger.success('Installation completed successfully!');
|
|
71
|
+
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error('Installation failed:', error.message);
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate installation environment
|
|
80
|
+
*/
|
|
81
|
+
async function validateEnvironment(targetDir, checkPython) {
|
|
82
|
+
const spinner = ora('Validating environment...').start();
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// Check target directory exists and is writable
|
|
86
|
+
await fs.ensureDir(targetDir);
|
|
87
|
+
await fs.access(targetDir, fs.constants.W_OK);
|
|
88
|
+
|
|
89
|
+
// Check dependencies if requested
|
|
90
|
+
if (checkPython) {
|
|
91
|
+
await dependencyChecker.validatePythonEnvironment();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
spinner.succeed('Environment validation complete');
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
spinner.fail('Environment validation failed');
|
|
98
|
+
throw new InstallationError(`Environment validation failed: ${error.message}`, 'ENV_VALIDATION_FAILED');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Handle existing .claude directory and CLAUDE.md
|
|
104
|
+
*/
|
|
105
|
+
async function handleExistingFiles(targetDir, force, verify, dryRun) {
|
|
106
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
107
|
+
const claudeMd = path.join(targetDir, 'CLAUDE.md');
|
|
108
|
+
|
|
109
|
+
const existingFiles = [];
|
|
110
|
+
if (await fs.pathExists(claudeDir)) existingFiles.push('.claude/');
|
|
111
|
+
if (await fs.pathExists(claudeMd)) existingFiles.push('CLAUDE.md');
|
|
112
|
+
|
|
113
|
+
if (existingFiles.length === 0) {
|
|
114
|
+
logger.info('No existing files found - proceeding with fresh installation');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (verify || dryRun) {
|
|
119
|
+
logger.info(`Found existing files: ${existingFiles.join(', ')}`);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (force) {
|
|
124
|
+
logger.info(`Force mode enabled - will overwrite: ${existingFiles.join(', ')}`);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Interactive prompt for handling existing files
|
|
129
|
+
const action = await prompts.handleExistingFiles(existingFiles);
|
|
130
|
+
|
|
131
|
+
switch (action) {
|
|
132
|
+
case 'backup':
|
|
133
|
+
await fileWriter.backupExistingFiles(targetDir, existingFiles);
|
|
134
|
+
break;
|
|
135
|
+
case 'skip':
|
|
136
|
+
logger.info('Skipping existing files');
|
|
137
|
+
break;
|
|
138
|
+
case 'cancel':
|
|
139
|
+
throw new InstallationError('Installation cancelled by user', 'USER_CANCELLED');
|
|
140
|
+
default:
|
|
141
|
+
// Overwrite - no action needed
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fetch setup files from GitHub repository
|
|
148
|
+
*/
|
|
149
|
+
async function fetchSetupFiles(gitRef) {
|
|
150
|
+
const spinner = ora(`Fetching setup files from GitHub (${gitRef})...`).start();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const files = await fetcher.fetchClaudeSetup(gitRef);
|
|
154
|
+
|
|
155
|
+
spinner.succeed(`Fetched ${Object.keys(files).length} files from GitHub`);
|
|
156
|
+
logger.debug('Fetched files:', Object.keys(files));
|
|
157
|
+
|
|
158
|
+
return files;
|
|
159
|
+
|
|
160
|
+
} catch (error) {
|
|
161
|
+
spinner.fail('Failed to fetch setup files');
|
|
162
|
+
throw new InstallationError(`GitHub fetch failed: ${error.message}`, 'GITHUB_FETCH_FAILED');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Install files to target directory
|
|
168
|
+
*/
|
|
169
|
+
async function installFiles(setupFiles, targetDir, verify, dryRun) {
|
|
170
|
+
const spinner = ora('Installing files...').start();
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const stats = await fileWriter.writeSetupFiles(setupFiles, targetDir, { verify, dryRun });
|
|
174
|
+
|
|
175
|
+
if (verify || dryRun) {
|
|
176
|
+
spinner.succeed(`Would install ${stats.fileCount} files (${stats.totalSize} bytes)`);
|
|
177
|
+
} else {
|
|
178
|
+
spinner.succeed(`Installed ${stats.fileCount} files (${stats.totalSize} bytes)`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
} catch (error) {
|
|
182
|
+
spinner.fail('File installation failed');
|
|
183
|
+
throw new InstallationError(`File installation failed: ${error.message}`, 'FILE_INSTALL_FAILED');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Configure local settings file
|
|
189
|
+
*/
|
|
190
|
+
async function configureLocalSettings(targetDir, verify, dryRun) {
|
|
191
|
+
const spinner = ora('Configuring local settings...').start();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const settingsPath = path.join(targetDir, '.claude', 'settings.local.json');
|
|
195
|
+
|
|
196
|
+
if (verify || dryRun) {
|
|
197
|
+
spinner.succeed('Would create settings.local.json');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Create basic local settings template
|
|
202
|
+
const defaultSettings = {
|
|
203
|
+
'observability': {
|
|
204
|
+
'server_url': 'http://localhost:3001',
|
|
205
|
+
'enabled': true
|
|
206
|
+
},
|
|
207
|
+
'hooks': {
|
|
208
|
+
'enabled': true,
|
|
209
|
+
'python_path': 'python3'
|
|
210
|
+
},
|
|
211
|
+
'agents': {
|
|
212
|
+
'max_concurrent': 10,
|
|
213
|
+
'timeout': 300
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
await fs.writeJson(settingsPath, defaultSettings, { spaces: 2 });
|
|
218
|
+
|
|
219
|
+
spinner.succeed('Local settings configured');
|
|
220
|
+
|
|
221
|
+
} catch (error) {
|
|
222
|
+
spinner.fail('Settings configuration failed');
|
|
223
|
+
throw new InstallationError(`Settings configuration failed: ${error.message}`, 'SETTINGS_CONFIG_FAILED');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Verify installation was successful
|
|
229
|
+
*/
|
|
230
|
+
async function verifyInstallation(targetDir) {
|
|
231
|
+
const spinner = ora('Verifying installation...').start();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
235
|
+
const claudeMd = path.join(targetDir, 'CLAUDE.md');
|
|
236
|
+
const settingsPath = path.join(claudeDir, 'settings.local.json');
|
|
237
|
+
|
|
238
|
+
// Check required files exist
|
|
239
|
+
const checks = [
|
|
240
|
+
{ path: claudeDir, name: '.claude directory' },
|
|
241
|
+
{ path: claudeMd, name: 'CLAUDE.md' },
|
|
242
|
+
{ path: settingsPath, name: 'settings.local.json' }
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
for (const check of checks) {
|
|
246
|
+
if (!(await fs.pathExists(check.path))) {
|
|
247
|
+
throw new Error(`Missing ${check.name}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
spinner.succeed('Installation verification complete');
|
|
252
|
+
|
|
253
|
+
} catch (error) {
|
|
254
|
+
spinner.fail('Installation verification failed');
|
|
255
|
+
throw new InstallationError(`Installation verification failed: ${error.message}`, 'VERIFICATION_FAILED');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
install,
|
|
261
|
+
validateEnvironment,
|
|
262
|
+
handleExistingFiles,
|
|
263
|
+
fetchSetupFiles,
|
|
264
|
+
installFiles,
|
|
265
|
+
configureLocalSettings,
|
|
266
|
+
verifyInstallation
|
|
267
|
+
};
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock Implementations for Team Integration - WP06
|
|
3
|
+
*
|
|
4
|
+
* Provides mock implementations that match the interface contracts
|
|
5
|
+
* for integration with WP03 (GitHub Fetcher) and WP04 (File Writer).
|
|
6
|
+
*
|
|
7
|
+
* These mocks allow the orchestrator to be tested independently
|
|
8
|
+
* while providing clear integration points for the real implementations.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mock Fetcher implementation matching FetcherAPI interface
|
|
13
|
+
* Will be replaced by IsabelMatrix's GitHub fetcher (WP03)
|
|
14
|
+
*/
|
|
15
|
+
class MockFetcher {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.mockData = options.mockData || this._getDefaultMockData();
|
|
18
|
+
this.simulateDelay = options.simulateDelay !== false;
|
|
19
|
+
this.shouldFail = options.shouldFail || false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async fetchDirectory(path, _options = {}) {
|
|
23
|
+
if (this.simulateDelay) {
|
|
24
|
+
await this._delay(500);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (this.shouldFail) {
|
|
28
|
+
throw new Error(`Mock fetcher: Failed to fetch directory ${path}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return this.mockData.directories[path] || {
|
|
32
|
+
path,
|
|
33
|
+
type: 'dir',
|
|
34
|
+
children: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async fetchFile(path, _options = {}) {
|
|
39
|
+
if (this.simulateDelay) {
|
|
40
|
+
await this._delay(200);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (this.shouldFail) {
|
|
44
|
+
throw new Error(`Mock fetcher: Failed to fetch file ${path}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.mockData.files[path] || {
|
|
48
|
+
path,
|
|
49
|
+
content: `# Mock content for ${path}`,
|
|
50
|
+
encoding: 'utf-8',
|
|
51
|
+
sha: 'mock-sha-' + Date.now()
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async validateConnection() {
|
|
56
|
+
if (this.simulateDelay) {
|
|
57
|
+
await this._delay(100);
|
|
58
|
+
}
|
|
59
|
+
return !this.shouldFail;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_delay(ms) {
|
|
63
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_getDefaultMockData() {
|
|
67
|
+
return {
|
|
68
|
+
directories: {
|
|
69
|
+
'.claude': {
|
|
70
|
+
path: '.claude',
|
|
71
|
+
type: 'dir',
|
|
72
|
+
children: [
|
|
73
|
+
{
|
|
74
|
+
path: 'settings.json',
|
|
75
|
+
type: 'file',
|
|
76
|
+
content: JSON.stringify({
|
|
77
|
+
version: '1.0.0',
|
|
78
|
+
observability_server: 'http://localhost:4000',
|
|
79
|
+
hooks_enabled: true
|
|
80
|
+
}, null, 2),
|
|
81
|
+
encoding: 'utf-8'
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
path: 'agents',
|
|
85
|
+
type: 'dir',
|
|
86
|
+
children: [
|
|
87
|
+
{
|
|
88
|
+
path: 'engineer.md',
|
|
89
|
+
type: 'file',
|
|
90
|
+
content: '# Engineer Agent\n\nImplementation specialist.',
|
|
91
|
+
encoding: 'utf-8'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
path: 'architect.md',
|
|
95
|
+
type: 'file',
|
|
96
|
+
content: '# Architect Agent\n\nSystem design specialist.',
|
|
97
|
+
encoding: 'utf-8'
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: 'hooks',
|
|
103
|
+
type: 'dir',
|
|
104
|
+
children: [
|
|
105
|
+
{
|
|
106
|
+
path: 'comms',
|
|
107
|
+
type: 'dir',
|
|
108
|
+
children: [
|
|
109
|
+
{
|
|
110
|
+
path: 'send_message.py',
|
|
111
|
+
type: 'file',
|
|
112
|
+
content: '#!/usr/bin/env python3\n# Team communication hook\nprint("Send message hook")',
|
|
113
|
+
encoding: 'utf-8',
|
|
114
|
+
permissions: 0o755
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: 'get_unread_messages.py',
|
|
118
|
+
type: 'file',
|
|
119
|
+
content: '#!/usr/bin/env python3\n# Message retrieval hook\nprint("Get messages hook")',
|
|
120
|
+
encoding: 'utf-8',
|
|
121
|
+
permissions: 0o755
|
|
122
|
+
}
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
files: {
|
|
131
|
+
'CLAUDE.md': {
|
|
132
|
+
path: 'CLAUDE.md',
|
|
133
|
+
content: '# Claude Code Multi-Agent Setup\n\nThis project is configured for Claude multi-agent orchestration.\n\n## Features\n- Multi-agent team coordination\n- Observability and monitoring\n- Hook-based extensibility\n\n## Getting Started\nReview .claude/settings.local.json for configuration.',
|
|
134
|
+
encoding: 'utf-8',
|
|
135
|
+
sha: 'mock-claude-md-sha'
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Mock Writer implementation matching WriterAPI interface
|
|
144
|
+
* Will be replaced by JackTensor's file writer (WP04)
|
|
145
|
+
*/
|
|
146
|
+
class MockWriter {
|
|
147
|
+
constructor(options = {}) {
|
|
148
|
+
this.simulateDelay = options.simulateDelay !== false;
|
|
149
|
+
this.shouldFail = options.shouldFail || false;
|
|
150
|
+
this.dryRun = options.dryRun || false;
|
|
151
|
+
this.writtenFiles = new Set();
|
|
152
|
+
this.backups = new Map();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async writeFile(filePath, content, options = {}) {
|
|
156
|
+
if (this.simulateDelay) {
|
|
157
|
+
await this._delay(100);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (this.shouldFail) {
|
|
161
|
+
throw new Error(`Mock writer: Failed to write file ${filePath}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (options.dryRun || this.dryRun) {
|
|
165
|
+
console.log(`[DRY RUN] Would write file: ${filePath}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this.writtenFiles.add(filePath);
|
|
170
|
+
console.log(`[MOCK] Wrote file: ${filePath} (${content.length} bytes)`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async writeDirectory(files, options = {}) {
|
|
174
|
+
if (this.simulateDelay) {
|
|
175
|
+
await this._delay(300);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (this.shouldFail) {
|
|
179
|
+
throw new Error('Mock writer: Failed to write directory');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const result = {
|
|
183
|
+
written: [],
|
|
184
|
+
skipped: [],
|
|
185
|
+
backed_up: new Map(),
|
|
186
|
+
errors: []
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const file of files) {
|
|
190
|
+
const targetPath = options.targetDir
|
|
191
|
+
? require('path').join(options.targetDir, file.path)
|
|
192
|
+
: file.path;
|
|
193
|
+
|
|
194
|
+
if (options.dryRun || this.dryRun) {
|
|
195
|
+
console.log(`[DRY RUN] Would write: ${targetPath}`);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Simulate backup if file exists and backup is enabled
|
|
200
|
+
if (options.backup && this.writtenFiles.has(targetPath)) {
|
|
201
|
+
const backupPath = `${targetPath}.backup-${Date.now()}`;
|
|
202
|
+
result.backed_up.set(targetPath, backupPath);
|
|
203
|
+
this.backups.set(targetPath, backupPath);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
this.writtenFiles.add(targetPath);
|
|
207
|
+
result.written.push(targetPath);
|
|
208
|
+
|
|
209
|
+
console.log(`[MOCK] Wrote: ${targetPath} (${file.content?.length || 0} bytes)`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async backup(filePath) {
|
|
216
|
+
if (this.simulateDelay) {
|
|
217
|
+
await this._delay(50);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const backupPath = `${filePath}.backup-${Date.now()}`;
|
|
221
|
+
this.backups.set(filePath, backupPath);
|
|
222
|
+
|
|
223
|
+
console.log(`[MOCK] Created backup: ${filePath} -> ${backupPath}`);
|
|
224
|
+
return backupPath;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async rollback(backupPath, originalPath) {
|
|
228
|
+
if (this.simulateDelay) {
|
|
229
|
+
await this._delay(100);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`[MOCK] Rolled back: ${backupPath} -> ${originalPath}`);
|
|
233
|
+
this.writtenFiles.delete(originalPath);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async removeFile(filePath) {
|
|
237
|
+
if (this.simulateDelay) {
|
|
238
|
+
await this._delay(50);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
console.log(`[MOCK] Removed file: ${filePath}`);
|
|
242
|
+
this.writtenFiles.delete(filePath);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
_delay(ms) {
|
|
246
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Mock Logger implementation
|
|
252
|
+
*/
|
|
253
|
+
class MockLogger {
|
|
254
|
+
constructor(options = {}) {
|
|
255
|
+
this.verbose = options.verbose || false;
|
|
256
|
+
this.prefix = options.prefix || '[MOCK]';
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
info(message, ...args) {
|
|
260
|
+
console.log(`${this.prefix} [INFO] ${message}`, ...args);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
warn(message, ...args) {
|
|
264
|
+
console.warn(`${this.prefix} [WARN] ${message}`, ...args);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
error(message, ...args) {
|
|
268
|
+
console.error(`${this.prefix} [ERROR] ${message}`, ...args);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
debug(message, ...args) {
|
|
272
|
+
if (this.verbose) {
|
|
273
|
+
console.log(`${this.prefix} [DEBUG] ${message}`, ...args);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
success(message, ...args) {
|
|
278
|
+
console.log(`${this.prefix} [SUCCESS] ${message}`, ...args);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Mock Validator implementation
|
|
284
|
+
*/
|
|
285
|
+
class MockValidator {
|
|
286
|
+
constructor(options = {}) {
|
|
287
|
+
this.shouldFail = options.shouldFail || false;
|
|
288
|
+
this.issues = options.issues || [];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async validateTargetDirectory(_targetDir) {
|
|
292
|
+
if (this.shouldFail) {
|
|
293
|
+
return {
|
|
294
|
+
code: 'E301',
|
|
295
|
+
message: 'Target directory is not writable',
|
|
296
|
+
severity: 'error'
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async validatePythonEnvironment() {
|
|
303
|
+
if (this.shouldFail) {
|
|
304
|
+
return {
|
|
305
|
+
code: 'E302',
|
|
306
|
+
message: 'Python or uv not found',
|
|
307
|
+
severity: 'error'
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async checkExistingFiles(_targetDir) {
|
|
314
|
+
// Return any pre-configured issues
|
|
315
|
+
return this.issues.find(issue => issue.type === 'existing_files') || null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async validateNetworkAccess() {
|
|
319
|
+
if (this.shouldFail) {
|
|
320
|
+
return {
|
|
321
|
+
code: 'E101',
|
|
322
|
+
message: 'Cannot connect to GitHub',
|
|
323
|
+
severity: 'error'
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async pathExists(_path) {
|
|
330
|
+
// Mock - assume paths exist unless configured otherwise
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Factory function to create a complete set of mock dependencies
|
|
337
|
+
*/
|
|
338
|
+
function createMockDependencies(options = {}) {
|
|
339
|
+
return {
|
|
340
|
+
fetcher: new MockFetcher(options.fetcher),
|
|
341
|
+
writer: new MockWriter(options.writer),
|
|
342
|
+
logger: new MockLogger(options.logger),
|
|
343
|
+
validator: new MockValidator(options.validator)
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Helper to create mock dependencies that simulate specific scenarios
|
|
349
|
+
*/
|
|
350
|
+
function createScenarioMocks(scenario) {
|
|
351
|
+
switch (scenario) {
|
|
352
|
+
case 'success':
|
|
353
|
+
return createMockDependencies();
|
|
354
|
+
|
|
355
|
+
case 'network_failure':
|
|
356
|
+
return createMockDependencies({
|
|
357
|
+
fetcher: { shouldFail: true },
|
|
358
|
+
validator: { shouldFail: true }
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
case 'write_failure':
|
|
362
|
+
return createMockDependencies({
|
|
363
|
+
writer: { shouldFail: true }
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
case 'validation_failure':
|
|
367
|
+
return createMockDependencies({
|
|
368
|
+
validator: { shouldFail: true }
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
case 'dry_run':
|
|
372
|
+
return createMockDependencies({
|
|
373
|
+
writer: { dryRun: true }
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
default:
|
|
377
|
+
return createMockDependencies();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
module.exports = {
|
|
382
|
+
MockFetcher,
|
|
383
|
+
MockWriter,
|
|
384
|
+
MockLogger,
|
|
385
|
+
MockValidator,
|
|
386
|
+
createMockDependencies,
|
|
387
|
+
createScenarioMocks
|
|
388
|
+
};
|