ai-sprint-kit 2.0.4 → 2.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 +28 -14
- package/bin/ai-sprint.js +439 -8
- package/lib/ci-generator.js +248 -0
- package/lib/interactive.js +249 -0
- package/lib/recovery.js +279 -0
- package/lib/updater.js +340 -0
- package/lib/validator.js +264 -0
- package/package.json +3 -2
package/lib/recovery.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detect incomplete/partial installation
|
|
7
|
+
*/
|
|
8
|
+
async function detectPartialInstallation(targetDir) {
|
|
9
|
+
const checks = {
|
|
10
|
+
hasClaudeDir: false,
|
|
11
|
+
hasAgents: false,
|
|
12
|
+
hasCommands: false,
|
|
13
|
+
hasSettings: false,
|
|
14
|
+
hasClaudeMd: false,
|
|
15
|
+
hasTempDir: false
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Check .claude directory
|
|
19
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(claudeDir);
|
|
22
|
+
checks.hasClaudeDir = true;
|
|
23
|
+
} catch {}
|
|
24
|
+
|
|
25
|
+
// Check agents
|
|
26
|
+
const agentsDir = path.join(claudeDir, 'agents');
|
|
27
|
+
try {
|
|
28
|
+
const files = await fs.readdir(agentsDir);
|
|
29
|
+
checks.hasAgents = files.filter(f => f.endsWith('.md')).length >= 5;
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
// Check commands
|
|
33
|
+
const commandsDir = path.join(claudeDir, 'commands');
|
|
34
|
+
try {
|
|
35
|
+
const files = await fs.readdir(commandsDir);
|
|
36
|
+
checks.hasCommands = files.filter(f => f.endsWith('.md')).length >= 8;
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
// Check settings
|
|
40
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
41
|
+
try {
|
|
42
|
+
await fs.access(settingsPath);
|
|
43
|
+
checks.hasSettings = true;
|
|
44
|
+
} catch {}
|
|
45
|
+
|
|
46
|
+
// Check CLAUDE.md
|
|
47
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
48
|
+
try {
|
|
49
|
+
await fs.access(claudeMdPath);
|
|
50
|
+
checks.hasClaudeMd = true;
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
// Check temp directory (sign of failed install)
|
|
54
|
+
const tempDir = path.join(targetDir, '.ai-sprint-temp');
|
|
55
|
+
try {
|
|
56
|
+
await fs.access(tempDir);
|
|
57
|
+
checks.hasTempDir = true;
|
|
58
|
+
} catch {}
|
|
59
|
+
|
|
60
|
+
// Determine if installation is partial
|
|
61
|
+
const passedChecks = Object.values(checks).filter(Boolean).length;
|
|
62
|
+
const isPartial = passedChecks > 0 && passedChecks < 6;
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
isPartial,
|
|
66
|
+
isComplete: passedChecks === 6,
|
|
67
|
+
isEmpty: passedChecks === 0,
|
|
68
|
+
checks,
|
|
69
|
+
passedChecks
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Clean up temporary files from failed installations
|
|
75
|
+
*/
|
|
76
|
+
async function cleanupTempFiles(targetDir) {
|
|
77
|
+
const tempDirs = [
|
|
78
|
+
'.ai-sprint-temp',
|
|
79
|
+
'.ai-sprint-temp-update',
|
|
80
|
+
'.ai-sprint-backup'
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
const cleaned = [];
|
|
84
|
+
|
|
85
|
+
for (const dir of tempDirs) {
|
|
86
|
+
const dirPath = path.join(targetDir, dir);
|
|
87
|
+
try {
|
|
88
|
+
await fs.access(dirPath);
|
|
89
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
90
|
+
cleaned.push(dir);
|
|
91
|
+
} catch {}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return cleaned;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Repair installation by fixing missing components
|
|
99
|
+
*/
|
|
100
|
+
async function repairInstallation(targetDir) {
|
|
101
|
+
const { cloneProRepo, copyProContent, cleanup } = require('./installer');
|
|
102
|
+
|
|
103
|
+
// Step 1: Detect issues
|
|
104
|
+
const diagnosis = await detectPartialInstallation(targetDir);
|
|
105
|
+
|
|
106
|
+
// Step 2: Clean temp files
|
|
107
|
+
const cleaned = await cleanupTempFiles(targetDir);
|
|
108
|
+
|
|
109
|
+
// Step 3: Clone latest version
|
|
110
|
+
const tempDir = await cloneProRepo(targetDir);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
// Step 4: Copy missing files (force=true to overwrite broken files)
|
|
114
|
+
await copyProContent(tempDir, targetDir, { force: true });
|
|
115
|
+
|
|
116
|
+
// Step 5: Cleanup
|
|
117
|
+
await cleanup(targetDir);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
diagnosis,
|
|
122
|
+
cleaned,
|
|
123
|
+
message: 'Installation repaired successfully'
|
|
124
|
+
};
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Cleanup on failure
|
|
127
|
+
await cleanup(targetDir);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Perform clean reinstall
|
|
134
|
+
*/
|
|
135
|
+
async function reinstallInstallation(targetDir) {
|
|
136
|
+
const { cloneProRepo, copyProContent, cleanup, checkExisting } = require('./installer');
|
|
137
|
+
|
|
138
|
+
// Step 1: Remove existing installation
|
|
139
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
140
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
141
|
+
const aiContextDir = path.join(targetDir, 'ai_context');
|
|
142
|
+
|
|
143
|
+
const removed = [];
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
await fs.access(claudeDir);
|
|
147
|
+
await fs.rm(claudeDir, { recursive: true, force: true });
|
|
148
|
+
removed.push('.claude/');
|
|
149
|
+
} catch {}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
await fs.access(claudeMdPath);
|
|
153
|
+
await fs.rm(claudeMdPath);
|
|
154
|
+
removed.push('CLAUDE.md');
|
|
155
|
+
} catch {}
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
await fs.access(aiContextDir);
|
|
159
|
+
await fs.rm(aiContextDir, { recursive: true, force: true });
|
|
160
|
+
removed.push('ai_context/');
|
|
161
|
+
} catch {}
|
|
162
|
+
|
|
163
|
+
// Step 2: Clean temp files
|
|
164
|
+
const cleaned = await cleanupTempFiles(targetDir);
|
|
165
|
+
|
|
166
|
+
// Step 3: Fresh install
|
|
167
|
+
const tempDir = await cloneProRepo(targetDir);
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
await copyProContent(tempDir, targetDir, { force: false });
|
|
171
|
+
await cleanup(targetDir);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
removed,
|
|
176
|
+
cleaned,
|
|
177
|
+
message: 'Clean reinstall completed'
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
await cleanup(targetDir);
|
|
181
|
+
throw error;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Diagnose installation issues
|
|
187
|
+
*/
|
|
188
|
+
async function diagnoseIssues(targetDir) {
|
|
189
|
+
const issues = [];
|
|
190
|
+
const suggestions = [];
|
|
191
|
+
|
|
192
|
+
// Check temp directories
|
|
193
|
+
const tempDir = path.join(targetDir, '.ai-sprint-temp');
|
|
194
|
+
try {
|
|
195
|
+
await fs.access(tempDir);
|
|
196
|
+
issues.push('Temporary installation directory exists');
|
|
197
|
+
suggestions.push('Run: ai-sprint repair (or manually remove .ai-sprint-temp)');
|
|
198
|
+
} catch {}
|
|
199
|
+
|
|
200
|
+
// Check .claude directory
|
|
201
|
+
const claudeDir = path.join(targetDir, '.claude');
|
|
202
|
+
try {
|
|
203
|
+
const stat = await fs.stat(claudeDir);
|
|
204
|
+
if (!stat.isDirectory()) {
|
|
205
|
+
issues.push('.claude exists but is not a directory');
|
|
206
|
+
suggestions.push('Remove the .claude file and run: ai-sprint init');
|
|
207
|
+
}
|
|
208
|
+
} catch {
|
|
209
|
+
issues.push('.claude directory missing');
|
|
210
|
+
suggestions.push('Run: ai-sprint init');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check if CLAUDE.md is valid
|
|
214
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
215
|
+
try {
|
|
216
|
+
const content = await fs.readFile(claudeMdPath, 'utf8');
|
|
217
|
+
if (!content.includes('AI Sprint') && !content.includes('Claude Code')) {
|
|
218
|
+
issues.push('CLAUDE.md exists but may not be AI Sprint Kit');
|
|
219
|
+
suggestions.push('Run: ai-sprint reinstall');
|
|
220
|
+
}
|
|
221
|
+
} catch {
|
|
222
|
+
issues.push('CLAUDE.md missing');
|
|
223
|
+
suggestions.push('Run: ai-sprint init or ai-sprint repair');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check permissions
|
|
227
|
+
try {
|
|
228
|
+
await fs.access(claudeDir, fs.constants.W_OK);
|
|
229
|
+
} catch {
|
|
230
|
+
issues.push('No write permission to .claude directory');
|
|
231
|
+
suggestions.push('Fix permissions: chmod +w .claude');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { issues, suggestions };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Get recovery options based on installation state
|
|
239
|
+
*/
|
|
240
|
+
async function getRecoveryOptions(targetDir) {
|
|
241
|
+
const diagnosis = await detectPartialInstallation(targetDir);
|
|
242
|
+
const issues = await diagnoseIssues(targetDir);
|
|
243
|
+
|
|
244
|
+
const options = {
|
|
245
|
+
canRepair: false,
|
|
246
|
+
canReinstall: false,
|
|
247
|
+
recommended: null
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (diagnosis.isEmpty) {
|
|
251
|
+
options.canReinstall = true;
|
|
252
|
+
options.recommended = 'init';
|
|
253
|
+
} else if (diagnosis.isPartial) {
|
|
254
|
+
options.canRepair = true;
|
|
255
|
+
options.canReinstall = true;
|
|
256
|
+
options.recommended = 'repair';
|
|
257
|
+
} else if (diagnosis.isComplete && issues.issues.length > 0) {
|
|
258
|
+
options.canRepair = true;
|
|
259
|
+
options.canReinstall = true;
|
|
260
|
+
options.recommended = 'repair';
|
|
261
|
+
} else if (diagnosis.isComplete) {
|
|
262
|
+
options.recommended = 'validate';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
diagnosis,
|
|
267
|
+
issues,
|
|
268
|
+
options
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
detectPartialInstallation,
|
|
274
|
+
cleanupTempFiles,
|
|
275
|
+
repairInstallation,
|
|
276
|
+
reinstallInstallation,
|
|
277
|
+
diagnoseIssues,
|
|
278
|
+
getRecoveryOptions
|
|
279
|
+
};
|
package/lib/updater.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execFileSync } = require('child_process');
|
|
4
|
+
const semver = require('semver');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Get installed version from CLAUDE.md
|
|
8
|
+
*/
|
|
9
|
+
async function getInstalledVersion(targetDir) {
|
|
10
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
11
|
+
try {
|
|
12
|
+
const content = await fs.readFile(claudeMdPath, 'utf8');
|
|
13
|
+
const match = content.match(/Version:\s*(\d+\.\d+\.\d+)/);
|
|
14
|
+
if (match) {
|
|
15
|
+
return match[1];
|
|
16
|
+
}
|
|
17
|
+
} catch {
|
|
18
|
+
// CLAUDE.md might not exist or be unreadable
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get latest version from GitHub repository
|
|
25
|
+
*/
|
|
26
|
+
async function getLatestVersion() {
|
|
27
|
+
try {
|
|
28
|
+
// Use gh CLI to get latest release tag
|
|
29
|
+
const output = execFileSync('gh', [
|
|
30
|
+
'release',
|
|
31
|
+
'list',
|
|
32
|
+
'--repo', 'apiasak/ai-sprint-pro',
|
|
33
|
+
'--limit', '1'
|
|
34
|
+
], { encoding: 'utf8' });
|
|
35
|
+
|
|
36
|
+
// Output format: "v2.0.0 Release title"
|
|
37
|
+
const match = output.trim().match(/^v(\d+\.\d+\.\d+)/);
|
|
38
|
+
if (match) {
|
|
39
|
+
return match[1];
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new Error('Failed to fetch latest version from GitHub');
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if update is available
|
|
49
|
+
*/
|
|
50
|
+
async function checkUpdate(targetDir = process.cwd()) {
|
|
51
|
+
const installed = await getInstalledVersion(targetDir);
|
|
52
|
+
const latest = await getLatestVersion();
|
|
53
|
+
|
|
54
|
+
if (!installed) {
|
|
55
|
+
return {
|
|
56
|
+
hasUpdate: false,
|
|
57
|
+
installed: null,
|
|
58
|
+
latest,
|
|
59
|
+
message: 'Unable to determine installed version'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!latest) {
|
|
64
|
+
return {
|
|
65
|
+
hasUpdate: false,
|
|
66
|
+
installed,
|
|
67
|
+
latest: null,
|
|
68
|
+
message: 'Unable to fetch latest version'
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hasUpdate = semver.gt(latest, installed);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
hasUpdate,
|
|
76
|
+
installed,
|
|
77
|
+
latest,
|
|
78
|
+
message: hasUpdate
|
|
79
|
+
? `Update available: ${installed} → ${latest}`
|
|
80
|
+
: `Already up to date (${installed})`
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Backup configuration files
|
|
86
|
+
*/
|
|
87
|
+
async function backupConfig(targetDir) {
|
|
88
|
+
const backupDir = path.join(targetDir, '.ai-sprint-backup');
|
|
89
|
+
const configFiles = [
|
|
90
|
+
'.claude/settings.json',
|
|
91
|
+
'.claude/.env',
|
|
92
|
+
'.mcp.json'
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// Create backup directory
|
|
97
|
+
await fs.mkdir(backupDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
// Copy each config file if it exists
|
|
100
|
+
for (const file of configFiles) {
|
|
101
|
+
const sourcePath = path.join(targetDir, file);
|
|
102
|
+
const backupPath = path.join(backupDir, path.basename(file));
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await fs.copyFile(sourcePath, backupPath);
|
|
106
|
+
} catch {
|
|
107
|
+
// File doesn't exist, skip
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return backupDir;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
throw new Error(`Failed to backup config: ${error.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Restore configuration files from backup
|
|
119
|
+
*/
|
|
120
|
+
async function restoreConfig(targetDir, backupDir) {
|
|
121
|
+
const configFiles = [
|
|
122
|
+
['settings.json', '.claude/settings.json'],
|
|
123
|
+
['.env', '.claude/.env'],
|
|
124
|
+
['.mcp.json', '.mcp.json']
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
for (const [backupFile, targetFile] of configFiles) {
|
|
129
|
+
const sourcePath = path.join(backupDir, backupFile);
|
|
130
|
+
const destPath = path.join(targetDir, targetFile);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await fs.copyFile(sourcePath, destPath);
|
|
134
|
+
} catch {
|
|
135
|
+
// Backup file doesn't exist, skip
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(`Failed to restore config: ${error.message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get changelog between versions
|
|
145
|
+
*/
|
|
146
|
+
async function getChangelog(fromVersion, toVersion) {
|
|
147
|
+
try {
|
|
148
|
+
const output = execFileSync('gh', [
|
|
149
|
+
'release',
|
|
150
|
+
'view',
|
|
151
|
+
`v${toVersion}`,
|
|
152
|
+
'--repo', 'apiasak/ai-sprint-pro',
|
|
153
|
+
'--json', 'body'
|
|
154
|
+
], { encoding: 'utf8' });
|
|
155
|
+
|
|
156
|
+
const data = JSON.parse(output);
|
|
157
|
+
return data.body || 'No changelog available';
|
|
158
|
+
} catch {
|
|
159
|
+
return 'Changelog not available';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Update AI Sprint Kit installation
|
|
165
|
+
*/
|
|
166
|
+
async function updateInstallation(targetDir = process.cwd(), options = {}) {
|
|
167
|
+
const {
|
|
168
|
+
force = false,
|
|
169
|
+
backup = true
|
|
170
|
+
} = options;
|
|
171
|
+
|
|
172
|
+
// Step 1: Check for updates
|
|
173
|
+
const updateInfo = await checkUpdate(targetDir);
|
|
174
|
+
|
|
175
|
+
if (!updateInfo.hasUpdate && !force) {
|
|
176
|
+
return {
|
|
177
|
+
success: false,
|
|
178
|
+
message: updateInfo.message,
|
|
179
|
+
installed: updateInfo.installed,
|
|
180
|
+
latest: updateInfo.latest
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Step 2: Backup config
|
|
185
|
+
let backupDir = null;
|
|
186
|
+
if (backup) {
|
|
187
|
+
backupDir = await backupConfig(targetDir);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 3: Clone latest version to temp directory
|
|
191
|
+
const tempDir = path.join(targetDir, '.ai-sprint-temp-update');
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Remove temp dir if exists
|
|
195
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
196
|
+
|
|
197
|
+
// Clone latest version
|
|
198
|
+
execFileSync('git', [
|
|
199
|
+
'clone',
|
|
200
|
+
'--depth', '1',
|
|
201
|
+
'--branch', `v${updateInfo.latest}`,
|
|
202
|
+
'git@github.com:apiasak/ai-sprint-pro.git',
|
|
203
|
+
tempDir
|
|
204
|
+
], { stdio: 'pipe' });
|
|
205
|
+
|
|
206
|
+
// Step 4: Copy new files (similar to installer.js)
|
|
207
|
+
await copyProContent(tempDir, targetDir, { force: true });
|
|
208
|
+
|
|
209
|
+
// Step 5: Restore config
|
|
210
|
+
if (backup && backupDir) {
|
|
211
|
+
await restoreConfig(targetDir, backupDir);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Step 6: Get changelog
|
|
215
|
+
const changelog = await getChangelog(updateInfo.installed, updateInfo.latest);
|
|
216
|
+
|
|
217
|
+
// Step 7: Cleanup
|
|
218
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
219
|
+
if (backupDir) {
|
|
220
|
+
await fs.rm(backupDir, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
message: `Updated from ${updateInfo.installed} to ${updateInfo.latest}`,
|
|
226
|
+
from: updateInfo.installed,
|
|
227
|
+
to: updateInfo.latest,
|
|
228
|
+
changelog
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
// Cleanup on failure
|
|
232
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
233
|
+
if (backupDir) {
|
|
234
|
+
await fs.rm(backupDir, { recursive: true, force: true });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
throw new Error(`Update failed: ${error.message}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Copy pro content (from installer.js, reused here)
|
|
243
|
+
*/
|
|
244
|
+
async function copyProContent(sourceDir, targetDir, options = {}) {
|
|
245
|
+
const { force = false } = options;
|
|
246
|
+
|
|
247
|
+
const itemsToCopy = [
|
|
248
|
+
{ src: '.claude', dest: '.claude' },
|
|
249
|
+
{ src: 'ai_context', dest: 'ai_context' },
|
|
250
|
+
{ src: 'CLAUDE.md', dest: 'CLAUDE.md' },
|
|
251
|
+
{ src: 'docs', dest: '.claude/docs' }
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
for (const item of itemsToCopy) {
|
|
255
|
+
const sourcePath = path.join(sourceDir, item.src);
|
|
256
|
+
const destPath = path.join(targetDir, item.dest);
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const stat = await fs.stat(sourcePath);
|
|
260
|
+
|
|
261
|
+
if (stat.isDirectory()) {
|
|
262
|
+
await copyDirectory(sourcePath, destPath, force);
|
|
263
|
+
} else {
|
|
264
|
+
await copyFile(sourcePath, destPath, force);
|
|
265
|
+
}
|
|
266
|
+
} catch (error) {
|
|
267
|
+
throw new Error(`Failed to copy ${item.src}: ${error.message}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Copy directory recursively
|
|
274
|
+
*/
|
|
275
|
+
async function copyDirectory(src, dest, force) {
|
|
276
|
+
const exists = await fileExists(dest);
|
|
277
|
+
|
|
278
|
+
if (exists && !force) {
|
|
279
|
+
return; // Skip if exists and not forcing
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (exists && force) {
|
|
283
|
+
await fs.rm(dest, { recursive: true, force: true });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await fs.mkdir(dest, { recursive: true });
|
|
287
|
+
|
|
288
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
289
|
+
|
|
290
|
+
for (const entry of entries) {
|
|
291
|
+
const srcPath = path.join(src, entry.name);
|
|
292
|
+
const destPath = path.join(dest, entry.name);
|
|
293
|
+
|
|
294
|
+
if (entry.isDirectory()) {
|
|
295
|
+
await copyDirectory(srcPath, destPath, force);
|
|
296
|
+
} else {
|
|
297
|
+
await fs.copyFile(srcPath, destPath);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Copy single file with force option
|
|
304
|
+
*/
|
|
305
|
+
async function copyFile(src, dest, force) {
|
|
306
|
+
const exists = await fileExists(dest);
|
|
307
|
+
|
|
308
|
+
if (exists && !force) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (exists && force) {
|
|
313
|
+
await fs.rm(dest);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await fs.copyFile(src, dest);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Check if file exists
|
|
321
|
+
*/
|
|
322
|
+
async function fileExists(filePath) {
|
|
323
|
+
try {
|
|
324
|
+
await fs.access(filePath);
|
|
325
|
+
return true;
|
|
326
|
+
} catch {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
module.exports = {
|
|
332
|
+
getInstalledVersion,
|
|
333
|
+
getLatestVersion,
|
|
334
|
+
checkUpdate,
|
|
335
|
+
backupConfig,
|
|
336
|
+
restoreConfig,
|
|
337
|
+
getChangelog,
|
|
338
|
+
updateInstallation,
|
|
339
|
+
copyProContent
|
|
340
|
+
};
|