@wundr.io/cli 1.0.0 → 1.0.2-dev.20260530174250.ef0ec927

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.
Files changed (230) hide show
  1. package/README.md +696 -280
  2. package/bin/wundr.js +13 -5
  3. package/package.json +30 -9
  4. package/src/ai/ai-service.ts +6 -4
  5. package/src/ai/claude-client.ts +6 -2
  6. package/src/ai/conversation-manager.ts +12 -5
  7. package/src/cli.ts +42 -13
  8. package/src/commands/ai.ts +340 -64
  9. package/src/commands/alignment.ts +1212 -0
  10. package/src/commands/analyze-optimized.ts +371 -33
  11. package/src/commands/analyze.ts +8 -6
  12. package/src/commands/batch.ts +166 -26
  13. package/src/commands/chat.ts +20 -10
  14. package/src/commands/claude-init.ts +31 -27
  15. package/src/commands/claude-setup.ts +761 -81
  16. package/src/commands/computer-setup.ts +524 -12
  17. package/src/commands/create-command.ts +3 -3
  18. package/src/commands/create.ts +9 -6
  19. package/src/commands/dashboard.ts +11 -6
  20. package/src/commands/govern.ts +11 -6
  21. package/src/commands/governance.ts +1005 -0
  22. package/src/commands/guardian.ts +887 -0
  23. package/src/commands/init.ts +104 -11
  24. package/src/commands/orchestrator.ts +789 -0
  25. package/src/commands/performance-optimizer.ts +15 -10
  26. package/src/commands/plugins.ts +8 -5
  27. package/src/commands/project-update.ts +1156 -0
  28. package/src/commands/rag.ts +1011 -0
  29. package/src/commands/session.ts +631 -0
  30. package/src/commands/setup.ts +42 -344
  31. package/src/commands/test-init.ts +3 -2
  32. package/src/commands/test.ts +3 -2
  33. package/src/commands/watch.ts +21 -11
  34. package/src/commands/worktree.ts +1057 -0
  35. package/src/context/context-manager.ts +5 -2
  36. package/src/context/session-manager.ts +18 -7
  37. package/src/framework/command-interface.ts +520 -0
  38. package/src/framework/command-registry.ts +942 -0
  39. package/src/framework/completion-exporter.ts +383 -0
  40. package/src/framework/debug-logger.ts +519 -0
  41. package/src/framework/error-handler.ts +867 -0
  42. package/src/framework/help-generator.ts +540 -0
  43. package/src/framework/index.ts +169 -0
  44. package/src/framework/interactive-repl.ts +703 -0
  45. package/src/framework/output-formatter.ts +834 -0
  46. package/src/framework/progress-manager.ts +539 -0
  47. package/src/index.ts +3 -2
  48. package/src/interactive/interactive-mode.ts +14 -7
  49. package/src/lib/conflict-resolution.ts +818 -0
  50. package/src/lib/merge-strategy.ts +550 -0
  51. package/src/lib/safety-mechanisms.ts +451 -0
  52. package/src/lib/state-detection.ts +1030 -0
  53. package/src/nlp/command-mapper.ts +8 -3
  54. package/src/nlp/command-parser.ts +5 -2
  55. package/src/nlp/intent-parser.ts +23 -9
  56. package/src/plugins/plugin-manager.ts +50 -24
  57. package/src/tests/computer-setup-integration.test.ts +470 -0
  58. package/src/types/index.ts +1 -1
  59. package/src/types/modules.d.ts +425 -1
  60. package/src/utils/backup-rollback-manager.ts +366 -0
  61. package/src/utils/claude-config-installer.ts +823 -0
  62. package/src/utils/config-manager.ts +9 -6
  63. package/src/utils/error-handler.ts +3 -1
  64. package/src/utils/logger.ts +35 -12
  65. package/templates/batch/ci-cd.yaml +7 -7
  66. package/test-suites/api/health.spec.ts +20 -23
  67. package/test-suites/helpers/test-config.ts +14 -13
  68. package/test-suites/ui/accessibility.spec.ts +27 -22
  69. package/test-suites/ui/smoke.spec.ts +26 -21
  70. package/dist/ai/ai-service.d.ts +0 -152
  71. package/dist/ai/ai-service.d.ts.map +0 -1
  72. package/dist/ai/ai-service.js +0 -430
  73. package/dist/ai/ai-service.js.map +0 -1
  74. package/dist/ai/claude-client.d.ts +0 -130
  75. package/dist/ai/claude-client.d.ts.map +0 -1
  76. package/dist/ai/claude-client.js +0 -339
  77. package/dist/ai/claude-client.js.map +0 -1
  78. package/dist/ai/conversation-manager.d.ts +0 -164
  79. package/dist/ai/conversation-manager.d.ts.map +0 -1
  80. package/dist/ai/conversation-manager.js +0 -612
  81. package/dist/ai/conversation-manager.js.map +0 -1
  82. package/dist/ai/index.d.ts +0 -5
  83. package/dist/ai/index.d.ts.map +0 -1
  84. package/dist/ai/index.js +0 -8
  85. package/dist/ai/index.js.map +0 -1
  86. package/dist/cli.d.ts +0 -36
  87. package/dist/cli.d.ts.map +0 -1
  88. package/dist/cli.js +0 -173
  89. package/dist/cli.js.map +0 -1
  90. package/dist/commands/ai.d.ts +0 -89
  91. package/dist/commands/ai.d.ts.map +0 -1
  92. package/dist/commands/ai.js +0 -735
  93. package/dist/commands/ai.js.map +0 -1
  94. package/dist/commands/analyze-optimized.d.ts +0 -14
  95. package/dist/commands/analyze-optimized.d.ts.map +0 -1
  96. package/dist/commands/analyze-optimized.js +0 -437
  97. package/dist/commands/analyze-optimized.js.map +0 -1
  98. package/dist/commands/analyze.d.ts +0 -65
  99. package/dist/commands/analyze.d.ts.map +0 -1
  100. package/dist/commands/analyze.js +0 -435
  101. package/dist/commands/analyze.js.map +0 -1
  102. package/dist/commands/batch.d.ts +0 -71
  103. package/dist/commands/batch.d.ts.map +0 -1
  104. package/dist/commands/batch.js +0 -738
  105. package/dist/commands/batch.js.map +0 -1
  106. package/dist/commands/chat.d.ts +0 -71
  107. package/dist/commands/chat.d.ts.map +0 -1
  108. package/dist/commands/chat.js +0 -674
  109. package/dist/commands/chat.js.map +0 -1
  110. package/dist/commands/claude-init.d.ts +0 -28
  111. package/dist/commands/claude-init.d.ts.map +0 -1
  112. package/dist/commands/claude-init.js +0 -587
  113. package/dist/commands/claude-init.js.map +0 -1
  114. package/dist/commands/claude-setup.d.ts +0 -32
  115. package/dist/commands/claude-setup.d.ts.map +0 -1
  116. package/dist/commands/claude-setup.js +0 -570
  117. package/dist/commands/claude-setup.js.map +0 -1
  118. package/dist/commands/computer-setup-commands.d.ts +0 -39
  119. package/dist/commands/computer-setup-commands.d.ts.map +0 -1
  120. package/dist/commands/computer-setup-commands.js +0 -563
  121. package/dist/commands/computer-setup-commands.js.map +0 -1
  122. package/dist/commands/computer-setup.d.ts +0 -7
  123. package/dist/commands/computer-setup.d.ts.map +0 -1
  124. package/dist/commands/computer-setup.js +0 -481
  125. package/dist/commands/computer-setup.js.map +0 -1
  126. package/dist/commands/create-command.d.ts +0 -7
  127. package/dist/commands/create-command.d.ts.map +0 -1
  128. package/dist/commands/create-command.js +0 -158
  129. package/dist/commands/create-command.js.map +0 -1
  130. package/dist/commands/create.d.ts +0 -74
  131. package/dist/commands/create.d.ts.map +0 -1
  132. package/dist/commands/create.js +0 -556
  133. package/dist/commands/create.js.map +0 -1
  134. package/dist/commands/dashboard.d.ts +0 -91
  135. package/dist/commands/dashboard.d.ts.map +0 -1
  136. package/dist/commands/dashboard.js +0 -537
  137. package/dist/commands/dashboard.js.map +0 -1
  138. package/dist/commands/govern.d.ts +0 -70
  139. package/dist/commands/govern.d.ts.map +0 -1
  140. package/dist/commands/govern.js +0 -480
  141. package/dist/commands/govern.js.map +0 -1
  142. package/dist/commands/init.d.ts +0 -55
  143. package/dist/commands/init.d.ts.map +0 -1
  144. package/dist/commands/init.js +0 -584
  145. package/dist/commands/init.js.map +0 -1
  146. package/dist/commands/performance-optimizer.d.ts +0 -30
  147. package/dist/commands/performance-optimizer.d.ts.map +0 -1
  148. package/dist/commands/performance-optimizer.js +0 -649
  149. package/dist/commands/performance-optimizer.js.map +0 -1
  150. package/dist/commands/plugins.d.ts +0 -87
  151. package/dist/commands/plugins.d.ts.map +0 -1
  152. package/dist/commands/plugins.js +0 -685
  153. package/dist/commands/plugins.js.map +0 -1
  154. package/dist/commands/setup.d.ts +0 -29
  155. package/dist/commands/setup.d.ts.map +0 -1
  156. package/dist/commands/setup.js +0 -399
  157. package/dist/commands/setup.js.map +0 -1
  158. package/dist/commands/test-init.d.ts +0 -9
  159. package/dist/commands/test-init.d.ts.map +0 -1
  160. package/dist/commands/test-init.js +0 -222
  161. package/dist/commands/test-init.js.map +0 -1
  162. package/dist/commands/test.d.ts +0 -25
  163. package/dist/commands/test.d.ts.map +0 -1
  164. package/dist/commands/test.js +0 -217
  165. package/dist/commands/test.js.map +0 -1
  166. package/dist/commands/watch.d.ts +0 -76
  167. package/dist/commands/watch.d.ts.map +0 -1
  168. package/dist/commands/watch.js +0 -610
  169. package/dist/commands/watch.js.map +0 -1
  170. package/dist/context/context-manager.d.ts +0 -155
  171. package/dist/context/context-manager.d.ts.map +0 -1
  172. package/dist/context/context-manager.js +0 -383
  173. package/dist/context/context-manager.js.map +0 -1
  174. package/dist/context/index.d.ts +0 -3
  175. package/dist/context/index.d.ts.map +0 -1
  176. package/dist/context/index.js +0 -6
  177. package/dist/context/index.js.map +0 -1
  178. package/dist/context/session-manager.d.ts +0 -207
  179. package/dist/context/session-manager.d.ts.map +0 -1
  180. package/dist/context/session-manager.js +0 -682
  181. package/dist/context/session-manager.js.map +0 -1
  182. package/dist/index.d.ts +0 -8
  183. package/dist/index.d.ts.map +0 -1
  184. package/dist/index.js +0 -51
  185. package/dist/index.js.map +0 -1
  186. package/dist/interactive/interactive-mode.d.ts +0 -76
  187. package/dist/interactive/interactive-mode.d.ts.map +0 -1
  188. package/dist/interactive/interactive-mode.js +0 -730
  189. package/dist/interactive/interactive-mode.js.map +0 -1
  190. package/dist/nlp/command-mapper.d.ts +0 -174
  191. package/dist/nlp/command-mapper.d.ts.map +0 -1
  192. package/dist/nlp/command-mapper.js +0 -623
  193. package/dist/nlp/command-mapper.js.map +0 -1
  194. package/dist/nlp/command-parser.d.ts +0 -106
  195. package/dist/nlp/command-parser.d.ts.map +0 -1
  196. package/dist/nlp/command-parser.js +0 -416
  197. package/dist/nlp/command-parser.js.map +0 -1
  198. package/dist/nlp/index.d.ts +0 -5
  199. package/dist/nlp/index.d.ts.map +0 -1
  200. package/dist/nlp/index.js +0 -8
  201. package/dist/nlp/index.js.map +0 -1
  202. package/dist/nlp/intent-classifier.d.ts +0 -59
  203. package/dist/nlp/intent-classifier.d.ts.map +0 -1
  204. package/dist/nlp/intent-classifier.js +0 -384
  205. package/dist/nlp/intent-classifier.js.map +0 -1
  206. package/dist/nlp/intent-parser.d.ts +0 -152
  207. package/dist/nlp/intent-parser.d.ts.map +0 -1
  208. package/dist/nlp/intent-parser.js +0 -739
  209. package/dist/nlp/intent-parser.js.map +0 -1
  210. package/dist/plugins/plugin-manager.d.ts +0 -120
  211. package/dist/plugins/plugin-manager.d.ts.map +0 -1
  212. package/dist/plugins/plugin-manager.js +0 -595
  213. package/dist/plugins/plugin-manager.js.map +0 -1
  214. package/dist/types/index.d.ts +0 -224
  215. package/dist/types/index.d.ts.map +0 -1
  216. package/dist/types/index.js +0 -3
  217. package/dist/types/index.js.map +0 -1
  218. package/dist/utils/config-manager.d.ts +0 -73
  219. package/dist/utils/config-manager.d.ts.map +0 -1
  220. package/dist/utils/config-manager.js +0 -339
  221. package/dist/utils/config-manager.js.map +0 -1
  222. package/dist/utils/error-handler.d.ts +0 -46
  223. package/dist/utils/error-handler.d.ts.map +0 -1
  224. package/dist/utils/error-handler.js +0 -169
  225. package/dist/utils/error-handler.js.map +0 -1
  226. package/dist/utils/logger.d.ts +0 -25
  227. package/dist/utils/logger.d.ts.map +0 -1
  228. package/dist/utils/logger.js +0 -94
  229. package/dist/utils/logger.js.map +0 -1
  230. package/src/commands/computer-setup-commands.ts +0 -709
@@ -1,7 +1,10 @@
1
+ import { spawn } from 'child_process';
1
2
  import { EventEmitter } from 'events';
2
- import { spawn, ChildProcess } from 'child_process';
3
+
3
4
  import { logger } from '../utils/logger';
4
- import { IntentResult } from './intent-parser';
5
+
6
+ import type { IntentResult } from './intent-parser';
7
+ import type { ChildProcess } from 'child_process';
5
8
 
6
9
  /**
7
10
  * Command execution result
@@ -807,7 +810,9 @@ export class CommandMapper extends EventEmitter {
807
810
  case 'directory_exists':
808
811
  const fsDir = await import('fs-extra');
809
812
  const dirPath = parameters[rule.rule as string];
810
- if (!dirPath) return false;
813
+ if (!dirPath) {
814
+ return false;
815
+ }
811
816
  const stats = await fsDir.stat(dirPath).catch(() => null);
812
817
  return stats ? stats.isDirectory() : false;
813
818
 
@@ -1,5 +1,6 @@
1
1
  import { logger } from '../utils/logger';
2
- import { AIService, ConversationContext } from '../ai/ai-service';
2
+
3
+ import type { AIService, ConversationContext } from '../ai/ai-service';
3
4
 
4
5
  /**
5
6
  * Natural language command parsing result
@@ -141,7 +142,9 @@ export class CommandParser {
141
142
 
142
143
  for (let i = 0; i < steps.length; i++) {
143
144
  const step = steps[i];
144
- if (!step) continue;
145
+ if (!step) {
146
+ continue;
147
+ }
145
148
 
146
149
  const stepResult = await this.parseCommand(step, context);
147
150
 
@@ -1,7 +1,9 @@
1
1
  import { EventEmitter } from 'events';
2
- import { ClaudeClient, ClaudeMessage } from '../ai/claude-client';
2
+
3
3
  import { logger } from '../utils/logger';
4
4
 
5
+ import type { ClaudeClient, ClaudeMessage } from '../ai/claude-client';
6
+
5
7
  /**
6
8
  * Intent classification result
7
9
  */
@@ -559,7 +561,9 @@ Extract parameters from the user input and respond with JSON only:
559
561
  };
560
562
 
561
563
  for (const [intent, pattern] of this.commandPatterns) {
562
- if (!availableCommands.includes(intent)) continue;
564
+ if (!availableCommands.includes(intent)) {
565
+ continue;
566
+ }
563
567
 
564
568
  const confidence = this.calculatePatternScore(input, pattern);
565
569
 
@@ -592,7 +596,9 @@ Extract parameters from the user input and respond with JSON only:
592
596
 
593
597
  let matchCount = 0;
594
598
  for (const word of patternWords) {
595
- if (word.startsWith('{') && word.endsWith('}')) continue; // Skip parameters
599
+ if (word.startsWith('{') && word.endsWith('}')) {
600
+ continue;
601
+ } // Skip parameters
596
602
  if (inputWords.includes(word)) {
597
603
  matchCount++;
598
604
  }
@@ -752,12 +758,13 @@ Analyze and respond with JSON only:
752
758
 
753
759
  if (patternResult.intent === aiResult.intent) {
754
760
  finalResult.confidence = Math.min(0.98, aiResult.confidence + 0.1);
755
- finalResult.reasoning += ` (confirmed by pattern matching)`;
761
+ finalResult.reasoning += ' (confirmed by pattern matching)';
756
762
  } else if (patternResult.confidence > 0.8) {
757
763
  // High-confidence pattern match might override AI
758
764
  if (patternResult.confidence > aiResult.confidence) {
759
765
  finalResult = patternResult;
760
- finalResult.reasoning += ` (overridden by high-confidence pattern match)`;
766
+ finalResult.reasoning +=
767
+ ' (overridden by high-confidence pattern match)';
761
768
  }
762
769
  }
763
770
 
@@ -784,7 +791,8 @@ Analyze and respond with JSON only:
784
791
  // Add safety warnings for destructive commands
785
792
  const commandPattern = this.commandPatterns.get(result.intent);
786
793
  if (commandPattern?.destructive) {
787
- result.clarification = `This is a destructive operation. Are you sure you want to proceed?`;
794
+ result.clarification =
795
+ 'This is a destructive operation. Are you sure you want to proceed?';
788
796
  }
789
797
 
790
798
  // Enrich with context-aware suggestions
@@ -922,7 +930,9 @@ Analyze and respond with JSON only:
922
930
 
923
931
  for (const command of availableCommands) {
924
932
  const pattern = this.commandPatterns.get(command);
925
- if (!pattern) continue;
933
+ if (!pattern) {
934
+ continue;
935
+ }
926
936
 
927
937
  // Check if partial input matches command or examples
928
938
  const confidence = Math.max(
@@ -1063,8 +1073,12 @@ Provide ${limit} suggestions in JSON format:
1063
1073
  negativeWords.includes(word)
1064
1074
  ).length;
1065
1075
 
1066
- if (positiveCount > negativeCount) return 'positive';
1067
- if (negativeCount > positiveCount) return 'negative';
1076
+ if (positiveCount > negativeCount) {
1077
+ return 'positive';
1078
+ }
1079
+ if (negativeCount > positiveCount) {
1080
+ return 'negative';
1081
+ }
1068
1082
  return 'neutral';
1069
1083
  }
1070
1084
 
@@ -1,11 +1,19 @@
1
- import fs from 'fs-extra';
2
- import path from 'path';
3
1
  import { spawn, type ChildProcess } from 'child_process';
2
+ import path from 'path';
3
+
4
4
  import chalk from 'chalk';
5
- import { ConfigManager } from '../utils/config-manager';
6
- import { logger } from '../utils/logger';
5
+ import fs from 'fs-extra';
6
+
7
7
  import { errorHandler } from '../utils/error-handler';
8
- import { Plugin, PluginContext, PluginCommand, PluginHook } from '../types';
8
+ import { logger } from '../utils/logger';
9
+
10
+ import type {
11
+ Plugin,
12
+ PluginContext,
13
+ PluginCommand,
14
+ PluginHook,
15
+ } from '../types';
16
+ import type { ConfigManager } from '../utils/config-manager';
9
17
 
10
18
  /**
11
19
  * Plugin management system for CLI extensibility
@@ -306,28 +314,46 @@ export class PluginManager {
306
314
  }
307
315
 
308
316
  /**
309
- * Get available plugins from registry
317
+ * Get available plugins by scanning the plugins directory on the filesystem.
318
+ * Returns an empty array if the directory does not exist or contains no valid plugins.
310
319
  */
311
320
  async getAvailablePlugins(): Promise<any[]> {
312
321
  try {
313
- // This would query a plugin registry
314
- // For now, return mock data
315
- return [
316
- {
317
- name: '@wundr/plugin-git',
318
- version: '1.0.0',
319
- description: 'Git integration plugin',
320
- downloads: 1000,
321
- updated: new Date().toISOString(),
322
- },
323
- {
324
- name: '@wundr/plugin-docker',
325
- version: '1.2.0',
326
- description: 'Docker integration plugin',
327
- downloads: 800,
328
- updated: new Date().toISOString(),
329
- },
330
- ];
322
+ if (!(await fs.pathExists(this.pluginsDir))) {
323
+ logger.debug(`Plugin directory not found: ${this.pluginsDir}`);
324
+ return [];
325
+ }
326
+
327
+ const entries = await fs.readdir(this.pluginsDir);
328
+ const plugins: any[] = [];
329
+
330
+ for (const entry of entries) {
331
+ const entryPath = path.join(this.pluginsDir, entry);
332
+ const packageJsonPath = path.join(entryPath, 'package.json');
333
+
334
+ if (!(await fs.pathExists(packageJsonPath))) {
335
+ continue;
336
+ }
337
+
338
+ try {
339
+ const packageJson = await fs.readJson(packageJsonPath);
340
+ const stat = await fs.stat(entryPath);
341
+ plugins.push({
342
+ name: packageJson.name || entry,
343
+ version: packageJson.version || 'unknown',
344
+ description: packageJson.description || '',
345
+ author: packageJson.author || '',
346
+ updated: stat.mtime.toISOString(),
347
+ });
348
+ } catch (parseError) {
349
+ logger.debug(
350
+ `Failed to read package.json for plugin ${entry}:`,
351
+ parseError
352
+ );
353
+ }
354
+ }
355
+
356
+ return plugins;
331
357
  } catch (error) {
332
358
  logger.debug('Failed to get available plugins:', error);
333
359
  return [];
@@ -0,0 +1,470 @@
1
+ /**
2
+ * Integration tests for computer-setup with Claude Code configuration
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ beforeEach,
10
+ afterEach,
11
+ jest,
12
+ } from '@jest/globals';
13
+ import * as fs from 'fs/promises';
14
+ import * as path from 'path';
15
+ import { existsSync } from 'fs';
16
+ import { BackupRollbackManager } from '../utils/backup-rollback-manager';
17
+ import { ClaudeConfigInstaller } from '../utils/claude-config-installer';
18
+
19
+ describe('Computer Setup Integration Tests', () => {
20
+ const testDir = path.join(__dirname, '.test-temp');
21
+ const backupDir = path.join(testDir, 'backups');
22
+ const claudeDir = path.join(testDir, '.claude');
23
+
24
+ beforeEach(async () => {
25
+ // Create test directories
26
+ await fs.mkdir(testDir, { recursive: true });
27
+ await fs.mkdir(backupDir, { recursive: true });
28
+ await fs.mkdir(claudeDir, { recursive: true });
29
+ });
30
+
31
+ afterEach(async () => {
32
+ // Clean up test directories
33
+ await fs.rm(testDir, { recursive: true, force: true });
34
+ });
35
+
36
+ describe('BackupRollbackManager', () => {
37
+ let backupManager: BackupRollbackManager;
38
+
39
+ beforeEach(async () => {
40
+ backupManager = new BackupRollbackManager(backupDir);
41
+ await backupManager.initialize();
42
+ });
43
+
44
+ it('should initialize backup directory', async () => {
45
+ expect(existsSync(backupDir)).toBe(true);
46
+ });
47
+
48
+ it('should create backup of files', async () => {
49
+ // Create test files
50
+ const testFile1 = path.join(testDir, 'test1.txt');
51
+ const testFile2 = path.join(testDir, 'test2.txt');
52
+ await fs.writeFile(testFile1, 'content1');
53
+ await fs.writeFile(testFile2, 'content2');
54
+
55
+ // Create backup
56
+ const metadata = await backupManager.createBackup(
57
+ [testFile1, testFile2],
58
+ 'Test backup'
59
+ );
60
+
61
+ expect(metadata.success).toBe(true);
62
+ expect(metadata.files).toHaveLength(2);
63
+ expect(metadata.backupId).toBeTruthy();
64
+
65
+ // Verify backup files exist
66
+ for (const file of metadata.files) {
67
+ expect(existsSync(file.backupPath)).toBe(true);
68
+ }
69
+ });
70
+
71
+ it('should list backups', async () => {
72
+ // Create test file and backup
73
+ const testFile = path.join(testDir, 'test.txt');
74
+ await fs.writeFile(testFile, 'content');
75
+
76
+ await backupManager.createBackup([testFile], 'Backup 1');
77
+ await backupManager.createBackup([testFile], 'Backup 2');
78
+
79
+ const backups = await backupManager.listBackups();
80
+ expect(backups.length).toBeGreaterThanOrEqual(2);
81
+ });
82
+
83
+ it('should restore files from backup', async () => {
84
+ // Create original file
85
+ const testFile = path.join(testDir, 'test.txt');
86
+ await fs.writeFile(testFile, 'original content');
87
+
88
+ // Create backup
89
+ const metadata = await backupManager.createBackup(
90
+ [testFile],
91
+ 'Test backup'
92
+ );
93
+
94
+ // Modify file
95
+ await fs.writeFile(testFile, 'modified content');
96
+
97
+ // Restore from backup
98
+ const success = await backupManager.rollback({
99
+ backupId: metadata.backupId,
100
+ dryRun: false,
101
+ });
102
+
103
+ expect(success).toBe(true);
104
+
105
+ // Verify content restored
106
+ const content = await fs.readFile(testFile, 'utf-8');
107
+ expect(content).toBe('original content');
108
+ });
109
+
110
+ it('should verify backup integrity', async () => {
111
+ // Create test file and backup
112
+ const testFile = path.join(testDir, 'test.txt');
113
+ await fs.writeFile(testFile, 'content');
114
+
115
+ const metadata = await backupManager.createBackup(
116
+ [testFile],
117
+ 'Test backup'
118
+ );
119
+
120
+ const isValid = await backupManager.verifyBackup(metadata.backupId);
121
+ expect(isValid).toBe(true);
122
+ });
123
+
124
+ it('should clean up old backups', async () => {
125
+ // Create multiple backups
126
+ const testFile = path.join(testDir, 'test.txt');
127
+ await fs.writeFile(testFile, 'content');
128
+
129
+ for (let i = 0; i < 10; i++) {
130
+ await backupManager.createBackup([testFile], `Backup ${i}`);
131
+ }
132
+
133
+ // Clean up, keeping only 5
134
+ await backupManager.cleanupOldBackups(5);
135
+
136
+ const backups = await backupManager.listBackups();
137
+ expect(backups.length).toBeLessThanOrEqual(5);
138
+ });
139
+
140
+ it('should handle dry-run rollback', async () => {
141
+ const testFile = path.join(testDir, 'test.txt');
142
+ await fs.writeFile(testFile, 'original');
143
+
144
+ const metadata = await backupManager.createBackup([testFile], 'Test');
145
+
146
+ await fs.writeFile(testFile, 'modified');
147
+
148
+ const success = await backupManager.rollback({
149
+ backupId: metadata.backupId,
150
+ dryRun: true,
151
+ });
152
+
153
+ expect(success).toBe(true);
154
+
155
+ // File should not be restored in dry-run
156
+ const content = await fs.readFile(testFile, 'utf-8');
157
+ expect(content).toBe('modified');
158
+ });
159
+ });
160
+
161
+ describe('ClaudeConfigInstaller', () => {
162
+ let installer: ClaudeConfigInstaller;
163
+
164
+ beforeEach(async () => {
165
+ installer = new ClaudeConfigInstaller({
166
+ claudeDir,
167
+ sourceDir: testDir,
168
+ });
169
+ await installer.initialize();
170
+
171
+ // Create mock CLAUDE.md
172
+ await fs.writeFile(
173
+ path.join(testDir, 'CLAUDE.md'),
174
+ '# Claude Configuration\nTest content'
175
+ );
176
+ });
177
+
178
+ it('should initialize directory structure', async () => {
179
+ expect(existsSync(claudeDir)).toBe(true);
180
+ expect(existsSync(path.join(claudeDir, 'hooks'))).toBe(true);
181
+ expect(existsSync(path.join(claudeDir, 'agents'))).toBe(true);
182
+ expect(existsSync(path.join(claudeDir, 'workflows'))).toBe(true);
183
+ expect(existsSync(path.join(claudeDir, 'scripts'))).toBe(true);
184
+ });
185
+
186
+ it('should install CLAUDE.md', async () => {
187
+ const result = await installer.install({
188
+ dryRun: false,
189
+ skipBackup: true,
190
+ });
191
+
192
+ expect(result.success).toBe(true);
193
+ expect(result.installed).toContain('CLAUDE.md');
194
+ expect(existsSync(path.join(claudeDir, 'CLAUDE.md'))).toBe(true);
195
+ });
196
+
197
+ it('should install hooks', async () => {
198
+ const result = await installer.install({
199
+ dryRun: false,
200
+ skipBackup: true,
201
+ });
202
+
203
+ expect(result.success).toBe(true);
204
+
205
+ const hooksDir = path.join(claudeDir, 'hooks');
206
+ expect(existsSync(path.join(hooksDir, 'pre-commit'))).toBe(true);
207
+ expect(existsSync(path.join(hooksDir, 'post-checkout'))).toBe(true);
208
+ });
209
+
210
+ it('should install conventions', async () => {
211
+ const result = await installer.install({
212
+ dryRun: false,
213
+ skipBackup: true,
214
+ });
215
+
216
+ expect(result.success).toBe(true);
217
+ expect(result.installed).toContain('conventions.json');
218
+
219
+ const conventionsPath = path.join(claudeDir, 'conventions.json');
220
+ expect(existsSync(conventionsPath)).toBe(true);
221
+
222
+ const conventions = JSON.parse(
223
+ await fs.readFile(conventionsPath, 'utf-8')
224
+ );
225
+ expect(conventions).toHaveProperty('fileNaming');
226
+ expect(conventions).toHaveProperty('codeStyle');
227
+ });
228
+
229
+ it('should install agent templates', async () => {
230
+ const result = await installer.install({
231
+ dryRun: false,
232
+ skipBackup: true,
233
+ });
234
+
235
+ expect(result.success).toBe(true);
236
+
237
+ const agentsDir = path.join(claudeDir, 'agents');
238
+ expect(existsSync(path.join(agentsDir, 'backend-developer.json'))).toBe(
239
+ true
240
+ );
241
+ expect(existsSync(path.join(agentsDir, 'frontend-developer.json'))).toBe(
242
+ true
243
+ );
244
+ expect(existsSync(path.join(agentsDir, 'fullstack-developer.json'))).toBe(
245
+ true
246
+ );
247
+ });
248
+
249
+ it('should install git-worktree workflows', async () => {
250
+ const result = await installer.install({
251
+ dryRun: false,
252
+ skipBackup: true,
253
+ });
254
+
255
+ expect(result.success).toBe(true);
256
+
257
+ const workflowsDir = path.join(claudeDir, 'workflows');
258
+ expect(
259
+ existsSync(path.join(workflowsDir, 'feature-development.json'))
260
+ ).toBe(true);
261
+ expect(existsSync(path.join(workflowsDir, 'bug-fix.json'))).toBe(true);
262
+ });
263
+
264
+ it('should install validation scripts', async () => {
265
+ const result = await installer.install({
266
+ dryRun: false,
267
+ skipBackup: true,
268
+ });
269
+
270
+ expect(result.success).toBe(true);
271
+
272
+ const scriptsDir = path.join(claudeDir, 'scripts');
273
+ expect(existsSync(path.join(scriptsDir, 'validate-setup.sh'))).toBe(true);
274
+ expect(existsSync(path.join(scriptsDir, 'check-config.sh'))).toBe(true);
275
+ });
276
+
277
+ it('should handle dry-run installation', async () => {
278
+ const result = await installer.install({
279
+ dryRun: true,
280
+ skipBackup: true,
281
+ });
282
+
283
+ expect(result.success).toBe(true);
284
+ expect(result.installed.every(f => f.includes('dry-run'))).toBe(true);
285
+
286
+ // Files should not exist in dry-run
287
+ expect(existsSync(path.join(claudeDir, 'CLAUDE.md'))).toBe(false);
288
+ });
289
+
290
+ it('should skip existing files without overwrite', async () => {
291
+ // Create existing file
292
+ await fs.writeFile(path.join(claudeDir, 'CLAUDE.md'), 'existing content');
293
+
294
+ const result = await installer.install({
295
+ dryRun: false,
296
+ skipBackup: true,
297
+ overwrite: false,
298
+ });
299
+
300
+ expect(result.skipped).toContain('CLAUDE.md');
301
+
302
+ // Content should remain unchanged
303
+ const content = await fs.readFile(
304
+ path.join(claudeDir, 'CLAUDE.md'),
305
+ 'utf-8'
306
+ );
307
+ expect(content).toBe('existing content');
308
+ });
309
+
310
+ it('should overwrite existing files with overwrite flag', async () => {
311
+ // Create existing file
312
+ await fs.writeFile(path.join(claudeDir, 'CLAUDE.md'), 'existing content');
313
+
314
+ const result = await installer.install({
315
+ dryRun: false,
316
+ skipBackup: true,
317
+ overwrite: true,
318
+ });
319
+
320
+ expect(result.installed).toContain('CLAUDE.md');
321
+
322
+ // Content should be updated
323
+ const content = await fs.readFile(
324
+ path.join(claudeDir, 'CLAUDE.md'),
325
+ 'utf-8'
326
+ );
327
+ expect(content).toContain('Test content');
328
+ });
329
+
330
+ it('should create backup before installation', async () => {
331
+ // Create existing configurations
332
+ await fs.writeFile(path.join(claudeDir, 'CLAUDE.md'), 'existing');
333
+ await fs.writeFile(
334
+ path.join(claudeDir, 'conventions.json'),
335
+ JSON.stringify({ test: true })
336
+ );
337
+
338
+ const result = await installer.install({
339
+ dryRun: false,
340
+ skipBackup: false,
341
+ overwrite: true,
342
+ });
343
+
344
+ expect(result.backupId).toBeTruthy();
345
+ });
346
+ });
347
+
348
+ describe('End-to-End Integration', () => {
349
+ it('should complete full installation workflow', async () => {
350
+ const backupManager = new BackupRollbackManager(backupDir);
351
+ const installer = new ClaudeConfigInstaller({
352
+ claudeDir,
353
+ sourceDir: testDir,
354
+ });
355
+
356
+ await backupManager.initialize();
357
+ await installer.initialize();
358
+
359
+ // Create mock source file
360
+ await fs.writeFile(
361
+ path.join(testDir, 'CLAUDE.md'),
362
+ '# Enhanced Claude Configuration'
363
+ );
364
+
365
+ // Install configurations
366
+ const installResult = await installer.install({
367
+ dryRun: false,
368
+ skipBackup: false,
369
+ });
370
+
371
+ expect(installResult.success).toBe(true);
372
+ expect(installResult.installed.length).toBeGreaterThan(0);
373
+
374
+ // Verify all components installed
375
+ expect(existsSync(path.join(claudeDir, 'CLAUDE.md'))).toBe(true);
376
+ expect(existsSync(path.join(claudeDir, 'conventions.json'))).toBe(true);
377
+ expect(existsSync(path.join(claudeDir, 'hooks', 'pre-commit'))).toBe(
378
+ true
379
+ );
380
+ expect(
381
+ existsSync(path.join(claudeDir, 'agents', 'backend-developer.json'))
382
+ ).toBe(true);
383
+ expect(
384
+ existsSync(
385
+ path.join(claudeDir, 'workflows', 'feature-development.json')
386
+ )
387
+ ).toBe(true);
388
+ expect(
389
+ existsSync(path.join(claudeDir, 'scripts', 'validate-setup.sh'))
390
+ ).toBe(true);
391
+ });
392
+
393
+ it('should support complete backup and rollback cycle', async () => {
394
+ const backupManager = new BackupRollbackManager(backupDir);
395
+ const installer = new ClaudeConfigInstaller({
396
+ claudeDir,
397
+ sourceDir: testDir,
398
+ });
399
+
400
+ await backupManager.initialize();
401
+ await installer.initialize();
402
+
403
+ // Create initial config
404
+ const initialConfig = path.join(claudeDir, 'CLAUDE.md');
405
+ await fs.writeFile(initialConfig, 'Initial configuration');
406
+
407
+ // Create backup
408
+ const backup = await backupManager.createBackup(
409
+ [initialConfig],
410
+ 'Pre-update backup'
411
+ );
412
+
413
+ // Update configuration
414
+ await fs.writeFile(initialConfig, 'Updated configuration');
415
+
416
+ // Rollback to original
417
+ const success = await backupManager.rollback({
418
+ backupId: backup.backupId,
419
+ dryRun: false,
420
+ });
421
+
422
+ expect(success).toBe(true);
423
+
424
+ // Verify rollback
425
+ const content = await fs.readFile(initialConfig, 'utf-8');
426
+ expect(content).toBe('Initial configuration');
427
+ });
428
+ });
429
+
430
+ describe('Error Handling', () => {
431
+ it('should handle missing source files gracefully', async () => {
432
+ const installer = new ClaudeConfigInstaller({
433
+ claudeDir,
434
+ sourceDir: testDir,
435
+ });
436
+
437
+ await installer.initialize();
438
+
439
+ // Don't create CLAUDE.md source
440
+ const result = await installer.install({
441
+ dryRun: false,
442
+ skipBackup: true,
443
+ });
444
+
445
+ // Should skip missing files
446
+ expect(result.skipped).toContain('CLAUDE.md');
447
+ });
448
+
449
+ it('should handle permission errors', async () => {
450
+ const installer = new ClaudeConfigInstaller({
451
+ claudeDir: '/root/unauthorized', // Likely no permission
452
+ sourceDir: testDir,
453
+ });
454
+
455
+ await expect(installer.initialize()).rejects.toThrow();
456
+ });
457
+
458
+ it('should handle corrupted backup metadata', async () => {
459
+ const backupManager = new BackupRollbackManager(backupDir);
460
+ await backupManager.initialize();
461
+
462
+ // Corrupt metadata file
463
+ const metadataPath = path.join(backupDir, 'metadata.json');
464
+ await fs.writeFile(metadataPath, 'invalid json {');
465
+
466
+ const backups = await backupManager.listBackups();
467
+ expect(backups).toEqual([]);
468
+ });
469
+ });
470
+ });
@@ -1,4 +1,4 @@
1
- import { Command } from 'commander';
1
+ import type { Command } from 'commander';
2
2
 
3
3
  /**
4
4
  * Core types for the Wundr CLI