@paulduvall/claude-dev-toolkit 0.0.1-alpha.2 → 0.0.1-alpha.21
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/LICENSE +21 -0
- package/README.md +88 -37
- package/bin/claude-commands +307 -65
- package/commands/active/xarchitecture.md +393 -0
- package/commands/active/xconfig.md +127 -0
- package/commands/active/xcontinue.md +92 -0
- package/commands/active/xdebug.md +130 -0
- package/commands/active/xdocs.md +178 -0
- package/commands/active/xexplore.md +94 -0
- package/commands/active/xgit.md +149 -0
- package/commands/active/xpipeline.md +152 -0
- package/commands/active/xquality.md +96 -0
- package/commands/active/xrefactor.md +198 -0
- package/commands/active/xrelease.md +142 -0
- package/commands/active/xsecurity.md +92 -0
- package/commands/active/xspec.md +174 -0
- package/commands/active/xtdd.md +151 -0
- package/commands/active/xtest.md +89 -0
- package/commands/active/xverify.md +80 -0
- package/commands/experiments/xact.md +742 -0
- package/commands/experiments/xanalytics.md +113 -0
- package/commands/experiments/xanalyze.md +70 -0
- package/commands/experiments/xapi.md +161 -0
- package/commands/experiments/xatomic.md +112 -0
- package/commands/experiments/xaws.md +85 -0
- package/commands/experiments/xcicd.md +337 -0
- package/commands/experiments/xcommit.md +122 -0
- package/commands/experiments/xcompliance.md +182 -0
- package/commands/experiments/xconstraints.md +89 -0
- package/commands/experiments/xcoverage.md +90 -0
- package/commands/experiments/xdb.md +102 -0
- package/commands/experiments/xdesign.md +121 -0
- package/commands/experiments/xdevcontainer.md +238 -0
- package/commands/experiments/xevaluate.md +111 -0
- package/commands/experiments/xfootnote.md +12 -0
- package/commands/experiments/xgenerate.md +117 -0
- package/commands/experiments/xgovernance.md +149 -0
- package/commands/experiments/xgreen.md +66 -0
- package/commands/experiments/xiac.md +118 -0
- package/commands/experiments/xincident.md +137 -0
- package/commands/experiments/xinfra.md +115 -0
- package/commands/experiments/xknowledge.md +115 -0
- package/commands/experiments/xmaturity.md +120 -0
- package/commands/experiments/xmetrics.md +118 -0
- package/commands/experiments/xmonitoring.md +128 -0
- package/commands/experiments/xnew.md +903 -0
- package/commands/experiments/xobservable.md +114 -0
- package/commands/experiments/xoidc.md +165 -0
- package/commands/experiments/xoptimize.md +115 -0
- package/commands/experiments/xperformance.md +112 -0
- package/commands/experiments/xplanning.md +131 -0
- package/commands/experiments/xpolicy.md +115 -0
- package/commands/experiments/xproduct.md +98 -0
- package/commands/experiments/xreadiness.md +75 -0
- package/commands/experiments/xred.md +55 -0
- package/commands/experiments/xrisk.md +128 -0
- package/commands/experiments/xrules.md +124 -0
- package/commands/experiments/xsandbox.md +120 -0
- package/commands/experiments/xscan.md +102 -0
- package/commands/experiments/xsetup.md +123 -0
- package/commands/experiments/xtemplate.md +116 -0
- package/commands/experiments/xtrace.md +212 -0
- package/commands/experiments/xux.md +171 -0
- package/commands/experiments/xvalidate.md +104 -0
- package/commands/experiments/xworkflow.md +113 -0
- package/hooks/.smellrc.example.json +19 -0
- package/hooks/README.md +263 -0
- package/hooks/check-commit-signing.py +127 -0
- package/hooks/check-complexity.py +38 -0
- package/hooks/check-security.py +37 -0
- package/hooks/claude-wrapper.sh +29 -0
- package/hooks/config.py +110 -0
- package/hooks/file-logger.sh +100 -0
- package/hooks/lib/argument-parser.sh +427 -0
- package/hooks/lib/config-constants.sh +230 -0
- package/hooks/lib/context-manager.sh +560 -0
- package/hooks/lib/error-handler.sh +423 -0
- package/hooks/lib/execution-engine.sh +444 -0
- package/hooks/lib/execution-results.sh +113 -0
- package/hooks/lib/execution-simulation.sh +114 -0
- package/hooks/lib/field-validators.sh +104 -0
- package/hooks/lib/file-utils.sh +398 -0
- package/hooks/lib/subagent-discovery.sh +468 -0
- package/hooks/lib/subagent-validator.sh +407 -0
- package/hooks/lib/validation-reporter.sh +134 -0
- package/hooks/on-error-debug.sh +226 -0
- package/hooks/pre-commit-quality.sh +204 -0
- package/hooks/pre-commit-test-runner.sh +132 -0
- package/hooks/pre-write-security.sh +115 -0
- package/hooks/prevent-credential-exposure.sh +279 -0
- package/hooks/security_bandit.py +177 -0
- package/hooks/security_checks.py +97 -0
- package/hooks/security_secrets.py +81 -0
- package/hooks/security_trojan.py +61 -0
- package/hooks/settings.example.json +52 -0
- package/hooks/smell_checks.py +238 -0
- package/hooks/smell_javascript.py +231 -0
- package/hooks/smell_python.py +110 -0
- package/hooks/smell_ruff.py +70 -0
- package/hooks/smell_types.py +72 -0
- package/hooks/subagent-trigger-simple.sh +202 -0
- package/hooks/subagent-trigger.sh +253 -0
- package/hooks/suppression.py +82 -0
- package/hooks/tab-color.sh +70 -0
- package/hooks/verify-before-edit.sh +135 -0
- package/lib/backup-restore-command.js +140 -0
- package/lib/base/base-command.js +252 -0
- package/lib/base/command-result.js +184 -0
- package/lib/config/constants.js +255 -0
- package/lib/config.js +48 -6
- package/lib/configure-command.js +428 -0
- package/lib/dependency-validator.js +64 -5
- package/lib/hook-installer-core.js +2 -2
- package/lib/installation-instruction-generator.js +213 -495
- package/lib/installer.js +134 -56
- package/lib/oidc-command.js +740 -0
- package/lib/services/backup-list-service.js +226 -0
- package/lib/services/backup-service.js +230 -0
- package/lib/services/command-installer-service.js +217 -0
- package/lib/services/logger-service.js +201 -0
- package/lib/services/package-manager-service.js +319 -0
- package/lib/services/platform-instruction-service.js +294 -0
- package/lib/services/recovery-instruction-service.js +348 -0
- package/lib/services/restore-service.js +221 -0
- package/lib/setup-command.js +359 -0
- package/lib/setup-wizard.js +155 -262
- package/lib/uninstall-command.js +100 -0
- package/lib/utils/claude-path-config.js +184 -0
- package/lib/utils/file-system-utils.js +152 -0
- package/lib/utils.js +8 -4
- package/lib/verify-command.js +430 -0
- package/package.json +7 -3
- package/scripts/postinstall.js +172 -157
- package/subagents/debug-specialist.md +7 -0
- package/templates/README.md +115 -0
- package/templates/basic-settings.json +30 -0
- package/templates/comprehensive-settings.json +57 -0
- package/templates/global-claude.md +344 -0
- package/templates/hybrid-hook-config.yaml +132 -0
- package/templates/security-focused-settings.json +62 -0
- package/templates/subagent-hooks.yaml +188 -0
- package/lib/package-manager-service.js +0 -270
- package/subagents/debug-context.md +0 -197
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup List Service
|
|
3
|
+
* Service for listing and managing backup inventory
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const FileSystemUtils = require('../utils/file-system-utils');
|
|
9
|
+
const ClaudePathConfig = require('../utils/claude-path-config');
|
|
10
|
+
|
|
11
|
+
class BackupListService {
|
|
12
|
+
constructor(config = null) {
|
|
13
|
+
this.config = config || new ClaudePathConfig();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* List all available backups
|
|
18
|
+
*/
|
|
19
|
+
async list() {
|
|
20
|
+
if (!fs.existsSync(this.config.backupsDir)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const backups = [];
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const entries = fs.readdirSync(this.config.backupsDir);
|
|
28
|
+
|
|
29
|
+
for (const entry of entries) {
|
|
30
|
+
const fullPath = path.join(this.config.backupsDir, entry);
|
|
31
|
+
const stats = FileSystemUtils.getStats(fullPath);
|
|
32
|
+
|
|
33
|
+
if (!stats) continue;
|
|
34
|
+
|
|
35
|
+
// Compressed backups
|
|
36
|
+
if (entry.endsWith('.tar.gz')) {
|
|
37
|
+
const name = entry.replace('.tar.gz', '');
|
|
38
|
+
backups.push({
|
|
39
|
+
name,
|
|
40
|
+
type: 'compressed',
|
|
41
|
+
size: stats.size,
|
|
42
|
+
modified: stats.mtime,
|
|
43
|
+
path: fullPath
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Directory backups
|
|
47
|
+
else if (stats.isDirectory() && !entry.startsWith('.')) {
|
|
48
|
+
const metadata = await this.readBackupMetadata(fullPath);
|
|
49
|
+
|
|
50
|
+
backups.push({
|
|
51
|
+
name: entry,
|
|
52
|
+
type: 'directory',
|
|
53
|
+
size: FileSystemUtils.getDirectorySize(fullPath),
|
|
54
|
+
modified: stats.mtime,
|
|
55
|
+
path: fullPath,
|
|
56
|
+
metadata
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Sort by modification time (newest first)
|
|
62
|
+
backups.sort((a, b) => b.modified - a.modified);
|
|
63
|
+
|
|
64
|
+
return backups;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new Error(`Failed to list backups: ${error.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Display backups in user-friendly format
|
|
72
|
+
*/
|
|
73
|
+
async display() {
|
|
74
|
+
console.log('📦 Available Backups:\n');
|
|
75
|
+
|
|
76
|
+
const backups = await this.list();
|
|
77
|
+
|
|
78
|
+
if (backups.length === 0) {
|
|
79
|
+
console.log('No backups found');
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
backups.forEach(backup => {
|
|
84
|
+
const date = backup.modified.toLocaleString();
|
|
85
|
+
const size = FileSystemUtils.formatSize(backup.size);
|
|
86
|
+
const type = backup.type === 'compressed' ? '📦' : '📁';
|
|
87
|
+
|
|
88
|
+
console.log(`${type} ${backup.name}`);
|
|
89
|
+
console.log(` Date: ${date}`);
|
|
90
|
+
console.log(` Size: ${size}`);
|
|
91
|
+
|
|
92
|
+
if (backup.metadata) {
|
|
93
|
+
console.log(` Files: ${backup.metadata.totalFiles}`);
|
|
94
|
+
if (backup.metadata.components) {
|
|
95
|
+
const components = Object.keys(backup.metadata.components).filter(k => backup.metadata.components[k]);
|
|
96
|
+
if (components.length > 0) {
|
|
97
|
+
console.log(` Contains: ${components.join(', ')}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log('');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
console.log(`Total: ${backups.length} backup(s)`);
|
|
105
|
+
console.log('\n💡 To restore a backup, run:');
|
|
106
|
+
console.log(' claude-commands restore <backup-name>');
|
|
107
|
+
|
|
108
|
+
return backups;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Find a specific backup by name
|
|
113
|
+
*/
|
|
114
|
+
async findBackup(name) {
|
|
115
|
+
const backups = await this.list();
|
|
116
|
+
return backups.find(backup => backup.name === name);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Get backup details
|
|
121
|
+
*/
|
|
122
|
+
async getBackupDetails(name) {
|
|
123
|
+
const backup = await this.findBackup(name);
|
|
124
|
+
|
|
125
|
+
if (!backup) {
|
|
126
|
+
throw new Error(`Backup '${name}' not found`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const details = {
|
|
130
|
+
...backup,
|
|
131
|
+
exists: fs.existsSync(backup.path),
|
|
132
|
+
readable: FileSystemUtils.isReadable(backup.path)
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Add component details if metadata available
|
|
136
|
+
if (backup.metadata && backup.metadata.components) {
|
|
137
|
+
details.components = backup.metadata.components;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return details;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Read backup metadata if available
|
|
145
|
+
*/
|
|
146
|
+
async readBackupMetadata(backupPath) {
|
|
147
|
+
const metadataPath = path.join(backupPath, 'backup-metadata.json');
|
|
148
|
+
|
|
149
|
+
if (!FileSystemUtils.isReadable(metadataPath)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const content = FileSystemUtils.readFile(metadataPath);
|
|
155
|
+
return JSON.parse(content);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
// Return null for invalid metadata
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Clean up old backups (keep only N most recent)
|
|
164
|
+
*/
|
|
165
|
+
async cleanup(keepCount = 10) {
|
|
166
|
+
const backups = await this.list();
|
|
167
|
+
|
|
168
|
+
if (backups.length <= keepCount) {
|
|
169
|
+
return { cleaned: 0, kept: backups.length };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const toRemove = backups.slice(keepCount);
|
|
173
|
+
let cleanedCount = 0;
|
|
174
|
+
|
|
175
|
+
for (const backup of toRemove) {
|
|
176
|
+
try {
|
|
177
|
+
if (FileSystemUtils.remove(backup.path)) {
|
|
178
|
+
cleanedCount++;
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠️ Warning: Could not remove backup ${backup.name} - ${error.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
cleaned: cleanedCount,
|
|
187
|
+
kept: backups.length - cleanedCount
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get backup statistics
|
|
193
|
+
*/
|
|
194
|
+
async getStats() {
|
|
195
|
+
const backups = await this.list();
|
|
196
|
+
|
|
197
|
+
let totalSize = 0;
|
|
198
|
+
let oldestDate = null;
|
|
199
|
+
let newestDate = null;
|
|
200
|
+
const types = { compressed: 0, directory: 0 };
|
|
201
|
+
|
|
202
|
+
backups.forEach(backup => {
|
|
203
|
+
totalSize += backup.size;
|
|
204
|
+
types[backup.type]++;
|
|
205
|
+
|
|
206
|
+
if (!oldestDate || backup.modified < oldestDate) {
|
|
207
|
+
oldestDate = backup.modified;
|
|
208
|
+
}
|
|
209
|
+
if (!newestDate || backup.modified > newestDate) {
|
|
210
|
+
newestDate = backup.modified;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
count: backups.length,
|
|
216
|
+
totalSize,
|
|
217
|
+
formattedSize: FileSystemUtils.formatSize(totalSize),
|
|
218
|
+
types,
|
|
219
|
+
oldestDate,
|
|
220
|
+
newestDate,
|
|
221
|
+
averageSize: backups.length > 0 ? totalSize / backups.length : 0
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = BackupListService;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Service
|
|
3
|
+
* Focused service for creating backups of Claude Code configuration
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const FileSystemUtils = require('../utils/file-system-utils');
|
|
9
|
+
const ClaudePathConfig = require('../utils/claude-path-config');
|
|
10
|
+
|
|
11
|
+
class BackupService {
|
|
12
|
+
constructor(config = null) {
|
|
13
|
+
this.config = config || new ClaudePathConfig();
|
|
14
|
+
this.backedUpCount = 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a complete backup
|
|
19
|
+
*/
|
|
20
|
+
async create(name = null) {
|
|
21
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('.')[0];
|
|
22
|
+
const backupName = name || `backup-${timestamp}`;
|
|
23
|
+
const backupPath = path.join(this.config.backupsDir, backupName);
|
|
24
|
+
|
|
25
|
+
// Ensure backup directory exists
|
|
26
|
+
FileSystemUtils.ensureDirectory(this.config.backupsDir);
|
|
27
|
+
|
|
28
|
+
// Check if backup already exists
|
|
29
|
+
if (fs.existsSync(backupPath)) {
|
|
30
|
+
throw new Error(`Backup '${backupName}' already exists`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Create backup directory
|
|
34
|
+
FileSystemUtils.ensureDirectory(backupPath);
|
|
35
|
+
|
|
36
|
+
// Reset counter
|
|
37
|
+
this.backedUpCount = 0;
|
|
38
|
+
|
|
39
|
+
// Backup components
|
|
40
|
+
const components = {};
|
|
41
|
+
let totalFiles = 0;
|
|
42
|
+
let totalSize = 0;
|
|
43
|
+
|
|
44
|
+
// Backup settings
|
|
45
|
+
if (FileSystemUtils.isReadable(this.config.settingsPath)) {
|
|
46
|
+
await this.backupSettings(backupPath);
|
|
47
|
+
components.settings = true;
|
|
48
|
+
totalFiles++;
|
|
49
|
+
const stats = FileSystemUtils.getStats(this.config.settingsPath);
|
|
50
|
+
if (stats) totalSize += stats.size;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Backup commands
|
|
54
|
+
const commandsResult = await this.backupCommands(backupPath);
|
|
55
|
+
if (commandsResult.count > 0) {
|
|
56
|
+
components.commands = true;
|
|
57
|
+
totalFiles += commandsResult.count;
|
|
58
|
+
totalSize += commandsResult.size;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Backup hooks
|
|
62
|
+
const hooksResult = await this.backupHooks(backupPath);
|
|
63
|
+
if (hooksResult.count > 0) {
|
|
64
|
+
components.hooks = true;
|
|
65
|
+
totalFiles += hooksResult.count;
|
|
66
|
+
totalSize += hooksResult.size;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create metadata
|
|
70
|
+
const metadata = this.createMetadata(backupName, components, totalFiles, totalSize);
|
|
71
|
+
await this.saveMetadata(backupPath, metadata);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
name: backupName,
|
|
75
|
+
path: backupPath,
|
|
76
|
+
components,
|
|
77
|
+
totalFiles,
|
|
78
|
+
totalSize,
|
|
79
|
+
metadata
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Backup settings.json file
|
|
85
|
+
*/
|
|
86
|
+
async backupSettings(backupPath) {
|
|
87
|
+
if (!FileSystemUtils.isReadable(this.config.settingsPath)) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const destPath = path.join(backupPath, 'settings.json');
|
|
92
|
+
const success = FileSystemUtils.copyFile(this.config.settingsPath, destPath);
|
|
93
|
+
|
|
94
|
+
if (success) {
|
|
95
|
+
this.backedUpCount++;
|
|
96
|
+
console.log('✅ Backed up settings.json');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return success;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Backup commands directory
|
|
104
|
+
*/
|
|
105
|
+
async backupCommands(backupPath) {
|
|
106
|
+
if (!fs.existsSync(this.config.commandsDir)) {
|
|
107
|
+
return { count: 0, size: 0 };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const commandsBackupDir = path.join(backupPath, 'commands');
|
|
111
|
+
FileSystemUtils.ensureDirectory(commandsBackupDir);
|
|
112
|
+
|
|
113
|
+
let count = 0;
|
|
114
|
+
let size = 0;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const files = fs.readdirSync(this.config.commandsDir);
|
|
118
|
+
|
|
119
|
+
for (const file of files) {
|
|
120
|
+
if (file.endsWith('.md')) {
|
|
121
|
+
const sourcePath = path.join(this.config.commandsDir, file);
|
|
122
|
+
const destPath = path.join(commandsBackupDir, file);
|
|
123
|
+
|
|
124
|
+
if (FileSystemUtils.copyFile(sourcePath, destPath)) {
|
|
125
|
+
count++;
|
|
126
|
+
const stats = FileSystemUtils.getStats(sourcePath);
|
|
127
|
+
if (stats) size += stats.size;
|
|
128
|
+
this.backedUpCount++;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (count > 0) {
|
|
134
|
+
console.log(`✅ Backed up ${count} commands`);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.warn(`⚠️ Warning: Could not backup commands - ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { count, size };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Backup hooks directory
|
|
145
|
+
*/
|
|
146
|
+
async backupHooks(backupPath) {
|
|
147
|
+
const hooksDir = this.config.hooksDir;
|
|
148
|
+
|
|
149
|
+
if (!fs.existsSync(hooksDir)) {
|
|
150
|
+
return { count: 0, size: 0 };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const hooksBackupDir = path.join(backupPath, 'hooks');
|
|
154
|
+
FileSystemUtils.ensureDirectory(hooksBackupDir);
|
|
155
|
+
|
|
156
|
+
let count = 0;
|
|
157
|
+
let size = 0;
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const files = fs.readdirSync(hooksDir);
|
|
161
|
+
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
const sourcePath = path.join(hooksDir, file);
|
|
164
|
+
const stats = FileSystemUtils.getStats(sourcePath);
|
|
165
|
+
|
|
166
|
+
if (stats && stats.isFile()) {
|
|
167
|
+
const destPath = path.join(hooksBackupDir, file);
|
|
168
|
+
|
|
169
|
+
if (FileSystemUtils.copyFile(sourcePath, destPath)) {
|
|
170
|
+
count++;
|
|
171
|
+
size += stats.size;
|
|
172
|
+
this.backedUpCount++;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (count > 0) {
|
|
178
|
+
console.log(`✅ Backed up ${count} hooks`);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.warn(`⚠️ Warning: Could not backup hooks - ${error.message}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { count, size };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create backup metadata
|
|
189
|
+
*/
|
|
190
|
+
createMetadata(name, components, totalFiles, totalSize) {
|
|
191
|
+
return {
|
|
192
|
+
name,
|
|
193
|
+
timestamp: new Date().toISOString(),
|
|
194
|
+
components,
|
|
195
|
+
totalFiles,
|
|
196
|
+
totalSize,
|
|
197
|
+
claudeVersion: this.getClaudeVersion(),
|
|
198
|
+
system: {
|
|
199
|
+
platform: require('os').platform(),
|
|
200
|
+
arch: require('os').arch(),
|
|
201
|
+
nodeVersion: process.version
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Save metadata to backup directory
|
|
208
|
+
*/
|
|
209
|
+
async saveMetadata(backupPath, metadata) {
|
|
210
|
+
const metadataPath = path.join(backupPath, 'backup-metadata.json');
|
|
211
|
+
const content = JSON.stringify(metadata, null, 2);
|
|
212
|
+
|
|
213
|
+
return FileSystemUtils.writeFile(metadataPath, content);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get Claude Code version
|
|
218
|
+
*/
|
|
219
|
+
getClaudeVersion() {
|
|
220
|
+
try {
|
|
221
|
+
const packagePath = path.join(__dirname, '..', '..', 'package.json');
|
|
222
|
+
const packageData = JSON.parse(FileSystemUtils.readFile(packagePath) || '{}');
|
|
223
|
+
return packageData.version || 'unknown';
|
|
224
|
+
} catch (error) {
|
|
225
|
+
return 'unknown';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = BackupService;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Installer Service
|
|
3
|
+
* Focused service for installing commands from the NPM package
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const FileSystemUtils = require('../utils/file-system-utils');
|
|
9
|
+
const ClaudePathConfig = require('../utils/claude-path-config');
|
|
10
|
+
|
|
11
|
+
class CommandInstallerService {
|
|
12
|
+
constructor(config = null) {
|
|
13
|
+
this.config = config || new ClaudePathConfig();
|
|
14
|
+
this.installedCount = 0;
|
|
15
|
+
this.skippedCount = 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Install commands based on options
|
|
20
|
+
*/
|
|
21
|
+
async install(options = {}) {
|
|
22
|
+
// Reset counters
|
|
23
|
+
this.installedCount = 0;
|
|
24
|
+
this.skippedCount = 0;
|
|
25
|
+
|
|
26
|
+
// Ensure commands directory exists
|
|
27
|
+
FileSystemUtils.ensureDirectory(this.config.commandsDir);
|
|
28
|
+
|
|
29
|
+
// Get commands to install
|
|
30
|
+
const commandsToInstall = this.getCommandsToInstall(options);
|
|
31
|
+
|
|
32
|
+
// Install commands
|
|
33
|
+
const installResults = {
|
|
34
|
+
active: 0,
|
|
35
|
+
experimental: 0
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
for (const cmd of commandsToInstall) {
|
|
39
|
+
const sourcePath = path.join(cmd.source, cmd.file);
|
|
40
|
+
const destPath = path.join(this.config.commandsDir, cmd.file);
|
|
41
|
+
|
|
42
|
+
if (FileSystemUtils.copyFile(sourcePath, destPath, 0o644)) {
|
|
43
|
+
this.installedCount++;
|
|
44
|
+
installResults[cmd.type]++;
|
|
45
|
+
} else {
|
|
46
|
+
console.error(`⚠️ Failed to install ${cmd.file}`);
|
|
47
|
+
this.skippedCount++;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
installedCount: this.installedCount,
|
|
53
|
+
skippedCount: this.skippedCount,
|
|
54
|
+
results: installResults,
|
|
55
|
+
commands: commandsToInstall
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get list of commands to install based on options
|
|
61
|
+
*/
|
|
62
|
+
getCommandsToInstall(options) {
|
|
63
|
+
const commands = [];
|
|
64
|
+
|
|
65
|
+
// Determine which command sets to install
|
|
66
|
+
const installActive = options.active || options.all ||
|
|
67
|
+
(!options.active && !options.experiments && !options.all);
|
|
68
|
+
const installExperiments = options.experiments || options.all;
|
|
69
|
+
|
|
70
|
+
// Collect active commands
|
|
71
|
+
if (installActive) {
|
|
72
|
+
const activeCommands = this.getCommandsFromDirectory(
|
|
73
|
+
this.config.packageActiveCommandsDir,
|
|
74
|
+
'active'
|
|
75
|
+
);
|
|
76
|
+
commands.push(...activeCommands);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Collect experimental commands
|
|
80
|
+
if (installExperiments) {
|
|
81
|
+
const experimentalCommands = this.getCommandsFromDirectory(
|
|
82
|
+
this.config.packageExperimentalCommandsDir,
|
|
83
|
+
'experimental'
|
|
84
|
+
);
|
|
85
|
+
commands.push(...experimentalCommands);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Apply include/exclude filters
|
|
89
|
+
return this.applyFilters(commands, options);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get commands from a specific directory
|
|
94
|
+
*/
|
|
95
|
+
getCommandsFromDirectory(dirPath, type) {
|
|
96
|
+
if (!fs.existsSync(dirPath)) {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
return fs.readdirSync(dirPath)
|
|
102
|
+
.filter(f => f.endsWith('.md'))
|
|
103
|
+
.map(f => ({
|
|
104
|
+
file: f,
|
|
105
|
+
source: dirPath,
|
|
106
|
+
type: type
|
|
107
|
+
}));
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.warn(`⚠️ Warning: Could not read ${type} commands directory - ${error.message}`);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Apply include/exclude filters to command list
|
|
116
|
+
*/
|
|
117
|
+
applyFilters(commands, options) {
|
|
118
|
+
let filteredCommands = commands;
|
|
119
|
+
|
|
120
|
+
// Apply include filter
|
|
121
|
+
if (options.include) {
|
|
122
|
+
const patterns = Array.isArray(options.include) ? options.include : [options.include];
|
|
123
|
+
filteredCommands = filteredCommands.filter(cmd =>
|
|
124
|
+
patterns.some(pattern => this.matchesPattern(cmd.file, pattern))
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Apply exclude filter
|
|
129
|
+
if (options.exclude) {
|
|
130
|
+
const patterns = Array.isArray(options.exclude) ? options.exclude : [options.exclude];
|
|
131
|
+
filteredCommands = filteredCommands.filter(cmd =>
|
|
132
|
+
!patterns.some(pattern => this.matchesPattern(cmd.file, pattern))
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return filteredCommands;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a command matches a pattern
|
|
141
|
+
*/
|
|
142
|
+
matchesPattern(filename, pattern) {
|
|
143
|
+
// Simple pattern matching with wildcard support
|
|
144
|
+
if (pattern.includes('*')) {
|
|
145
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
|
|
146
|
+
return regex.test(filename);
|
|
147
|
+
}
|
|
148
|
+
return filename.includes(pattern);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get preview of what would be installed (dry run)
|
|
153
|
+
*/
|
|
154
|
+
getDryRunPreview(options) {
|
|
155
|
+
const commandsToInstall = this.getCommandsToInstall(options);
|
|
156
|
+
|
|
157
|
+
const byType = {
|
|
158
|
+
active: commandsToInstall.filter(c => c.type === 'active'),
|
|
159
|
+
experimental: commandsToInstall.filter(c => c.type === 'experimental')
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
total: commandsToInstall.length,
|
|
164
|
+
byType,
|
|
165
|
+
destination: this.config.commandsDir
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate installation requirements
|
|
171
|
+
*/
|
|
172
|
+
validateInstallation() {
|
|
173
|
+
const issues = [];
|
|
174
|
+
|
|
175
|
+
// Check if package directories exist
|
|
176
|
+
if (!fs.existsSync(this.config.packageActiveCommandsDir)) {
|
|
177
|
+
issues.push('Active commands directory not found in package');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!fs.existsSync(this.config.packageExperimentalCommandsDir)) {
|
|
181
|
+
issues.push('Experimental commands directory not found in package');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if destination parent directory exists and is writable
|
|
185
|
+
const claudeDir = this.config.claudeDir;
|
|
186
|
+
try {
|
|
187
|
+
// Try to ensure the directory exists first
|
|
188
|
+
FileSystemUtils.ensureDirectory(claudeDir);
|
|
189
|
+
|
|
190
|
+
// Then check if it's writable
|
|
191
|
+
if (fs.existsSync(claudeDir) && !FileSystemUtils.isWritable(claudeDir)) {
|
|
192
|
+
issues.push('Cannot write to Claude directory - check permissions');
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// If we can't create the directory, that's a permission issue
|
|
196
|
+
issues.push(`Cannot create Claude directory: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
valid: issues.length === 0,
|
|
201
|
+
issues
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Get installation statistics
|
|
207
|
+
*/
|
|
208
|
+
getStats() {
|
|
209
|
+
return {
|
|
210
|
+
installed: this.installedCount,
|
|
211
|
+
skipped: this.skippedCount,
|
|
212
|
+
total: this.installedCount + this.skippedCount
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
module.exports = CommandInstallerService;
|