ai-sprint-kit 2.0.4 → 2.1.1

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.
@@ -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
+ };